Skip to content

fix(plugin-timeline): refuse an unusable gantt date range with a diagnostic that names the offending value - #6772

Merged
os-sales merged 3 commits into
mainfrom
claude/issue-6759-gantt-unusable-date-ranges
Aug 29, 2026
Merged

fix(plugin-timeline): refuse an unusable gantt date range with a diagnostic that names the offending value#6772
os-sales merged 3 commits into
mainfrom
claude/issue-6759-gantt-unusable-date-ranges

Conversation

@os-sales

@os-sales os-sales commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Fixes #6759

A gantt whose date range cannot be used now renders a diagnostic that names the offending value, instead of crashing or drawing a chart that is confidently wrong.

The two defects, and why they land together

objectui#6750 taught the gantt branch about the EMPTY list. Two other input classes were left, and they failed in opposite directions — which is itself the evidence that no policy existed yet, and the reason triage refused to let them be fixed one at a time.

Measured on this card's base b98352a15, with a throwaway probe before any change. Five inputs, five ways to be wrong:

probe input before
CASE-1 startDate: 'not-a-date' RangeError: Invalid time value
CASE-1b valid startDate, endDate: 'also-bad' RangeError: Invalid time value
CASE-1c endDate absent entirely RangeError: Invalid time value
CASE-2 minDate: '2030-01-01', maxDate: '2026-03-15' axis: [] · bars: ["left: 157.9250720461095%; width: -4.322766570605188%;"]
CASE-2b minDate: 'whenever' axis: [] · bars: [null]

CASE-2b is the one worth reading twice. It was not in the card. A pinned value never reaches calculateDateRange — the caller resolves schema.minDate || dateRange.minDate afterwards — so an unparseable pin does not crash. It fails the other way: bars: [null] is a bar element carrying no style attribute at all, because the geometry came out NaN and the CSSOM rejects left: NaN%. That is the silence objectui#6750's dev measured, and it is why every assertion in the new pin file is positive about the diagnostic and counts bar elements rather than reading a style attribute — a pin phrased as "the bad geometry is gone" would have passed on the unfixed code, for the wrong reason.

CASE-1c likewise. new Date(undefined) is also an invalid date, so a row item that merely OMITS endDate crashed the render too. It is not a widening: any guard phrased as "every date must parse" covers it, and excluding it would have meant writing extra code to keep one input class crashing.

The fix

One guard pair at the top of the gantt branch in packages/plugin-timeline/src/renderer.tsx:

  1. findUnusableGanttDate scans every row.items[].startDate / endDate, plus a truthy pinned minDate / maxDate, and reports the first value that does not parse together with the path the author wrote it at.
  2. After the range resolves, an inverted range (minDate after maxDate) is refused on the same policy.

Either one renders the shape objectui#6655 established one file away — a role="alert" element, data-testid="timeline-unusable-date-range" — carrying two new translated strings across all ten locale packs:

Unusable gantt date range — items[0].items[0].startDate is "not-a-date", which is not a valid date. …
Unusable gantt date range — minDate "2030-01-01" is after maxDate "2026-03-15". …

The value is quoted, so an empty or space-padded date is visible rather than vanishing into the sentence, and undefined is spelled as itself so a forgotten key reads differently from an empty one.

What it deliberately does not do

  • generateTimeScaleHeaders is untouched. Its refusal is what produced the zero-column axis CASE-2's bar hung under, and the fix refuses ABOVE it rather than relaxing it. Its objectui#6750 verdict — "needed no change" — still stands; a note was added to its docstring recording that its refusal is now unreachable from the gantt branch but is not dead code (it is exported and pinned directly).
  • objectui#6750's emptyGanttDateRange sentinel is not widened to swallow these. An empty list is an ordinary state; an unparseable value is an author error, and substituting a plausible range for the second is the consumer-side tolerance both cards rejected.
  • No authoring-boundary validation. That would constrain where minDate / maxDate are declared — schema side, cross-repo — and was fenced out. It was not needed: refusing at the read site covers every measured input.
  • A null date is left exactly as it was. new Date(null) is the epoch, not an invalid date, so it parses and is not a fault here. It is a real defect in the same family — filed as finding(plugin-timeline): a null gantt date is silently coerced to the epoch — a 649-column axis and a negative-width bar, with no diagnostic #6770, with the measurement.

Evidence

Every number below is from a run on a64e96ca8, the head of this branch, on a clean worktree.

Ablation — the pins detect the defect, they do not pass for the wrong reason. The renderer was reverted to the base blob while the new pins and the i18n keys stayed in place, so only the guard was removed. The mutation was confirmed on disk by blob hash before the run, and the restore by blob hash after it:

HEAD_BLOB=c4a6b301f65f77492e5a07328d0dcd69b4c24456
BASE_BLOB=683aee563c9d4cddd6d597afd8a0f04ede4b741a
AFTER_MUTATE_ON_DISK=683aee563c9d4cddd6d597afd8a0f04ede4b741a   GUARD_MARKER_COUNT_AFTER_MUTATE=0
  Test Files  1 failed (1)
       Tests  13 failed | 7 passed (20)
  AssertionError: expected [Function] to not throw an error but 'RangeError: Invalid time value' was thrown
AFTER_RESTORE_ON_DISK=c4a6b301f65f77492e5a07328d0dcd69b4c24456  GUARD_MARKER_COUNT_AFTER_RESTORE=3
GIT_DIFF_HEAD_EMPTY=yes

The 13 that go red are the ones asserting the new behaviour. The 7 that stay green are the ones holding behaviour this card must NOT change — the healthy chart's exact float geometry, objectui#6750's empty-list sentinel and its degenerate minDate === maxDate axis, generateTimeScaleHeaders's own refusal, and the gantt-only scoping. Both halves are the point.

Gates (union re-run on a64e96ca8 after the final commit; exit codes captured before any pipe):

pnpm exec vitest run packages/plugin-timeline/ packages/i18n/   VITEST_EXIT=0
   Test Files  73 passed (73)   Tests  1049 passed (1049)
pnpm check:i18n-keys          EXIT=0
pnpm check:i18n-drift         EXIT=0   (2 keys added, 0 en values changed)
pnpm check:control-bytes      EXIT=0   (scanned 5631 tracked text files)
check-changeset-presence.mjs  EXIT=0   (13 source files of 2 released packages, 1 changeset)
pnpm changeset:check          EXIT=0
pnpm --filter @object-ui/plugin-timeline lint / type-check      EXIT=0 / 0
pnpm --filter @object-ui/i18n            lint / type-check      EXIT=0 / 0

check:i18n-keys prints its own verdict: "Every in-scope call-site key resolves against the en pack (2834 keys), every literal inline defaultValue matches the value the pack serves, every call site passes exactly the arguments that value has holes for…".

Lint scope, declared. The repo-wide pnpm lint was not run; the two touched packages were linted with the exact per-package invocation turbo run lint makes (eslint . from the package root), reporting 0 errors in both — 22 files and 84 files respectively, counted from eslint's own --format json output rather than from a guess about which files count. That narrowing excludes no relevant file: this repo's eslint.config.js extends tseslint.configs.recommended and declares no parserOptions.project / projectService, so type-aware linting is off and every file's verdict is computed from that file alone — a diff cannot move the verdict of a file it does not touch. (Warnings went 89 to 87 in plugin-timeline; the two removed were unused bindings in the new pins.)

No downstream type sweep, and why. The i18n edit widens TranslationKeys = typeof en by two nested string properties. Nothing in the repo consumes that type structurally — the only reference outside packages/i18n/src is a bare type re-export in packages/react/src/index.ts — and the other nine packs are plain const zh = {…} rather than annotated, so pack parity is held at runtime by all-locales-key-parity.test.ts (green, in the 1049 above). A widening with no structural consumer cannot change a downstream verdict.

File surface

packages/plugin-timeline/ as fenced, plus packages/i18n/src/locales/*.ts. The ten locale packs are a mechanical consequence of copying objectui#6655's shape: check:i18n-keys requires every key a t() call site asks for to exist in the en pack, and all-locales-key-parity.test.ts requires all ten packs to define it. None of the four sibling surfaces named in the dispatch was touched. objectui#6750's pin file timeline-gantt-empty-items.test.tsx is byte-identical to base (blob acd00f3b5 at base, at HEAD and on disk; 30 assertions both sides) — this change is additive to it, and the whole diff is 731 insertions with 0 deletions.

Following objectui#6655's own precedent, no docs change: that card added the same class of refusal diagnostic without touching content/docs/plugins/plugin-timeline.mdx or the package README.

Bundle Analysis: the framework per-chunk ceiling, re-baselined (3532228ed)

The per-chunk half of Bundle Analysis objected on a64e96ca8, and it was the only red check. PER_CHUNK_GZIP_CEILINGS['framework'] moves 502,000 to 512,000 and PER_CHUNK_BASELINE['framework'] moves 492,399 to 502,405 with it, in one commit, by the documented procedure.

Which side moved, measured rather than assumed. The base of this PR is b98352a15, which is also the tip of main, and Bundle Analysis ran on it as a push. Its own printed lines, against this PR's:

chunk main @ b98352a15 this PR @ a64e96ca8
vendor-objectstack 925.5 KB 925.5 KB
framework 488.4 KB / 490.2 KB ceiling — headroom 1.8 KB 490.6 KB — OVER by 0.4 KB
ui-components 385.4 KB 385.4 KB

So this PR moved framework by 2.2 KB. The 0.4 KB overshoot is what is left after a ceiling that main had already drifted to within 1.8 KB of — 0.02x the 89.0 KB regression this gate exists to catch, the thinnest of the three. That drift is not this card's and is not fixed here; it is evidence for objectui#6631, and the re-baseline is what puts the gauge back in range.

The local build reproduces CI byte-for-byte. Same chunk hashes as the failing run — index-ZdY0ak4p.js, framework-CKFkuRYq.js, ui-components-DTTrfOjp.js — so the exact figure the ceiling is derived from is the same one CI weighed: framework = 502,405 gzipped bytes, read from apps/console/dist/eager-closure.json.

Nothing could be trimmed instead — the added code is not in this chunk at all. plugin-timeline is a LAZY chunk: eager-closure.json does not list it, so findUnusableGanttDate, spellGanttDateValue, both refusal branches and the two TIMELINE_DEFAULT_TRANSLATIONS entries cost the eager budget zero. Every byte of the growth is packages/i18n locale data, which lands in framework. Confirmed by grep on the built chunks: framework-CKFkuRYq.js holds the English and Chinese message text and none of the renderer code (timeline-unusable-date-range appears 0 times); plugin-timeline-BoFUQkKS.js holds the renderer code.

How much the text costs, and how it compares to the neighbour this copied. Measured on the built framework chunk by deleting the strings and re-gzipping:

#6759 unusableRange   (2 messages x 10 locales): 20 strings, raw 5666 B, gzip cost 1999 B (1.95 KB)
#6655 objectBoundGantt (1 message  x 10 locales): 10 strings, raw 2738 B, gzip cost 1002 B (0.98 KB)
per-message: #6759 1000 B  vs  #6655 1002 B

Per message this diagnostic weighs 1,000 bytes against objectui#6655's 1,002 in the same chunk — the shape it copied, at the same price. It is 2x only because there are two distinct failure cases, and the 1,999 B of message text is essentially all of the ~2,283 B the chunk grew by (the rest is the unusableRange / malformedDate / inverted key names, ten times over). The only lever left was to say less, in ten languages, about what the author got wrong.

What the bytes buy: a hard RangeError render crash on a malformed or absent gantt date, and an inverted pinned range that drew a bar at width: -4.3% under a header row with zero cells and said nothing. A crash and a confidently-wrong render, both on ordinary author mistakes.

The margin follows this file's own convention, not an invented one. All three existing entries are the measurement plus ~2%, rounded to the nearest 1,000: 967,000/948,461 = 1.0195, 502,000/492,399 = 1.0195, 399,000/391,095 = 1.0202. 502,405 x 1.02 = 512,453, to the nearest 1,000 is 512,000 = 1.0191. Headroom is 9,595 bytes = 0.105x REGRESSION_THIS_GATE_MUST_CATCH_BYTES — the same multiple the retired framework ceiling carried (9,601 = 0.105x), and far under the one-regression bound evaluateHeadroomSensitivity errors on. No round-up "for future room".

The PER_CHUNK_BASELINE doc comment now records provenance per key rather than naming one commit for three numbers taken on two, which is itself a small piece of what objectui#6631 is about.

Gates, on the final commit 3532228ed:

node scripts/check-eager-closure-budget.mjs                     EXIT=0
  Console eager closure is 3182.5 KB gzipped across 48 of 513 chunks (budget: 3222.7 KB, headroom: 40.2 KB).
  Per-chunk eager budgets (3 chunks weighed):
    vendor-objectstack  925.5 KB / 944.3 KB ceiling (headroom 18.8 KB)
    framework           490.6 KB / 500.0 KB ceiling (headroom  9.4 KB)
    ui-components       385.4 KB / 389.6 KB ceiling (headroom  4.3 KB)
  Ceiling sensitivity (4 ceilings, each weighed against the report just read): all pass
    chunk `framework`   490.6 KB measured / 500.0 KB ceiling (headroom 9.4 KB = 0.11x the 89.0 KB regression)

pnpm exec vitest run --project unit
  scripts/__tests__/check-eager-closure-budget.test.ts
  scripts/__tests__/render-budget-comment.test.ts                EXIT=0   118 passed (118)
   (+ check-side-effects-array, vite-declared-lazy-views, vite-ineffective-dynamic-imports: 183 passed (183))
pnpm run lint:root                                               EXIT=0   0 errors, 28 warnings (all pre-existing)
pnpm run type-check:scripts                                      EXIT=0
pnpm run check:control-bytes                                     EXIT=0   scanned 5631 tracked text file(s)
pnpm run check:shell-escape-residue                              EXIT=0
pnpm run check:entry-guard                                       EXIT=0

Exit codes were captured by redirecting to a file first, never through a pipe, and each verdict above is the line the gate itself printed. The union was re-run after the final commit; git rev-parse --short HEAD on that run is 3532228ed.

Measurement session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49


Generated by Claude Code

claude added 2 commits August 29, 2026 09:42
… value

Two input classes reached the gantt branch with a date range that cannot be
drawn, and they failed in opposite directions — which is why they are fixed
together, under one policy.

A date that does not parse threw `RangeError: Invalid time value`: the crash
site objectui#6750 guarded for the EMPTY list, reached through a different
input. An inverted author-pinned range drew silent nonsense instead — the axis
guard in `generateTimeScaleHeaders` emitted no headers, but the row loop still
ran and `calculateBarDimensions` divided by a negative `totalDuration`.

Both now render the diagnostic shape objectui#6655 established one file away:
a `role="alert"` element naming the offending value and where it was authored.
The guard sits ABOVE `calculateDateRange` and `generateTimeScaleHeaders`, so
neither is relaxed and objectui#6750's empty-list sentinel is not widened to
swallow an author error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
`container` was destructured and never read in the two tests that assert
through `screen.getByTestId`. Lint warnings for `packages/plugin-timeline`
go 89 -> 87; errors stay at 0.

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

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 48 chunks) 3182.5 KB 3222.7 KB
Main entry chunk (gzip) 148.2 KB 350 KB
Entry file index-ZdY0ak4p.js
Status FAIL

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

Which half objected:

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

📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 11.89KB 4.50KB
app-shell (runtime-config.js) 20.61KB 7.35KB
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) 511.68KB 116.30KB
core (index.js) 5.30KB 2.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 173.10KB 47.96KB
fields (index.js) 240.93KB 60.76KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
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.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.95KB 10.97KB
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.55KB 0.62KB
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 (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.53KB 3.38KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.64KB 1.50KB
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) 1.93KB 0.88KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.89KB 12.91KB
plugin-charts (index.js) 64.66KB 18.32KB
plugin-chatbot (index.js) 190.33KB 45.10KB
plugin-dashboard (index.js) 133.44KB 34.48KB
plugin-designer (index.js) 212.87KB 43.19KB
plugin-detail (index.js) 245.43KB 62.45KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.96KB 32.63KB
plugin-gantt (index.js) 165.20KB 40.37KB
plugin-grid (index.js) 201.53KB 54.54KB
plugin-kanban (index.js) 53.11KB 14.62KB
plugin-list (index.js) 113.11KB 27.58KB
plugin-map (index.js) 20.17KB 6.66KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.51KB 11.94KB
plugin-timeline (index.js) 28.90KB 8.33KB
plugin-tree (index.js) 9.00KB 3.08KB
plugin-view (index.js) 85.87KB 21.12KB
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) 73.09KB 24.34KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 2.44KB 1.21KB
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 0.99KB
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 (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.72KB 2.24KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
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 (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) 3.40KB 1.71KB
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

…agnostics

The per-chunk half of `Bundle Analysis` objected: `framework` measured
502,405 gzipped bytes against a 502,000 ceiling. Raise that ceiling to
512,000 and move `PER_CHUNK_BASELINE['framework']` to 502,405 with it, by
the margin this file's other two entries already carry (~2% of the
measurement, rounded to the nearest 1,000 — 967,000/948,461 = 1.0195,
399,000/391,095 = 1.0202, and 512,000/502,405 = 1.0191). Headroom is
9,595 bytes = 0.105x REGRESSION_THIS_GATE_MUST_CATCH_BYTES, which is the
same multiple the retired framework ceiling carried (9,601).

What the bytes buy is recorded in the file: a `RangeError` render crash
on a malformed or absent gantt date, and an inverted pinned range that
drew a negative-width bar under a zero-column axis and said nothing.

There was nothing to trim instead. `plugin-timeline` is a LAZY chunk and
is not in the eager closure at all, so every line of the fix costs this
budget zero; all of the growth is `packages/i18n` locale data, which
lands in `framework`. Measured on the built chunk by deleting the twenty
strings and re-gzipping: the ten locales' message text is 1,999 of the
~2,283 bytes the chunk grew by, and per message it costs 1,000 bytes
against the 1,002 that #6655's single refusal already costs in the same
chunk.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 48 chunks) 3182.7 KB 3222.7 KB
Main entry chunk (gzip) 148.2 KB 350 KB
Entry file index-BGQ-b8dh.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) 11.89KB 4.50KB
app-shell (runtime-config.js) 20.61KB 7.35KB
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) 511.71KB 116.32KB
core (index.js) 5.30KB 2.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 173.10KB 47.96KB
fields (index.js) 240.93KB 60.76KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
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.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.95KB 10.97KB
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.55KB 0.62KB
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 (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.53KB 3.38KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.64KB 1.50KB
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) 1.93KB 0.88KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.92KB 12.93KB
plugin-charts (index.js) 64.68KB 18.35KB
plugin-chatbot (index.js) 190.33KB 45.10KB
plugin-dashboard (index.js) 133.48KB 34.51KB
plugin-designer (index.js) 212.87KB 43.19KB
plugin-detail (index.js) 245.46KB 62.46KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 133.03KB 32.64KB
plugin-gantt (index.js) 165.23KB 40.37KB
plugin-grid (index.js) 201.57KB 54.55KB
plugin-kanban (index.js) 53.14KB 14.64KB
plugin-list (index.js) 113.15KB 27.59KB
plugin-map (index.js) 20.20KB 6.66KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.51KB 11.94KB
plugin-timeline (index.js) 28.94KB 8.33KB
plugin-tree (index.js) 9.00KB 3.08KB
plugin-view (index.js) 85.87KB 21.12KB
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) 73.09KB 24.34KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 2.44KB 1.21KB
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 0.99KB
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 (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.72KB 2.24KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
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 (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) 3.40KB 1.71KB
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

2 participants