Skip to content

docs(gate): the eager-closure gate's prose stops writing counts it cannot keep - #7858

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-7528-eager-closure-frozen-counts
Sep 6, 2026
Merged

os-sam merged 1 commit into
mainfrom
claude/issue-7528-eager-closure-frozen-counts

Conversation

@claude

@claude claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #7528

The deliverable is "stop writing the count", not "52 becomes 50"

Three sentences described the aggregate as "one number over 52 chunks". The
card's diagnosis is the whole design brief: the count is not load-bearing for
any verdict
— the sentence is rhetorical ("one total over N chunks cannot say
WHERE the payload moved"
) — which is exactly why nothing ever caught it. And
today there were three different counts in play, so refreshing one literal
would have turned three into two and let the next build spread them again.

Each sentence now states its population and leaves the figure to the gate, which
prints it on every run. The checker's header carries the rule, generalised from
the one that file already applies to its own motivating table:

That table is the MOTIVATING measurement and stays pinned to 77f846a8b; it is
not the current reading.

An anchored count is a measurement of a named build and cannot go stale; an
unanchored one is a standing claim about the live closure, which moves on most
builds. That is also the distinction the card used to leave
vite-declared-lazy-views.ts's "42.5 KB of headroom" alone — it sits in a
paragraph dated to a b98352a15 measurement and reads as history.

The numbers, measured on this branch, not copied from the card

⚠️ A full pnpm turbo run build --filter='./packages/*' plus
pnpm --filter @object-ui/console build on 52cac3886, then
pnpm check:eager-closure:

reading card said measured here
the gate's live verdict line 50 of 518 across **50 of 518** chunks
the prose 52 52
the file's own BASELINE.chunks 48 48
eager closure, gzipped 3,258,288 B (on f620a26f6) 3,263,896 B

The three counts confirmed; the payload has grown 5,608 bytes since the card was
written. BASELINE.chunks is not a fourth defect and is untouched: it names
the commit it was measured on (3d257c85a), which is the anchored form the rule
asks for.

Site 2 — the workflow cited a ceiling that no longer exists

⚠️ Not a drifted number: 3.15 MB is 3,300,000 bytes, the ceiling objectui#6683
set and objectui#6776 retired. Established on today's tree before rewriting,
rather than assumed: the criterion actually in force is
MAX_EAGER_CLOSURE_GZIP_BYTES = 3_268_000 (3.12 MiB), plus the objectui#5924
sensitivity half that weighs each ceiling against the report it just read. So a
sentence whose entire purpose is to stop a reader concluding the payload is
acceptable was itself quoting a size the gate had not allowed for two
re-baselines.

The repair names no size at all — which is the rule the same paragraph already
applies to the ceiling itself ("The closure ceiling is NOT set here. It lives in
scripts/check-eager-closure-budget.mjs next to the reasoning that produced
it"
), now applied to the ceiling's value too. The removed literal is not quoted
back, for the same reason the pin refuses quotations.

The pin

One it.each in the gate's own test file — one gate, one home. It fails when a
numeral qualifies a chunk noun in either script's prose and no commit in its
window anchors it
, so the class fails loudly rather than waiting to be read.

Deliberately narrow, each limit a decision: numerals qualifying a chunk noun
(the file's "144 modules" and "166 modules" are module counts inside anchored
change records); own-line comments; and an anchor window of a paragraph plus the
one before it, because the header writes "Measured on 77f846a8b:" as a lead-in
and its table as a separate paragraph. A line of code breaks that window, so a
hash in one block never vouches for a count in another. The window errs toward
allowing, which is the tolerable side for a check on prose; the intolerable side
is a standing claim nobody refuses.

⚠️ It also means this diff may not quote its own old literals back — the pin
cannot tell a quotation from a claim. That is recorded in the docblock, and it is
why the prose above says "N" where the number used to be.

Ablation — three legs, and the sharp one is (b)

(a) The genuine pre-fix prose, pin retained. Both files went red, each naming
itself and the exact literals:

× check-eager-closure-budget.mjs writes no chunk count that no commit anchors
AssertionError: check-eager-closure-budget.mjs states a chunk count (52 chunks, 52 chunks) that names no commit …
  expected [ '52 chunks', '52 chunks' ] to deeply equal []
AssertionError: check-eager-closure-budget.test.ts states a chunk count (52 chunks) …
Tests  2 failed | 99 passed (101)

(b) A literal that is CORRECT TODAY — the leg that proves the pin governs the
class of writing rather than wrong numbers. 50 chunks and 50 of 518 chunks are
exactly what the gate printed an hour earlier, and both are still refused:

expected [ '50 chunks' ] to deeply equal []
expected [ '50 of 518 chunks' ] to deeply equal []
Tests  2 failed | 99 passed (101)

(c) The same today-correct count, ANCHORED — "Measured on 52cac3886, the
closure held 50 of 518 chunks" — is accepted (101 passed). So the pin is not
"no digits near chunks"; it is the anchoring rule, and the escape hatch its own
failure message offers actually works.

Ordering, and it is the reason all three legs are trustworthy: the implementation
was committed first, and only then was the restore trap armed — the sibling
seat on objectui#7448 had its trap fire on a precondition guard's own exit and
silently restore both files to a HEAD that did not contain the fix. Each leg
re-checked that HEAD carried the fix before arming (exit 92 if not). Mutation
proven on disk each time (blob hash flip plus an injected-anchor count) before
running; restored with git checkout HEAD -- ABSOLUTE_PATH; restoration proven
by state — git diff HEAD empty, porcelain empty, both blobs back to their
HEAD hashes. No build or dist/ is on this path: the pin reads the checkout.

⚠️ The pin's own self-test caught a real bug in the matcher during development —
a hash in one comment block anchored a count in the next block across a line of
code. That is why proseLines distinguishes a blank comment line from code, and
why the self-test asserts that case.

Verification, all on 8537eb4b4

check result
pnpm exec vitest run scripts/__tests__ (whole dir) Test Files 104 passed (104) · Tests 3065 passed (3065)
pnpm run type-check:scripts exit 0; --listFiles confirms both edited script files are in the program
node scripts/check-eager-closure-budget.mjs ✅ Console eager closure is 3187.4 KB gzipped across 50 of 518 chunks (budget: 3191.4 KB, headroom: 4.0 KB).
node scripts/check-control-bytes.mjs ✅ check-control-bytes: OK (scanned 6365 tracked text file(s); skipped 85 binary).
node scripts/check-governed-queue-guard.mjs --self-test OK check-governed-queue-guard self-test: 132 cases pass
node scripts/check-governed-queue-guard.mjs --test .github/workflows/performance-budget.yml ✅ NOT GOVERNED — 1 path(s) checked against 5 governed surface(s); none matched.
node scripts/check-shell-escape-residue.mjs ✅ check-shell-escape-residue: OK
node scripts/check-changeset-presence.mjs ✅ No source or published contract of a released package changed in this range, so no changeset is owed. — acted on: none added
control-byte self-scan over all three edited files no match

Every gate's own verdict line, with the exit code captured by redirecting to a
file first — never read through a pipe.

ESLint, narrowed and the narrowing proven (repo-wide pnpm lint is CI's run):
the two edited script files lint clean — --format json reports 2 files, 0 errors,
0 warnings. The workflow is outside ESLint's population entirely: asked with the
repo's own config it answers "File ignored because no matching configuration was
supplied."
And the config enables no type-aware linting
(js.configs.recommended + tseslint.configs.recommended, no
parserOptions.project or projectService), so this diff cannot move the verdict
on any untouched file.

Scope

Clause-② stayed no — no predicate, no scan population, no verdict, no
constant. Comments in three files, plus four it cases in the gate's existing
test file. The prose was checked against the gate's behaviour, not only its
count, and it was right about behaviour: the aggregate really is one number over
the whole closure and really cannot localise a regression, which is why
PER_CHUNK_GZIP_CEILINGS exists. objectui#7518's aggregate-headroom sentences
were not touched.

Also folded in, exactly as the card and its triage ruled — a clarifying sentence,
deleting neither figure: the header names both 3.07 MB and 3.12 MB in one
paragraph without saying which is which. They are BASELINE's measured payload
and MAX_EAGER_CLOSURE_GZIP_BYTES rendered in MiB; the note now says so.

Out of scope, filed unassigned after a REST dedup over the finding and
domain:devx label listings paged to exhaustion (203 unique open issues) with
#7528 as a known-hit control:

Neither is addressed here; both remain open.


Generated by Claude Code

…nnot keep

Three sentences described the aggregate as "one number over N chunks" with N
frozen as a literal, and the workflow's ceiling clause named a size the gate
retired two re-baselines ago. Neither number was load-bearing for a verdict,
which is why nothing caught either.

The prose now states its population and points at the gate's own verdict line
for the figure, and a pin refuses any chunk count no commit anchors.

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

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3187.4 KB 3191.4 KB
Main entry chunk (gzip) 143.2 KB 350 KB
Entry file index-DRjAR2je.js —
Status PASS —

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


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 510.60KB 116.20KB
core (index.js) 6.96KB 2.79KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 182.08KB 50.62KB
fields (index.js) 242.44KB 61.25KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.98KB 10.98KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 47.87KB 13.31KB
plugin-charts (index.js) 70.92KB 19.75KB
plugin-chatbot (index.js) 196.19KB 46.37KB
plugin-dashboard (index.js) 132.88KB 34.69KB
plugin-designer (index.js) 212.86KB 43.19KB
plugin-detail (index.js) 250.55KB 64.06KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.87KB 32.66KB
plugin-gantt (index.js) 167.26KB 41.00KB
plugin-grid (index.js) 209.29KB 56.78KB
plugin-kanban (index.js) 52.71KB 14.55KB
plugin-list (index.js) 113.28KB 27.59KB
plugin-map (index.js) 20.44KB 6.78KB
plugin-markdown (index.js) 13.93KB 4.81KB
plugin-report (index.js) 43.59KB 11.97KB
plugin-timeline (index.js) 30.84KB 8.85KB
plugin-tree (index.js) 9.20KB 3.19KB
plugin-view (index.js) 85.24KB 20.94KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Two more frozen measurements in the eager-closure gate's prose that #7518's ruling did not cover: "52 chunks" and the workflow's "3.15 MB"

1 participant