fix(core,fields): render a dataset measure over a date field as a date - #7442

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-7178-formatmeasure-date
Sep 3, 2026
Merged

fix(core,fields): render a dataset measure over a date field as a date#7442
os-project-manager merged 2 commits into
mainfrom
claude/issue-7178-formatmeasure-date

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#7178

Implements the director-seat ruling in comment 5509025252 (summon #8, 2026-09-02, maintainer verbatim 7189 A 其他同意) — option A. min / max over a date or datetime field stays a legal measure; nothing in @objectstack/spec is touched, and DatasetMeasureSchema is not narrowed.

The defect

formatMeasure in packages/core/src/utils/dataset-format.ts opened with:

if(v==null)return'—';if(typeofv!=='number')returnString(v);

The second line runs beforeformat is read, so for any non-numeric value the function was String(v) and nothing else. A min over a datetime rendered as a raw 24-character ISO string in the KPI tile's text-2xl font-semibold, wrapping to two lines, and the format the four call sites carefully thread through was dead on this path.

A date-shaped value now routes to the date display path ahead of that short-circuit. Because the fix lands in formatMeasure, all four call sites are served at once — the metric tile (DatasetWidget.tsx:768), chart values (:876), dataset table cells (:1083) and the metadata-admin dataset preview (DatasetPreview.tsx:235). None of them changed. PivotTable.tsx takes a number outright and is untouched.

⭐ The shared date path — what I found, and what reusing it cost

File + symbol: formatDate / formatDateTime / formatRelativeDate, previously in packages/fields/src/index.tsx (@object-ui/fields), now in packages/core/src/utils/date-display.ts.

That is the path a list cell uses: DateCellRenderer (registered for field type date at packages/fields/src/index.tsx) calls formatDate(value, field.format || 'relative', { dueLike, locale, t }), and ObjectGrid's date cells, ObjectGantt's tooltips and plugin-dashboard's recordFields all call the same two functions.

Reusing it required lifting it, and that is the substance of this PR. @object-ui/fields is a React package and depends on @object-ui/core; core is the React-free engine, so it could not import from it. That is exactly the objectui#4576 situation one type over — there, formatDisplayNumber lived in @object-ui/i18n, dataset-format.ts could not reach it, the two implementations drifted, and a German session read 1.234,5 % from a list cell beside 1.234,5% from a dashboard measure. #4576 removed the boundary by moving the pure function down into core and re-exporting from above.

This PR applies that same remedy:

  • DateDisplayOptions, formatRelativeDays, formatRelativeDate, formatDate and formatDateTime move verbatim into packages/core/src/utils/date-display.ts. No logic changed.
  • @object-ui/fields re-exports all four public names, so no consumer's import path or behaviour changes (ObjectGrid, ObjectGantt, plugin-dashboard, @object-ui/react).
  • packages/fields/src/__tests__/date-display.reexport-identity.test.ts pins reference identity — the cell renderer's formatDate and the measure formatter's are the same function object. A second copy would pass every behavioural assertion in the repo and fail only there.

⛔ No date formatter was written in dataset-format.ts. The new code there is a 12-line route: two anchored ISO shape tests, a Date.parse guard, and a call.

The date/datetime split mirrors ObjectGantt.tsx's sniffed-ISO dispatch verbatim, because that site is already the repo's answer to this exact situation — an ISO string with no field type attached, which is what a measure is by construction. That is why the absolute locale form is used rather than DateCellRenderer's || 'relative' default, which is keyed on knowing the value is a datefield in a row.

⭐ The format reachability answer — measured

The shared date path accepts format as a named STYLE, not as a date pattern.

formatDate(value, style, options)'s vocabulary is 'short' and 'relative'; every other string falls to its default locale-medium branch. So, for a date measure:

formatreachable?renders
'short'✅ yesJul 4, '24 — the same word DateCellRenderer honours from field.format
'relative'✅ yes3 days ago
'YYYY-MM-DD'❌ nothe locale default (Jul 4, 2024)
absentn/athe locale default

Threading the measure's format into that parameter is the same mapping the list cell already makes, not a new one — and it is not novel behaviour for a pattern either: plugin-dashboard's recordFields.tsx already routes a date-shaped format (/[YMDHms]/) into the same style slot and gets the same locale default.

⇒ Per the ruling's contingency, the remaining gap is the pattern case, not the whole of format. If the spec seat files the one-line describe() follow-up, "applies to numeric measures; date measures render by locale" would be slightly too strong — 'short' and 'relative'are read. Suggested wording: "numeral patterns apply to numeric measures; a date measure reads format as a display style (short, relative)." That follow-up is the domain:ui seat's to file — it is not in this PR.

The limit is pinned by a test rather than left silent, so teaching the shared path a pattern grammar later has a case that must be updated deliberately.

Tests

pnpm exec vitest run from the repo root (⚠️ a package-directory vitest run is refused by the repo's own guard, objectui#3378).

  • packages/core/src/utils/__tests__/dataset-format.date.test.ts13 passed
  • packages/plugin-dashboard/src/__tests__/DatasetWidget.dateMeasure.test.tsx5 passed (the ruling's pin: metric tile + dataset table)
  • packages/fields/src/__tests__/date-display.reexport-identity.test.ts2 passed

Every expected date string in the rendered-surface tests is derived by calling formatDateTime from @object-ui/fields — the module the cell renderers call, by the path they import it by — so the cases assert "the tile shows what a list cell would show", not "the tile shows the string I typed". A literal would pass just as well against a second date convention.

⭐ A numeric measure is byte-identical

A verbatim git show copy of the pre-fix function was compared against the new one across the full argument matrix (value × format × currency × percentScale × locale):

#7178 MATRIX: compared=33696 forms; distinct values that moved=4
(2026-02-30, 2026-07-04, 2026-07-04T07:00:00.000Z, 2026-07-04 07:00:00);
moved forms=3240; expected movers that did NOT move=0

33,696 forms compared; the only values that moved were the four ISO-shaped, parseable ones. Numbers, numeric strings ('1751612400000', '2026', '1e21'), the nullish em dash, arbitrary prose ('Acme Corp', 'March 5, 2026', '2026/07/04') and non-strings are all byte-identical.

Two findings from that sweep, both now pinned:

  • Date.parse('2026-02-30') is notNaN — V8 rolls it to March 2. It routes to the date path and renders Mar 2, which is what a list cell shows for the same stored string. That is agreement, not a defect; rejecting it here is what would create a second convention.
  • An ISO-shaped but unparseable value ('2026-13-45', '2024-07-04T99:99') keeps falling through to String(v). Routing it to formatDate would replace a raw string an author can still debug with an em dash that says nothing.

Red-then-green ablation

Two mutations, each proven on disk by blob hash + anchored grep counts before the run, each restored with git checkout HEAD -- ABSOLUTE_PATH under trap ... EXIT INT TERM, and each restore proven by state (git diff HEAD empty and blob hash equal to the HEAD blob), never by exit code. Directions were written down before the run and all four matched.

Vitest aliases every workspace package to src (vitest.config.mts), so these legs read source directly — no rebuild leg applies, and none is claimed.

legmutationresult
1restore the pre-fix return String(v) short-circuitcore 8 failed / 5 passed, dashboard 3 failed / 2 passed — every date and format case red; ⭐ every numeric must-not-change case stayed green
2replace the re-export with a behaviour-identical local wrapperidentity 1 failed / 1 passed — identity red, behaviour green

Leg 2 is the one that matters for the ruling's constraint: it is a "second date formatter" that behaves identically, and it is caught only by the identity pin. That is the drift objectui#4576 paid for once.

Also run

type-check and lint clean (exit 0) on @object-ui/core, @object-ui/fields, @object-ui/plugin-dashboard, after building each dependency closure.

Bundle budget — framework chunk

⚠️This PR puts framework 402 bytes over its ceiling. The Bundle Analysis check will be red. Reported, not worked around — and ⛔ no ceiling, budget script or PER_CHUNK_* entry is touched in this PR.

Measured with CI's own recipe (pnpm turbo run build --filter='./packages/*'pnpm --filter @object-ui/console buildnode scripts/check-eager-closure-budget.mjs) on both legs: this branch at 5ad78fde, and the branch point 6411def25 as the baseline. Gzipped bytes, read from apps/console/dist/eager-closure.json:

chunkbaselinethis branchdelta
framework523,823524,402+579
ui-components396,598396,088−510
vendor-objectstack948,329948,3290
aggregate eager closure3,254,6043,254,620+16

Verdicts: baseline exit 0 (green), this branch exit 1

❌ framework 512.1 KB / 511.7 KB ceiling (OVER by 0.4 KB)
✅ aggregate 3178.3 KB / 3191.4 KB ceiling (headroom 13.1 KB)

What the bytes are. They are not new code. This change is a relocation: the date formatter family leaves packages/fields (ui-components, −510 B) and lands in packages/core (framework, +579 B), because framework is /packages/(core|react|types)/ by path. The net cost to the payload a browser actually fetches is +16 bytes across a 3.25 MB eager closure — the two chunk rows nearly cancel, and the aggregate ceiling keeps 13.1 KB of headroom.

So the red is chunk attribution, not payload growth: framework had 177 bytes of headroom before this branch, and moving a shared function into the package that group is defined by consumes it. The relocation is the ruled design — core must own the date path because it is the React-free engine that formatMeasure lives in — so shrinking it back means either a second date formatter in dataset-format.ts (⛔ forbidden by the ruling and by objectui#4576) or leaving the defect unfixed.

⇒ This lands on the same blocked ceiling as #7391 and #7400, pending the maintainer's decision in #7399. Routing that is the PM's call, not this PR's.

Changeset

@object-ui/core: patchand@object-ui/fields: patch. The ruling named a @object-ui/core patch; @object-ui/fields is added because the lift genuinely changes that package's source and its dependency on core for those symbols. Flagging it as a deliberate, minimal deviation rather than a silent one.


🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

`formatMeasure` opened with `if (typeof v !== 'number') return String(v)`,
placed before `format` was ever read. A `min` / `max` measure over a date or
datetime field therefore printed its stored value verbatim -- a 24-character
ISO string in the KPI tile's `text-2xl font-semibold`, wrapping to two lines --
and the `format` that `DatasetMeasureSchema` accepts could never be read on
that path. A date-shaped value now routes to the date display path ahead of
that short-circuit, so all four dataset-bound surfaces are served at once.
No second date formatter was written. `formatDate`, `formatDateTime`,
`formatRelativeDate` and `DateDisplayOptions` MOVED from `@object-ui/fields`'
barrel down into `@object-ui/core` (`utils/date-display.ts`) -- the same remedy
objectui#4576 applied to `formatDisplayNumber`, for the same reason: `core` is
the React-free engine and could not import from a React package, so the
alternative was a parallel date convention in `dataset-format.ts`, which is the
drift #4576 already paid for once in percent. `@object-ui/fields` re-exports all
four names unchanged and a reference-identity test pins that the cell renderer
and the measure formatter call the same function object.
Numeric measures are byte-identical: 33,696 argument forms compared against a
verbatim copy of the pre-fix function, and the only values that moved were the
four ISO-shaped, parseable ones.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.3 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CyXQUsBx.js
StatusFAIL

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 halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.14KB13.02KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.27KB64.20KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Blocked, deliberately: Bundle Analysis red on a governance decision this seat cannot make

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Stating the blocker here on the PR rather than only on the card, so a reviewer landing on this page sees why it is red and why nothing is being pushed.

Failing check:Bundle Analysis (100528606288) on 5ad78fde. Every other check is green or skipped.

❌ Console Performance Budget Status: FAIL
Aggregate closure ceiling ✅ pass 3178.3 KB / 3191.4 KB
Per-chunk ceilings ❌ over its ceiling
Ceiling sensitivity (headroom) ✅ pass
Ceiling freshness ✅ pass
chunkbeforeafterceiling
framework523,823524,402 (+579)524,000 — over by 402
ui-components396,598396,088 (−510)399,000
aggregate eager closure3,254,6043,254,620 (+16)passes

It is this PR's byte cost, and it is still not this PR's to fix

The failure is real and attributable — I am not claiming otherwise. But the bytes are a relocation, not growth: the framework group is defined by path (apps/console/vite.config.ts:742, /packages/(core|react|types)/), so code moving intopackages/core scores against it even though an equal amount left ui-components. This PR's true payload cost is +16 bytes across a 3.25 MB closure, and the aggregate ceiling passes. CI's own package table shows both halves: fields 61.71 → 61.26 KB gz, core 2.32 → 2.42 KB gz.

The relocation is not optional. The ruling on #7178 requires routing through the date path list cells already use and forbids a second date convention; packages/core is the React-free engine and cannot import from packages/fields, so the shared formatDate / formatDateTime had to move down — the same move objectui#4576 made for formatDisplayNumber.

What I am NOT doing, and why

Where it goes

Parked behind #7399 (the open framework ceiling decision), alongside #7391 and #7400 — this is now the third PR blocked there. New evidence posted to #7399 at 5520783051: this PR is the first case where the gate reds on a change whose aggregate cost is +16 bytes, which is a property of how the per-chunk ceiling attributes bytes rather than of the change.

Full review of the implementation — including the one place the ruling's pin is not literally satisfiable for datetime, and why option A was taken anyway — is on the card at 5520776537. The PR stays draft; auto-merge is not enabled and it has not been enqueued.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Unblocked — the ceiling this was parked behind is fixed. main merged in; landing once green.

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Retiring the "Blocked, deliberately" notice above (5520817103) — its blocker no longer holds.

#7399 was ruled (maintainer, 2026-09-03, option A′) and landed as 177afeba1 (PR #7489). The framework chunk was 78.7% @object-ui/i18n locale catalogue, because framework and infrastructure tied at priority 80 and framework was written first. The catalogue now has its own i18n-locales chunk and both ceilings were re-baselined in the same change.

⭐ This PR's own analysis was right, and the fix vindicates it

That notice argued the failure was attribution, not growth:

the bytes are a relocation, not growth … This PR's true payload cost is +16 bytes across a 3.25 MB closure, and the aggregate ceiling passes.

That was correct, and it was correct for a deeper reason than the notice knew: the ceiling it tripped was not budgeting core|react|types at all. framework now measures ~60 KB against a 71,000 ceiling (baseline 61,465), where before it measured 523,823 against 524,000 — 41 bytes of headroom for the whole repository. This PR's +579 B fits with room to spare.

⚠️ For the record, and against my own earlier reasoning: I later argued on several PRs that a red here could be diagnosed from the path rule (/packages/(core|react|types)/). That rule is false in this repo — it names the chunk but not its contents — and I published three wrong causal stories from it before the measurement settled the question. The full retraction is on #7399. This PR's notice reasoned better than I did about its own failure.

What I did

Merged main in so CI rebuilds against the new grouping. ⛔ No ceiling was raised, ⛔ nothing was shrunk to fit, ⛔ no re-run spent — exactly as that notice promised.

⇒ Once every check is green by name, this seat lands it. The implementation review stands on the card (#7178 comment 5520776537), including the one place the ruling's pin is not literally satisfiable for datetime and why option A was taken anyway.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3180.9 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-DrgUn1yZ.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.24KB117.82KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.19KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.53KB27.66KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-project-manager
os-project-manager marked this pull request as ready for review September 3, 2026 11:09
@os-project-manager
os-project-manager added this pull request to the merge queueSep 3, 2026
@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Landing — 32/32 green, ready + auto-merge

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Following the unblock notice above (5524649818).

Every check read by name, not in aggregate

32 total: 27 success, 5 skipped, on head 5daf008b9. The five skips are the jobs that do not run on this event (dependabot, Test (coverage), Test (coverage shard …) and siblings) — ⛔ none is a check that ran and was ignored.

The ones that were the actual question:

checkreading
Bundle AnalysisPASS — 3180.9 KB / 3191.4 KB, 50 chunks (was 48)
Test (shard 1/4)4/4✅ all four success (11:06:56 / 11:09:02 / 11:08:57 / 11:08:33)
Type Check✅ success 11:03:13
Lint✅ success
Governed Surface Queue Guard✅ success
README Export Check✅ success — the check that would object to the lift's new @object-ui/core exports

The landing authority, stated

Clause-② is no on this card, ruled explicitly: 「Execution: domain:ui lane, S. Clause-② no (renderer side, no declared surface moves).」 (5509025252).

⚠️ I re-derived that from the diff rather than resting on the ruling's prediction, because the gate's own rule is 「diff 是事实,卡片语义是预测」 — and this diff does move symbols between packages, which the ruling's "no declared surface moves" did not anticipate. It still reads no: the path limb does not fire (nothing under packages/spec/src/**, no *.zod.ts), and the content limb does not either — no contract accept/reject behaviour changes, DatasetMeasureSchema is untouched, and nothing an author may write is newly accepted or refused. The four lifted names land in @object-ui/core and are re-exported from @object-ui/fields, so no consumer's import path or behaviour moves. README Export Check is green over exactly that surface.

⇒ Seat lands it. The implementation review is the ACCEPT on the card (#7178 comment 5520776537).

The one flag carried into landing, deliberately

That ACCEPT recorded, and did not paper over, that the ruling's pin is not literally satisfiable for datetime: DateTimeCellRenderer inlines toLocaleDateString/toLocaleTimeString and never calls formatDateTime, which has zero call sites — so there is no shared datetime path to route through. Option A was taken (dispatch to formatDateTime, the module's own declared datetime function, sniffing date-vs-datetime the way ObjectGantt already does), and the ACCEPT said plainly: "If the director seat or the maintainer reads the pin's wording as binding on datetime specifically, this is the place to object."

That objection window has been open since 05:06 today and the director seat has been active in this repo since (it ruled #7402 at 08:44). No objection was entered, so the flag lands as recorded rather than silently. The reconciliation itself is filed as #7443 and is not closed by this PR.

Follow-up discharged

The ruling also owed a one-line spec-lane follow-up, "filed by the domain:ui seat when the measurement is in hand". The measurement is in hand, and it is filed: objectstack#14933.

⭐ It does not copy the ruling's suggested sentence. The ruling assumed a date measure cannot read format at all; measured, 'short' and 'relative'are read and only the pattern case is unreachable — so "date measures render by locale" would have replaced one inaccuracy with another. The card says so and proposes wording that matches the measurement. It also records that there is no .describe() on that field to add a sentence to (packages/spec/src/ui/dataset.zod.ts:191-192 carries a JSDoc instead), which is a choice the spec lane should make deliberately rather than have made for it.


Generated by Claude Code

Sign up for freeto 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.

A dataset measure over a date field renders its raw ISO string — formatMeasure short-circuits before format is read

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(core,fields): render a dataset measure over a date field as a date - #7442

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-7178-formatmeasure-date
Sep 3, 2026
Merged

fix(core,fields): render a dataset measure over a date field as a date#7442
os-project-manager merged 2 commits into
mainfrom
claude/issue-7178-formatmeasure-date

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#7178

Implements the director-seat ruling in comment 5509025252 (summon #8, 2026-09-02, maintainer verbatim 7189 A 其他同意) — option A. min / max over a date or datetime field stays a legal measure; nothing in @objectstack/spec is touched, and DatasetMeasureSchema is not narrowed.

The defect

formatMeasure in packages/core/src/utils/dataset-format.ts opened with:

if(v==null)return'—';if(typeofv!=='number')returnString(v);

The second line runs beforeformat is read, so for any non-numeric value the function was String(v) and nothing else. A min over a datetime rendered as a raw 24-character ISO string in the KPI tile's text-2xl font-semibold, wrapping to two lines, and the format the four call sites carefully thread through was dead on this path.

A date-shaped value now routes to the date display path ahead of that short-circuit. Because the fix lands in formatMeasure, all four call sites are served at once — the metric tile (DatasetWidget.tsx:768), chart values (:876), dataset table cells (:1083) and the metadata-admin dataset preview (DatasetPreview.tsx:235). None of them changed. PivotTable.tsx takes a number outright and is untouched.

⭐ The shared date path — what I found, and what reusing it cost

File + symbol: formatDate / formatDateTime / formatRelativeDate, previously in packages/fields/src/index.tsx (@object-ui/fields), now in packages/core/src/utils/date-display.ts.

That is the path a list cell uses: DateCellRenderer (registered for field type date at packages/fields/src/index.tsx) calls formatDate(value, field.format || 'relative', { dueLike, locale, t }), and ObjectGrid's date cells, ObjectGantt's tooltips and plugin-dashboard's recordFields all call the same two functions.

Reusing it required lifting it, and that is the substance of this PR. @object-ui/fields is a React package and depends on @object-ui/core; core is the React-free engine, so it could not import from it. That is exactly the objectui#4576 situation one type over — there, formatDisplayNumber lived in @object-ui/i18n, dataset-format.ts could not reach it, the two implementations drifted, and a German session read 1.234,5 % from a list cell beside 1.234,5% from a dashboard measure. #4576 removed the boundary by moving the pure function down into core and re-exporting from above.

This PR applies that same remedy:

  • DateDisplayOptions, formatRelativeDays, formatRelativeDate, formatDate and formatDateTime move verbatim into packages/core/src/utils/date-display.ts. No logic changed.
  • @object-ui/fields re-exports all four public names, so no consumer's import path or behaviour changes (ObjectGrid, ObjectGantt, plugin-dashboard, @object-ui/react).
  • packages/fields/src/__tests__/date-display.reexport-identity.test.ts pins reference identity — the cell renderer's formatDate and the measure formatter's are the same function object. A second copy would pass every behavioural assertion in the repo and fail only there.

⛔ No date formatter was written in dataset-format.ts. The new code there is a 12-line route: two anchored ISO shape tests, a Date.parse guard, and a call.

The date/datetime split mirrors ObjectGantt.tsx's sniffed-ISO dispatch verbatim, because that site is already the repo's answer to this exact situation — an ISO string with no field type attached, which is what a measure is by construction. That is why the absolute locale form is used rather than DateCellRenderer's || 'relative' default, which is keyed on knowing the value is a datefield in a row.

⭐ The format reachability answer — measured

The shared date path accepts format as a named STYLE, not as a date pattern.

formatDate(value, style, options)'s vocabulary is 'short' and 'relative'; every other string falls to its default locale-medium branch. So, for a date measure:

formatreachable?renders
'short'✅ yesJul 4, '24 — the same word DateCellRenderer honours from field.format
'relative'✅ yes3 days ago
'YYYY-MM-DD'❌ nothe locale default (Jul 4, 2024)
absentn/athe locale default

Threading the measure's format into that parameter is the same mapping the list cell already makes, not a new one — and it is not novel behaviour for a pattern either: plugin-dashboard's recordFields.tsx already routes a date-shaped format (/[YMDHms]/) into the same style slot and gets the same locale default.

⇒ Per the ruling's contingency, the remaining gap is the pattern case, not the whole of format. If the spec seat files the one-line describe() follow-up, "applies to numeric measures; date measures render by locale" would be slightly too strong — 'short' and 'relative'are read. Suggested wording: "numeral patterns apply to numeric measures; a date measure reads format as a display style (short, relative)." That follow-up is the domain:ui seat's to file — it is not in this PR.

The limit is pinned by a test rather than left silent, so teaching the shared path a pattern grammar later has a case that must be updated deliberately.

Tests

pnpm exec vitest run from the repo root (⚠️ a package-directory vitest run is refused by the repo's own guard, objectui#3378).

  • packages/core/src/utils/__tests__/dataset-format.date.test.ts13 passed
  • packages/plugin-dashboard/src/__tests__/DatasetWidget.dateMeasure.test.tsx5 passed (the ruling's pin: metric tile + dataset table)
  • packages/fields/src/__tests__/date-display.reexport-identity.test.ts2 passed

Every expected date string in the rendered-surface tests is derived by calling formatDateTime from @object-ui/fields — the module the cell renderers call, by the path they import it by — so the cases assert "the tile shows what a list cell would show", not "the tile shows the string I typed". A literal would pass just as well against a second date convention.

⭐ A numeric measure is byte-identical

A verbatim git show copy of the pre-fix function was compared against the new one across the full argument matrix (value × format × currency × percentScale × locale):

#7178 MATRIX: compared=33696 forms; distinct values that moved=4
(2026-02-30, 2026-07-04, 2026-07-04T07:00:00.000Z, 2026-07-04 07:00:00);
moved forms=3240; expected movers that did NOT move=0

33,696 forms compared; the only values that moved were the four ISO-shaped, parseable ones. Numbers, numeric strings ('1751612400000', '2026', '1e21'), the nullish em dash, arbitrary prose ('Acme Corp', 'March 5, 2026', '2026/07/04') and non-strings are all byte-identical.

Two findings from that sweep, both now pinned:

  • Date.parse('2026-02-30') is notNaN — V8 rolls it to March 2. It routes to the date path and renders Mar 2, which is what a list cell shows for the same stored string. That is agreement, not a defect; rejecting it here is what would create a second convention.
  • An ISO-shaped but unparseable value ('2026-13-45', '2024-07-04T99:99') keeps falling through to String(v). Routing it to formatDate would replace a raw string an author can still debug with an em dash that says nothing.

Red-then-green ablation

Two mutations, each proven on disk by blob hash + anchored grep counts before the run, each restored with git checkout HEAD -- ABSOLUTE_PATH under trap ... EXIT INT TERM, and each restore proven by state (git diff HEAD empty and blob hash equal to the HEAD blob), never by exit code. Directions were written down before the run and all four matched.

Vitest aliases every workspace package to src (vitest.config.mts), so these legs read source directly — no rebuild leg applies, and none is claimed.

legmutationresult
1restore the pre-fix return String(v) short-circuitcore 8 failed / 5 passed, dashboard 3 failed / 2 passed — every date and format case red; ⭐ every numeric must-not-change case stayed green
2replace the re-export with a behaviour-identical local wrapperidentity 1 failed / 1 passed — identity red, behaviour green

Leg 2 is the one that matters for the ruling's constraint: it is a "second date formatter" that behaves identically, and it is caught only by the identity pin. That is the drift objectui#4576 paid for once.

Also run

type-check and lint clean (exit 0) on @object-ui/core, @object-ui/fields, @object-ui/plugin-dashboard, after building each dependency closure.

Bundle budget — framework chunk

⚠️This PR puts framework 402 bytes over its ceiling. The Bundle Analysis check will be red. Reported, not worked around — and ⛔ no ceiling, budget script or PER_CHUNK_* entry is touched in this PR.

Measured with CI's own recipe (pnpm turbo run build --filter='./packages/*'pnpm --filter @object-ui/console buildnode scripts/check-eager-closure-budget.mjs) on both legs: this branch at 5ad78fde, and the branch point 6411def25 as the baseline. Gzipped bytes, read from apps/console/dist/eager-closure.json:

chunkbaselinethis branchdelta
framework523,823524,402+579
ui-components396,598396,088−510
vendor-objectstack948,329948,3290
aggregate eager closure3,254,6043,254,620+16

Verdicts: baseline exit 0 (green), this branch exit 1

❌ framework 512.1 KB / 511.7 KB ceiling (OVER by 0.4 KB)
✅ aggregate 3178.3 KB / 3191.4 KB ceiling (headroom 13.1 KB)

What the bytes are. They are not new code. This change is a relocation: the date formatter family leaves packages/fields (ui-components, −510 B) and lands in packages/core (framework, +579 B), because framework is /packages/(core|react|types)/ by path. The net cost to the payload a browser actually fetches is +16 bytes across a 3.25 MB eager closure — the two chunk rows nearly cancel, and the aggregate ceiling keeps 13.1 KB of headroom.

So the red is chunk attribution, not payload growth: framework had 177 bytes of headroom before this branch, and moving a shared function into the package that group is defined by consumes it. The relocation is the ruled design — core must own the date path because it is the React-free engine that formatMeasure lives in — so shrinking it back means either a second date formatter in dataset-format.ts (⛔ forbidden by the ruling and by objectui#4576) or leaving the defect unfixed.

⇒ This lands on the same blocked ceiling as #7391 and #7400, pending the maintainer's decision in #7399. Routing that is the PM's call, not this PR's.

Changeset

@object-ui/core: patchand@object-ui/fields: patch. The ruling named a @object-ui/core patch; @object-ui/fields is added because the lift genuinely changes that package's source and its dependency on core for those symbols. Flagging it as a deliberate, minimal deviation rather than a silent one.


🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

`formatMeasure` opened with `if (typeof v !== 'number') return String(v)`,
placed before `format` was ever read. A `min` / `max` measure over a date or
datetime field therefore printed its stored value verbatim -- a 24-character
ISO string in the KPI tile's `text-2xl font-semibold`, wrapping to two lines --
and the `format` that `DatasetMeasureSchema` accepts could never be read on
that path. A date-shaped value now routes to the date display path ahead of
that short-circuit, so all four dataset-bound surfaces are served at once.
No second date formatter was written. `formatDate`, `formatDateTime`,
`formatRelativeDate` and `DateDisplayOptions` MOVED from `@object-ui/fields`'
barrel down into `@object-ui/core` (`utils/date-display.ts`) -- the same remedy
objectui#4576 applied to `formatDisplayNumber`, for the same reason: `core` is
the React-free engine and could not import from a React package, so the
alternative was a parallel date convention in `dataset-format.ts`, which is the
drift #4576 already paid for once in percent. `@object-ui/fields` re-exports all
four names unchanged and a reference-identity test pins that the cell renderer
and the measure formatter call the same function object.
Numeric measures are byte-identical: 33,696 argument forms compared against a
verbatim copy of the pre-fix function, and the only values that moved were the
four ISO-shaped, parseable ones.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.3 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CyXQUsBx.js
StatusFAIL

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 halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.14KB13.02KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.27KB64.20KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Blocked, deliberately: Bundle Analysis red on a governance decision this seat cannot make

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Stating the blocker here on the PR rather than only on the card, so a reviewer landing on this page sees why it is red and why nothing is being pushed.

Failing check:Bundle Analysis (100528606288) on 5ad78fde. Every other check is green or skipped.

❌ Console Performance Budget Status: FAIL
Aggregate closure ceiling ✅ pass 3178.3 KB / 3191.4 KB
Per-chunk ceilings ❌ over its ceiling
Ceiling sensitivity (headroom) ✅ pass
Ceiling freshness ✅ pass
chunkbeforeafterceiling
framework523,823524,402 (+579)524,000 — over by 402
ui-components396,598396,088 (−510)399,000
aggregate eager closure3,254,6043,254,620 (+16)passes

It is this PR's byte cost, and it is still not this PR's to fix

The failure is real and attributable — I am not claiming otherwise. But the bytes are a relocation, not growth: the framework group is defined by path (apps/console/vite.config.ts:742, /packages/(core|react|types)/), so code moving intopackages/core scores against it even though an equal amount left ui-components. This PR's true payload cost is +16 bytes across a 3.25 MB closure, and the aggregate ceiling passes. CI's own package table shows both halves: fields 61.71 → 61.26 KB gz, core 2.32 → 2.42 KB gz.

The relocation is not optional. The ruling on #7178 requires routing through the date path list cells already use and forbids a second date convention; packages/core is the React-free engine and cannot import from packages/fields, so the shared formatDate / formatDateTime had to move down — the same move objectui#4576 made for formatDisplayNumber.

What I am NOT doing, and why

Where it goes

Parked behind #7399 (the open framework ceiling decision), alongside #7391 and #7400 — this is now the third PR blocked there. New evidence posted to #7399 at 5520783051: this PR is the first case where the gate reds on a change whose aggregate cost is +16 bytes, which is a property of how the per-chunk ceiling attributes bytes rather than of the change.

Full review of the implementation — including the one place the ruling's pin is not literally satisfiable for datetime, and why option A was taken anyway — is on the card at 5520776537. The PR stays draft; auto-merge is not enabled and it has not been enqueued.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Unblocked — the ceiling this was parked behind is fixed. main merged in; landing once green.

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Retiring the "Blocked, deliberately" notice above (5520817103) — its blocker no longer holds.

#7399 was ruled (maintainer, 2026-09-03, option A′) and landed as 177afeba1 (PR #7489). The framework chunk was 78.7% @object-ui/i18n locale catalogue, because framework and infrastructure tied at priority 80 and framework was written first. The catalogue now has its own i18n-locales chunk and both ceilings were re-baselined in the same change.

⭐ This PR's own analysis was right, and the fix vindicates it

That notice argued the failure was attribution, not growth:

the bytes are a relocation, not growth … This PR's true payload cost is +16 bytes across a 3.25 MB closure, and the aggregate ceiling passes.

That was correct, and it was correct for a deeper reason than the notice knew: the ceiling it tripped was not budgeting core|react|types at all. framework now measures ~60 KB against a 71,000 ceiling (baseline 61,465), where before it measured 523,823 against 524,000 — 41 bytes of headroom for the whole repository. This PR's +579 B fits with room to spare.

⚠️ For the record, and against my own earlier reasoning: I later argued on several PRs that a red here could be diagnosed from the path rule (/packages/(core|react|types)/). That rule is false in this repo — it names the chunk but not its contents — and I published three wrong causal stories from it before the measurement settled the question. The full retraction is on #7399. This PR's notice reasoned better than I did about its own failure.

What I did

Merged main in so CI rebuilds against the new grouping. ⛔ No ceiling was raised, ⛔ nothing was shrunk to fit, ⛔ no re-run spent — exactly as that notice promised.

⇒ Once every check is green by name, this seat lands it. The implementation review stands on the card (#7178 comment 5520776537), including the one place the ruling's pin is not literally satisfiable for datetime and why option A was taken anyway.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3180.9 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-DrgUn1yZ.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.24KB117.82KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.19KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.53KB27.66KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-project-manager
os-project-manager marked this pull request as ready for review September 3, 2026 11:09
@os-project-manager
os-project-manager added this pull request to the merge queueSep 3, 2026
@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Landing — 32/32 green, ready + auto-merge

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Following the unblock notice above (5524649818).

Every check read by name, not in aggregate

32 total: 27 success, 5 skipped, on head 5daf008b9. The five skips are the jobs that do not run on this event (dependabot, Test (coverage), Test (coverage shard …) and siblings) — ⛔ none is a check that ran and was ignored.

The ones that were the actual question:

checkreading
Bundle AnalysisPASS — 3180.9 KB / 3191.4 KB, 50 chunks (was 48)
Test (shard 1/4)4/4✅ all four success (11:06:56 / 11:09:02 / 11:08:57 / 11:08:33)
Type Check✅ success 11:03:13
Lint✅ success
Governed Surface Queue Guard✅ success
README Export Check✅ success — the check that would object to the lift's new @object-ui/core exports

The landing authority, stated

Clause-② is no on this card, ruled explicitly: 「Execution: domain:ui lane, S. Clause-② no (renderer side, no declared surface moves).」 (5509025252).

⚠️ I re-derived that from the diff rather than resting on the ruling's prediction, because the gate's own rule is 「diff 是事实,卡片语义是预测」 — and this diff does move symbols between packages, which the ruling's "no declared surface moves" did not anticipate. It still reads no: the path limb does not fire (nothing under packages/spec/src/**, no *.zod.ts), and the content limb does not either — no contract accept/reject behaviour changes, DatasetMeasureSchema is untouched, and nothing an author may write is newly accepted or refused. The four lifted names land in @object-ui/core and are re-exported from @object-ui/fields, so no consumer's import path or behaviour moves. README Export Check is green over exactly that surface.

⇒ Seat lands it. The implementation review is the ACCEPT on the card (#7178 comment 5520776537).

The one flag carried into landing, deliberately

That ACCEPT recorded, and did not paper over, that the ruling's pin is not literally satisfiable for datetime: DateTimeCellRenderer inlines toLocaleDateString/toLocaleTimeString and never calls formatDateTime, which has zero call sites — so there is no shared datetime path to route through. Option A was taken (dispatch to formatDateTime, the module's own declared datetime function, sniffing date-vs-datetime the way ObjectGantt already does), and the ACCEPT said plainly: "If the director seat or the maintainer reads the pin's wording as binding on datetime specifically, this is the place to object."

That objection window has been open since 05:06 today and the director seat has been active in this repo since (it ruled #7402 at 08:44). No objection was entered, so the flag lands as recorded rather than silently. The reconciliation itself is filed as #7443 and is not closed by this PR.

Follow-up discharged

The ruling also owed a one-line spec-lane follow-up, "filed by the domain:ui seat when the measurement is in hand". The measurement is in hand, and it is filed: objectstack#14933.

⭐ It does not copy the ruling's suggested sentence. The ruling assumed a date measure cannot read format at all; measured, 'short' and 'relative'are read and only the pattern case is unreachable — so "date measures render by locale" would have replaced one inaccuracy with another. The card says so and proposes wording that matches the measurement. It also records that there is no .describe() on that field to add a sentence to (packages/spec/src/ui/dataset.zod.ts:191-192 carries a JSDoc instead), which is a choice the spec lane should make deliberately rather than have made for it.


Generated by Claude Code

Sign up for freeto 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.

A dataset measure over a date field renders its raw ISO string — formatMeasure short-circuits before format is read

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(core,fields): render a dataset measure over a date field as a date - #7442

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-7178-formatmeasure-date
Sep 3, 2026
Merged

fix(core,fields): render a dataset measure over a date field as a date#7442
os-project-manager merged 2 commits into
mainfrom
claude/issue-7178-formatmeasure-date

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#7178

Implements the director-seat ruling in comment 5509025252 (summon #8, 2026-09-02, maintainer verbatim 7189 A 其他同意) — option A. min / max over a date or datetime field stays a legal measure; nothing in @objectstack/spec is touched, and DatasetMeasureSchema is not narrowed.

The defect

formatMeasure in packages/core/src/utils/dataset-format.ts opened with:

if(v==null)return'—';if(typeofv!=='number')returnString(v);

The second line runs beforeformat is read, so for any non-numeric value the function was String(v) and nothing else. A min over a datetime rendered as a raw 24-character ISO string in the KPI tile's text-2xl font-semibold, wrapping to two lines, and the format the four call sites carefully thread through was dead on this path.

A date-shaped value now routes to the date display path ahead of that short-circuit. Because the fix lands in formatMeasure, all four call sites are served at once — the metric tile (DatasetWidget.tsx:768), chart values (:876), dataset table cells (:1083) and the metadata-admin dataset preview (DatasetPreview.tsx:235). None of them changed. PivotTable.tsx takes a number outright and is untouched.

⭐ The shared date path — what I found, and what reusing it cost

File + symbol: formatDate / formatDateTime / formatRelativeDate, previously in packages/fields/src/index.tsx (@object-ui/fields), now in packages/core/src/utils/date-display.ts.

That is the path a list cell uses: DateCellRenderer (registered for field type date at packages/fields/src/index.tsx) calls formatDate(value, field.format || 'relative', { dueLike, locale, t }), and ObjectGrid's date cells, ObjectGantt's tooltips and plugin-dashboard's recordFields all call the same two functions.

Reusing it required lifting it, and that is the substance of this PR. @object-ui/fields is a React package and depends on @object-ui/core; core is the React-free engine, so it could not import from it. That is exactly the objectui#4576 situation one type over — there, formatDisplayNumber lived in @object-ui/i18n, dataset-format.ts could not reach it, the two implementations drifted, and a German session read 1.234,5 % from a list cell beside 1.234,5% from a dashboard measure. #4576 removed the boundary by moving the pure function down into core and re-exporting from above.

This PR applies that same remedy:

  • DateDisplayOptions, formatRelativeDays, formatRelativeDate, formatDate and formatDateTime move verbatim into packages/core/src/utils/date-display.ts. No logic changed.
  • @object-ui/fields re-exports all four public names, so no consumer's import path or behaviour changes (ObjectGrid, ObjectGantt, plugin-dashboard, @object-ui/react).
  • packages/fields/src/__tests__/date-display.reexport-identity.test.ts pins reference identity — the cell renderer's formatDate and the measure formatter's are the same function object. A second copy would pass every behavioural assertion in the repo and fail only there.

⛔ No date formatter was written in dataset-format.ts. The new code there is a 12-line route: two anchored ISO shape tests, a Date.parse guard, and a call.

The date/datetime split mirrors ObjectGantt.tsx's sniffed-ISO dispatch verbatim, because that site is already the repo's answer to this exact situation — an ISO string with no field type attached, which is what a measure is by construction. That is why the absolute locale form is used rather than DateCellRenderer's || 'relative' default, which is keyed on knowing the value is a datefield in a row.

⭐ The format reachability answer — measured

The shared date path accepts format as a named STYLE, not as a date pattern.

formatDate(value, style, options)'s vocabulary is 'short' and 'relative'; every other string falls to its default locale-medium branch. So, for a date measure:

formatreachable?renders
'short'✅ yesJul 4, '24 — the same word DateCellRenderer honours from field.format
'relative'✅ yes3 days ago
'YYYY-MM-DD'❌ nothe locale default (Jul 4, 2024)
absentn/athe locale default

Threading the measure's format into that parameter is the same mapping the list cell already makes, not a new one — and it is not novel behaviour for a pattern either: plugin-dashboard's recordFields.tsx already routes a date-shaped format (/[YMDHms]/) into the same style slot and gets the same locale default.

⇒ Per the ruling's contingency, the remaining gap is the pattern case, not the whole of format. If the spec seat files the one-line describe() follow-up, "applies to numeric measures; date measures render by locale" would be slightly too strong — 'short' and 'relative'are read. Suggested wording: "numeral patterns apply to numeric measures; a date measure reads format as a display style (short, relative)." That follow-up is the domain:ui seat's to file — it is not in this PR.

The limit is pinned by a test rather than left silent, so teaching the shared path a pattern grammar later has a case that must be updated deliberately.

Tests

pnpm exec vitest run from the repo root (⚠️ a package-directory vitest run is refused by the repo's own guard, objectui#3378).

  • packages/core/src/utils/__tests__/dataset-format.date.test.ts13 passed
  • packages/plugin-dashboard/src/__tests__/DatasetWidget.dateMeasure.test.tsx5 passed (the ruling's pin: metric tile + dataset table)
  • packages/fields/src/__tests__/date-display.reexport-identity.test.ts2 passed

Every expected date string in the rendered-surface tests is derived by calling formatDateTime from @object-ui/fields — the module the cell renderers call, by the path they import it by — so the cases assert "the tile shows what a list cell would show", not "the tile shows the string I typed". A literal would pass just as well against a second date convention.

⭐ A numeric measure is byte-identical

A verbatim git show copy of the pre-fix function was compared against the new one across the full argument matrix (value × format × currency × percentScale × locale):

#7178 MATRIX: compared=33696 forms; distinct values that moved=4
(2026-02-30, 2026-07-04, 2026-07-04T07:00:00.000Z, 2026-07-04 07:00:00);
moved forms=3240; expected movers that did NOT move=0

33,696 forms compared; the only values that moved were the four ISO-shaped, parseable ones. Numbers, numeric strings ('1751612400000', '2026', '1e21'), the nullish em dash, arbitrary prose ('Acme Corp', 'March 5, 2026', '2026/07/04') and non-strings are all byte-identical.

Two findings from that sweep, both now pinned:

  • Date.parse('2026-02-30') is notNaN — V8 rolls it to March 2. It routes to the date path and renders Mar 2, which is what a list cell shows for the same stored string. That is agreement, not a defect; rejecting it here is what would create a second convention.
  • An ISO-shaped but unparseable value ('2026-13-45', '2024-07-04T99:99') keeps falling through to String(v). Routing it to formatDate would replace a raw string an author can still debug with an em dash that says nothing.

Red-then-green ablation

Two mutations, each proven on disk by blob hash + anchored grep counts before the run, each restored with git checkout HEAD -- ABSOLUTE_PATH under trap ... EXIT INT TERM, and each restore proven by state (git diff HEAD empty and blob hash equal to the HEAD blob), never by exit code. Directions were written down before the run and all four matched.

Vitest aliases every workspace package to src (vitest.config.mts), so these legs read source directly — no rebuild leg applies, and none is claimed.

legmutationresult
1restore the pre-fix return String(v) short-circuitcore 8 failed / 5 passed, dashboard 3 failed / 2 passed — every date and format case red; ⭐ every numeric must-not-change case stayed green
2replace the re-export with a behaviour-identical local wrapperidentity 1 failed / 1 passed — identity red, behaviour green

Leg 2 is the one that matters for the ruling's constraint: it is a "second date formatter" that behaves identically, and it is caught only by the identity pin. That is the drift objectui#4576 paid for once.

Also run

type-check and lint clean (exit 0) on @object-ui/core, @object-ui/fields, @object-ui/plugin-dashboard, after building each dependency closure.

Bundle budget — framework chunk

⚠️This PR puts framework 402 bytes over its ceiling. The Bundle Analysis check will be red. Reported, not worked around — and ⛔ no ceiling, budget script or PER_CHUNK_* entry is touched in this PR.

Measured with CI's own recipe (pnpm turbo run build --filter='./packages/*'pnpm --filter @object-ui/console buildnode scripts/check-eager-closure-budget.mjs) on both legs: this branch at 5ad78fde, and the branch point 6411def25 as the baseline. Gzipped bytes, read from apps/console/dist/eager-closure.json:

chunkbaselinethis branchdelta
framework523,823524,402+579
ui-components396,598396,088−510
vendor-objectstack948,329948,3290
aggregate eager closure3,254,6043,254,620+16

Verdicts: baseline exit 0 (green), this branch exit 1

❌ framework 512.1 KB / 511.7 KB ceiling (OVER by 0.4 KB)
✅ aggregate 3178.3 KB / 3191.4 KB ceiling (headroom 13.1 KB)

What the bytes are. They are not new code. This change is a relocation: the date formatter family leaves packages/fields (ui-components, −510 B) and lands in packages/core (framework, +579 B), because framework is /packages/(core|react|types)/ by path. The net cost to the payload a browser actually fetches is +16 bytes across a 3.25 MB eager closure — the two chunk rows nearly cancel, and the aggregate ceiling keeps 13.1 KB of headroom.

So the red is chunk attribution, not payload growth: framework had 177 bytes of headroom before this branch, and moving a shared function into the package that group is defined by consumes it. The relocation is the ruled design — core must own the date path because it is the React-free engine that formatMeasure lives in — so shrinking it back means either a second date formatter in dataset-format.ts (⛔ forbidden by the ruling and by objectui#4576) or leaving the defect unfixed.

⇒ This lands on the same blocked ceiling as #7391 and #7400, pending the maintainer's decision in #7399. Routing that is the PM's call, not this PR's.

Changeset

@object-ui/core: patchand@object-ui/fields: patch. The ruling named a @object-ui/core patch; @object-ui/fields is added because the lift genuinely changes that package's source and its dependency on core for those symbols. Flagging it as a deliberate, minimal deviation rather than a silent one.


🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

`formatMeasure` opened with `if (typeof v !== 'number') return String(v)`,
placed before `format` was ever read. A `min` / `max` measure over a date or
datetime field therefore printed its stored value verbatim -- a 24-character
ISO string in the KPI tile's `text-2xl font-semibold`, wrapping to two lines --
and the `format` that `DatasetMeasureSchema` accepts could never be read on
that path. A date-shaped value now routes to the date display path ahead of
that short-circuit, so all four dataset-bound surfaces are served at once.
No second date formatter was written. `formatDate`, `formatDateTime`,
`formatRelativeDate` and `DateDisplayOptions` MOVED from `@object-ui/fields`'
barrel down into `@object-ui/core` (`utils/date-display.ts`) -- the same remedy
objectui#4576 applied to `formatDisplayNumber`, for the same reason: `core` is
the React-free engine and could not import from a React package, so the
alternative was a parallel date convention in `dataset-format.ts`, which is the
drift #4576 already paid for once in percent. `@object-ui/fields` re-exports all
four names unchanged and a reference-identity test pins that the cell renderer
and the measure formatter call the same function object.
Numeric measures are byte-identical: 33,696 argument forms compared against a
verbatim copy of the pre-fix function, and the only values that moved were the
four ISO-shaped, parseable ones.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.3 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CyXQUsBx.js
StatusFAIL

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 halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.14KB13.02KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.27KB64.20KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Blocked, deliberately: Bundle Analysis red on a governance decision this seat cannot make

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Stating the blocker here on the PR rather than only on the card, so a reviewer landing on this page sees why it is red and why nothing is being pushed.

Failing check:Bundle Analysis (100528606288) on 5ad78fde. Every other check is green or skipped.

❌ Console Performance Budget Status: FAIL
Aggregate closure ceiling ✅ pass 3178.3 KB / 3191.4 KB
Per-chunk ceilings ❌ over its ceiling
Ceiling sensitivity (headroom) ✅ pass
Ceiling freshness ✅ pass
chunkbeforeafterceiling
framework523,823524,402 (+579)524,000 — over by 402
ui-components396,598396,088 (−510)399,000
aggregate eager closure3,254,6043,254,620 (+16)passes

It is this PR's byte cost, and it is still not this PR's to fix

The failure is real and attributable — I am not claiming otherwise. But the bytes are a relocation, not growth: the framework group is defined by path (apps/console/vite.config.ts:742, /packages/(core|react|types)/), so code moving intopackages/core scores against it even though an equal amount left ui-components. This PR's true payload cost is +16 bytes across a 3.25 MB closure, and the aggregate ceiling passes. CI's own package table shows both halves: fields 61.71 → 61.26 KB gz, core 2.32 → 2.42 KB gz.

The relocation is not optional. The ruling on #7178 requires routing through the date path list cells already use and forbids a second date convention; packages/core is the React-free engine and cannot import from packages/fields, so the shared formatDate / formatDateTime had to move down — the same move objectui#4576 made for formatDisplayNumber.

What I am NOT doing, and why

Where it goes

Parked behind #7399 (the open framework ceiling decision), alongside #7391 and #7400 — this is now the third PR blocked there. New evidence posted to #7399 at 5520783051: this PR is the first case where the gate reds on a change whose aggregate cost is +16 bytes, which is a property of how the per-chunk ceiling attributes bytes rather than of the change.

Full review of the implementation — including the one place the ruling's pin is not literally satisfiable for datetime, and why option A was taken anyway — is on the card at 5520776537. The PR stays draft; auto-merge is not enabled and it has not been enqueued.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Unblocked — the ceiling this was parked behind is fixed. main merged in; landing once green.

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Retiring the "Blocked, deliberately" notice above (5520817103) — its blocker no longer holds.

#7399 was ruled (maintainer, 2026-09-03, option A′) and landed as 177afeba1 (PR #7489). The framework chunk was 78.7% @object-ui/i18n locale catalogue, because framework and infrastructure tied at priority 80 and framework was written first. The catalogue now has its own i18n-locales chunk and both ceilings were re-baselined in the same change.

⭐ This PR's own analysis was right, and the fix vindicates it

That notice argued the failure was attribution, not growth:

the bytes are a relocation, not growth … This PR's true payload cost is +16 bytes across a 3.25 MB closure, and the aggregate ceiling passes.

That was correct, and it was correct for a deeper reason than the notice knew: the ceiling it tripped was not budgeting core|react|types at all. framework now measures ~60 KB against a 71,000 ceiling (baseline 61,465), where before it measured 523,823 against 524,000 — 41 bytes of headroom for the whole repository. This PR's +579 B fits with room to spare.

⚠️ For the record, and against my own earlier reasoning: I later argued on several PRs that a red here could be diagnosed from the path rule (/packages/(core|react|types)/). That rule is false in this repo — it names the chunk but not its contents — and I published three wrong causal stories from it before the measurement settled the question. The full retraction is on #7399. This PR's notice reasoned better than I did about its own failure.

What I did

Merged main in so CI rebuilds against the new grouping. ⛔ No ceiling was raised, ⛔ nothing was shrunk to fit, ⛔ no re-run spent — exactly as that notice promised.

⇒ Once every check is green by name, this seat lands it. The implementation review stands on the card (#7178 comment 5520776537), including the one place the ruling's pin is not literally satisfiable for datetime and why option A was taken anyway.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3180.9 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-DrgUn1yZ.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.24KB117.82KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.19KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.53KB27.66KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-project-manager
os-project-manager marked this pull request as ready for review September 3, 2026 11:09
@os-project-manager
os-project-manager added this pull request to the merge queueSep 3, 2026
@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Landing — 32/32 green, ready + auto-merge

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Following the unblock notice above (5524649818).

Every check read by name, not in aggregate

32 total: 27 success, 5 skipped, on head 5daf008b9. The five skips are the jobs that do not run on this event (dependabot, Test (coverage), Test (coverage shard …) and siblings) — ⛔ none is a check that ran and was ignored.

The ones that were the actual question:

checkreading
Bundle AnalysisPASS — 3180.9 KB / 3191.4 KB, 50 chunks (was 48)
Test (shard 1/4)4/4✅ all four success (11:06:56 / 11:09:02 / 11:08:57 / 11:08:33)
Type Check✅ success 11:03:13
Lint✅ success
Governed Surface Queue Guard✅ success
README Export Check✅ success — the check that would object to the lift's new @object-ui/core exports

The landing authority, stated

Clause-② is no on this card, ruled explicitly: 「Execution: domain:ui lane, S. Clause-② no (renderer side, no declared surface moves).」 (5509025252).

⚠️ I re-derived that from the diff rather than resting on the ruling's prediction, because the gate's own rule is 「diff 是事实,卡片语义是预测」 — and this diff does move symbols between packages, which the ruling's "no declared surface moves" did not anticipate. It still reads no: the path limb does not fire (nothing under packages/spec/src/**, no *.zod.ts), and the content limb does not either — no contract accept/reject behaviour changes, DatasetMeasureSchema is untouched, and nothing an author may write is newly accepted or refused. The four lifted names land in @object-ui/core and are re-exported from @object-ui/fields, so no consumer's import path or behaviour moves. README Export Check is green over exactly that surface.

⇒ Seat lands it. The implementation review is the ACCEPT on the card (#7178 comment 5520776537).

The one flag carried into landing, deliberately

That ACCEPT recorded, and did not paper over, that the ruling's pin is not literally satisfiable for datetime: DateTimeCellRenderer inlines toLocaleDateString/toLocaleTimeString and never calls formatDateTime, which has zero call sites — so there is no shared datetime path to route through. Option A was taken (dispatch to formatDateTime, the module's own declared datetime function, sniffing date-vs-datetime the way ObjectGantt already does), and the ACCEPT said plainly: "If the director seat or the maintainer reads the pin's wording as binding on datetime specifically, this is the place to object."

That objection window has been open since 05:06 today and the director seat has been active in this repo since (it ruled #7402 at 08:44). No objection was entered, so the flag lands as recorded rather than silently. The reconciliation itself is filed as #7443 and is not closed by this PR.

Follow-up discharged

The ruling also owed a one-line spec-lane follow-up, "filed by the domain:ui seat when the measurement is in hand". The measurement is in hand, and it is filed: objectstack#14933.

⭐ It does not copy the ruling's suggested sentence. The ruling assumed a date measure cannot read format at all; measured, 'short' and 'relative'are read and only the pattern case is unreachable — so "date measures render by locale" would have replaced one inaccuracy with another. The card says so and proposes wording that matches the measurement. It also records that there is no .describe() on that field to add a sentence to (packages/spec/src/ui/dataset.zod.ts:191-192 carries a JSDoc instead), which is a choice the spec lane should make deliberately rather than have made for it.


Generated by Claude Code

Sign up for freeto 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.

A dataset measure over a date field renders its raw ISO string — formatMeasure short-circuits before format is read

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(core,fields): render a dataset measure over a date field as a date - #7442

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-7178-formatmeasure-date
Sep 3, 2026
Merged

fix(core,fields): render a dataset measure over a date field as a date#7442
os-project-manager merged 2 commits into
mainfrom
claude/issue-7178-formatmeasure-date

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#7178

Implements the director-seat ruling in comment 5509025252 (summon #8, 2026-09-02, maintainer verbatim 7189 A 其他同意) — option A. min / max over a date or datetime field stays a legal measure; nothing in @objectstack/spec is touched, and DatasetMeasureSchema is not narrowed.

The defect

formatMeasure in packages/core/src/utils/dataset-format.ts opened with:

if(v==null)return'—';if(typeofv!=='number')returnString(v);

The second line runs beforeformat is read, so for any non-numeric value the function was String(v) and nothing else. A min over a datetime rendered as a raw 24-character ISO string in the KPI tile's text-2xl font-semibold, wrapping to two lines, and the format the four call sites carefully thread through was dead on this path.

A date-shaped value now routes to the date display path ahead of that short-circuit. Because the fix lands in formatMeasure, all four call sites are served at once — the metric tile (DatasetWidget.tsx:768), chart values (:876), dataset table cells (:1083) and the metadata-admin dataset preview (DatasetPreview.tsx:235). None of them changed. PivotTable.tsx takes a number outright and is untouched.

⭐ The shared date path — what I found, and what reusing it cost

File + symbol: formatDate / formatDateTime / formatRelativeDate, previously in packages/fields/src/index.tsx (@object-ui/fields), now in packages/core/src/utils/date-display.ts.

That is the path a list cell uses: DateCellRenderer (registered for field type date at packages/fields/src/index.tsx) calls formatDate(value, field.format || 'relative', { dueLike, locale, t }), and ObjectGrid's date cells, ObjectGantt's tooltips and plugin-dashboard's recordFields all call the same two functions.

Reusing it required lifting it, and that is the substance of this PR. @object-ui/fields is a React package and depends on @object-ui/core; core is the React-free engine, so it could not import from it. That is exactly the objectui#4576 situation one type over — there, formatDisplayNumber lived in @object-ui/i18n, dataset-format.ts could not reach it, the two implementations drifted, and a German session read 1.234,5 % from a list cell beside 1.234,5% from a dashboard measure. #4576 removed the boundary by moving the pure function down into core and re-exporting from above.

This PR applies that same remedy:

  • DateDisplayOptions, formatRelativeDays, formatRelativeDate, formatDate and formatDateTime move verbatim into packages/core/src/utils/date-display.ts. No logic changed.
  • @object-ui/fields re-exports all four public names, so no consumer's import path or behaviour changes (ObjectGrid, ObjectGantt, plugin-dashboard, @object-ui/react).
  • packages/fields/src/__tests__/date-display.reexport-identity.test.ts pins reference identity — the cell renderer's formatDate and the measure formatter's are the same function object. A second copy would pass every behavioural assertion in the repo and fail only there.

⛔ No date formatter was written in dataset-format.ts. The new code there is a 12-line route: two anchored ISO shape tests, a Date.parse guard, and a call.

The date/datetime split mirrors ObjectGantt.tsx's sniffed-ISO dispatch verbatim, because that site is already the repo's answer to this exact situation — an ISO string with no field type attached, which is what a measure is by construction. That is why the absolute locale form is used rather than DateCellRenderer's || 'relative' default, which is keyed on knowing the value is a datefield in a row.

⭐ The format reachability answer — measured

The shared date path accepts format as a named STYLE, not as a date pattern.

formatDate(value, style, options)'s vocabulary is 'short' and 'relative'; every other string falls to its default locale-medium branch. So, for a date measure:

formatreachable?renders
'short'✅ yesJul 4, '24 — the same word DateCellRenderer honours from field.format
'relative'✅ yes3 days ago
'YYYY-MM-DD'❌ nothe locale default (Jul 4, 2024)
absentn/athe locale default

Threading the measure's format into that parameter is the same mapping the list cell already makes, not a new one — and it is not novel behaviour for a pattern either: plugin-dashboard's recordFields.tsx already routes a date-shaped format (/[YMDHms]/) into the same style slot and gets the same locale default.

⇒ Per the ruling's contingency, the remaining gap is the pattern case, not the whole of format. If the spec seat files the one-line describe() follow-up, "applies to numeric measures; date measures render by locale" would be slightly too strong — 'short' and 'relative'are read. Suggested wording: "numeral patterns apply to numeric measures; a date measure reads format as a display style (short, relative)." That follow-up is the domain:ui seat's to file — it is not in this PR.

The limit is pinned by a test rather than left silent, so teaching the shared path a pattern grammar later has a case that must be updated deliberately.

Tests

pnpm exec vitest run from the repo root (⚠️ a package-directory vitest run is refused by the repo's own guard, objectui#3378).

  • packages/core/src/utils/__tests__/dataset-format.date.test.ts13 passed
  • packages/plugin-dashboard/src/__tests__/DatasetWidget.dateMeasure.test.tsx5 passed (the ruling's pin: metric tile + dataset table)
  • packages/fields/src/__tests__/date-display.reexport-identity.test.ts2 passed

Every expected date string in the rendered-surface tests is derived by calling formatDateTime from @object-ui/fields — the module the cell renderers call, by the path they import it by — so the cases assert "the tile shows what a list cell would show", not "the tile shows the string I typed". A literal would pass just as well against a second date convention.

⭐ A numeric measure is byte-identical

A verbatim git show copy of the pre-fix function was compared against the new one across the full argument matrix (value × format × currency × percentScale × locale):

#7178 MATRIX: compared=33696 forms; distinct values that moved=4
(2026-02-30, 2026-07-04, 2026-07-04T07:00:00.000Z, 2026-07-04 07:00:00);
moved forms=3240; expected movers that did NOT move=0

33,696 forms compared; the only values that moved were the four ISO-shaped, parseable ones. Numbers, numeric strings ('1751612400000', '2026', '1e21'), the nullish em dash, arbitrary prose ('Acme Corp', 'March 5, 2026', '2026/07/04') and non-strings are all byte-identical.

Two findings from that sweep, both now pinned:

  • Date.parse('2026-02-30') is notNaN — V8 rolls it to March 2. It routes to the date path and renders Mar 2, which is what a list cell shows for the same stored string. That is agreement, not a defect; rejecting it here is what would create a second convention.
  • An ISO-shaped but unparseable value ('2026-13-45', '2024-07-04T99:99') keeps falling through to String(v). Routing it to formatDate would replace a raw string an author can still debug with an em dash that says nothing.

Red-then-green ablation

Two mutations, each proven on disk by blob hash + anchored grep counts before the run, each restored with git checkout HEAD -- ABSOLUTE_PATH under trap ... EXIT INT TERM, and each restore proven by state (git diff HEAD empty and blob hash equal to the HEAD blob), never by exit code. Directions were written down before the run and all four matched.

Vitest aliases every workspace package to src (vitest.config.mts), so these legs read source directly — no rebuild leg applies, and none is claimed.

legmutationresult
1restore the pre-fix return String(v) short-circuitcore 8 failed / 5 passed, dashboard 3 failed / 2 passed — every date and format case red; ⭐ every numeric must-not-change case stayed green
2replace the re-export with a behaviour-identical local wrapperidentity 1 failed / 1 passed — identity red, behaviour green

Leg 2 is the one that matters for the ruling's constraint: it is a "second date formatter" that behaves identically, and it is caught only by the identity pin. That is the drift objectui#4576 paid for once.

Also run

type-check and lint clean (exit 0) on @object-ui/core, @object-ui/fields, @object-ui/plugin-dashboard, after building each dependency closure.

Bundle budget — framework chunk

⚠️This PR puts framework 402 bytes over its ceiling. The Bundle Analysis check will be red. Reported, not worked around — and ⛔ no ceiling, budget script or PER_CHUNK_* entry is touched in this PR.

Measured with CI's own recipe (pnpm turbo run build --filter='./packages/*'pnpm --filter @object-ui/console buildnode scripts/check-eager-closure-budget.mjs) on both legs: this branch at 5ad78fde, and the branch point 6411def25 as the baseline. Gzipped bytes, read from apps/console/dist/eager-closure.json:

chunkbaselinethis branchdelta
framework523,823524,402+579
ui-components396,598396,088−510
vendor-objectstack948,329948,3290
aggregate eager closure3,254,6043,254,620+16

Verdicts: baseline exit 0 (green), this branch exit 1

❌ framework 512.1 KB / 511.7 KB ceiling (OVER by 0.4 KB)
✅ aggregate 3178.3 KB / 3191.4 KB ceiling (headroom 13.1 KB)

What the bytes are. They are not new code. This change is a relocation: the date formatter family leaves packages/fields (ui-components, −510 B) and lands in packages/core (framework, +579 B), because framework is /packages/(core|react|types)/ by path. The net cost to the payload a browser actually fetches is +16 bytes across a 3.25 MB eager closure — the two chunk rows nearly cancel, and the aggregate ceiling keeps 13.1 KB of headroom.

So the red is chunk attribution, not payload growth: framework had 177 bytes of headroom before this branch, and moving a shared function into the package that group is defined by consumes it. The relocation is the ruled design — core must own the date path because it is the React-free engine that formatMeasure lives in — so shrinking it back means either a second date formatter in dataset-format.ts (⛔ forbidden by the ruling and by objectui#4576) or leaving the defect unfixed.

⇒ This lands on the same blocked ceiling as #7391 and #7400, pending the maintainer's decision in #7399. Routing that is the PM's call, not this PR's.

Changeset

@object-ui/core: patchand@object-ui/fields: patch. The ruling named a @object-ui/core patch; @object-ui/fields is added because the lift genuinely changes that package's source and its dependency on core for those symbols. Flagging it as a deliberate, minimal deviation rather than a silent one.


🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

`formatMeasure` opened with `if (typeof v !== 'number') return String(v)`,
placed before `format` was ever read. A `min` / `max` measure over a date or
datetime field therefore printed its stored value verbatim -- a 24-character
ISO string in the KPI tile's `text-2xl font-semibold`, wrapping to two lines --
and the `format` that `DatasetMeasureSchema` accepts could never be read on
that path. A date-shaped value now routes to the date display path ahead of
that short-circuit, so all four dataset-bound surfaces are served at once.
No second date formatter was written. `formatDate`, `formatDateTime`,
`formatRelativeDate` and `DateDisplayOptions` MOVED from `@object-ui/fields`'
barrel down into `@object-ui/core` (`utils/date-display.ts`) -- the same remedy
objectui#4576 applied to `formatDisplayNumber`, for the same reason: `core` is
the React-free engine and could not import from a React package, so the
alternative was a parallel date convention in `dataset-format.ts`, which is the
drift #4576 already paid for once in percent. `@object-ui/fields` re-exports all
four names unchanged and a reference-identity test pins that the cell renderer
and the measure formatter call the same function object.
Numeric measures are byte-identical: 33,696 argument forms compared against a
verbatim copy of the pre-fix function, and the only values that moved were the
four ISO-shaped, parseable ones.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.3 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CyXQUsBx.js
StatusFAIL

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 halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.14KB13.02KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.27KB64.20KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Blocked, deliberately: Bundle Analysis red on a governance decision this seat cannot make

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Stating the blocker here on the PR rather than only on the card, so a reviewer landing on this page sees why it is red and why nothing is being pushed.

Failing check:Bundle Analysis (100528606288) on 5ad78fde. Every other check is green or skipped.

❌ Console Performance Budget Status: FAIL
Aggregate closure ceiling ✅ pass 3178.3 KB / 3191.4 KB
Per-chunk ceilings ❌ over its ceiling
Ceiling sensitivity (headroom) ✅ pass
Ceiling freshness ✅ pass
chunkbeforeafterceiling
framework523,823524,402 (+579)524,000 — over by 402
ui-components396,598396,088 (−510)399,000
aggregate eager closure3,254,6043,254,620 (+16)passes

It is this PR's byte cost, and it is still not this PR's to fix

The failure is real and attributable — I am not claiming otherwise. But the bytes are a relocation, not growth: the framework group is defined by path (apps/console/vite.config.ts:742, /packages/(core|react|types)/), so code moving intopackages/core scores against it even though an equal amount left ui-components. This PR's true payload cost is +16 bytes across a 3.25 MB closure, and the aggregate ceiling passes. CI's own package table shows both halves: fields 61.71 → 61.26 KB gz, core 2.32 → 2.42 KB gz.

The relocation is not optional. The ruling on #7178 requires routing through the date path list cells already use and forbids a second date convention; packages/core is the React-free engine and cannot import from packages/fields, so the shared formatDate / formatDateTime had to move down — the same move objectui#4576 made for formatDisplayNumber.

What I am NOT doing, and why

Where it goes

Parked behind #7399 (the open framework ceiling decision), alongside #7391 and #7400 — this is now the third PR blocked there. New evidence posted to #7399 at 5520783051: this PR is the first case where the gate reds on a change whose aggregate cost is +16 bytes, which is a property of how the per-chunk ceiling attributes bytes rather than of the change.

Full review of the implementation — including the one place the ruling's pin is not literally satisfiable for datetime, and why option A was taken anyway — is on the card at 5520776537. The PR stays draft; auto-merge is not enabled and it has not been enqueued.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Unblocked — the ceiling this was parked behind is fixed. main merged in; landing once green.

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Retiring the "Blocked, deliberately" notice above (5520817103) — its blocker no longer holds.

#7399 was ruled (maintainer, 2026-09-03, option A′) and landed as 177afeba1 (PR #7489). The framework chunk was 78.7% @object-ui/i18n locale catalogue, because framework and infrastructure tied at priority 80 and framework was written first. The catalogue now has its own i18n-locales chunk and both ceilings were re-baselined in the same change.

⭐ This PR's own analysis was right, and the fix vindicates it

That notice argued the failure was attribution, not growth:

the bytes are a relocation, not growth … This PR's true payload cost is +16 bytes across a 3.25 MB closure, and the aggregate ceiling passes.

That was correct, and it was correct for a deeper reason than the notice knew: the ceiling it tripped was not budgeting core|react|types at all. framework now measures ~60 KB against a 71,000 ceiling (baseline 61,465), where before it measured 523,823 against 524,000 — 41 bytes of headroom for the whole repository. This PR's +579 B fits with room to spare.

⚠️ For the record, and against my own earlier reasoning: I later argued on several PRs that a red here could be diagnosed from the path rule (/packages/(core|react|types)/). That rule is false in this repo — it names the chunk but not its contents — and I published three wrong causal stories from it before the measurement settled the question. The full retraction is on #7399. This PR's notice reasoned better than I did about its own failure.

What I did

Merged main in so CI rebuilds against the new grouping. ⛔ No ceiling was raised, ⛔ nothing was shrunk to fit, ⛔ no re-run spent — exactly as that notice promised.

⇒ Once every check is green by name, this seat lands it. The implementation review stands on the card (#7178 comment 5520776537), including the one place the ruling's pin is not literally satisfiable for datetime and why option A was taken anyway.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3180.9 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-DrgUn1yZ.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.24KB117.82KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.19KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.53KB27.66KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-project-manager
os-project-manager marked this pull request as ready for review September 3, 2026 11:09
@os-project-manager
os-project-manager added this pull request to the merge queueSep 3, 2026
@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Landing — 32/32 green, ready + auto-merge

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Following the unblock notice above (5524649818).

Every check read by name, not in aggregate

32 total: 27 success, 5 skipped, on head 5daf008b9. The five skips are the jobs that do not run on this event (dependabot, Test (coverage), Test (coverage shard …) and siblings) — ⛔ none is a check that ran and was ignored.

The ones that were the actual question:

checkreading
Bundle AnalysisPASS — 3180.9 KB / 3191.4 KB, 50 chunks (was 48)
Test (shard 1/4)4/4✅ all four success (11:06:56 / 11:09:02 / 11:08:57 / 11:08:33)
Type Check✅ success 11:03:13
Lint✅ success
Governed Surface Queue Guard✅ success
README Export Check✅ success — the check that would object to the lift's new @object-ui/core exports

The landing authority, stated

Clause-② is no on this card, ruled explicitly: 「Execution: domain:ui lane, S. Clause-② no (renderer side, no declared surface moves).」 (5509025252).

⚠️ I re-derived that from the diff rather than resting on the ruling's prediction, because the gate's own rule is 「diff 是事实,卡片语义是预测」 — and this diff does move symbols between packages, which the ruling's "no declared surface moves" did not anticipate. It still reads no: the path limb does not fire (nothing under packages/spec/src/**, no *.zod.ts), and the content limb does not either — no contract accept/reject behaviour changes, DatasetMeasureSchema is untouched, and nothing an author may write is newly accepted or refused. The four lifted names land in @object-ui/core and are re-exported from @object-ui/fields, so no consumer's import path or behaviour moves. README Export Check is green over exactly that surface.

⇒ Seat lands it. The implementation review is the ACCEPT on the card (#7178 comment 5520776537).

The one flag carried into landing, deliberately

That ACCEPT recorded, and did not paper over, that the ruling's pin is not literally satisfiable for datetime: DateTimeCellRenderer inlines toLocaleDateString/toLocaleTimeString and never calls formatDateTime, which has zero call sites — so there is no shared datetime path to route through. Option A was taken (dispatch to formatDateTime, the module's own declared datetime function, sniffing date-vs-datetime the way ObjectGantt already does), and the ACCEPT said plainly: "If the director seat or the maintainer reads the pin's wording as binding on datetime specifically, this is the place to object."

That objection window has been open since 05:06 today and the director seat has been active in this repo since (it ruled #7402 at 08:44). No objection was entered, so the flag lands as recorded rather than silently. The reconciliation itself is filed as #7443 and is not closed by this PR.

Follow-up discharged

The ruling also owed a one-line spec-lane follow-up, "filed by the domain:ui seat when the measurement is in hand". The measurement is in hand, and it is filed: objectstack#14933.

⭐ It does not copy the ruling's suggested sentence. The ruling assumed a date measure cannot read format at all; measured, 'short' and 'relative'are read and only the pattern case is unreachable — so "date measures render by locale" would have replaced one inaccuracy with another. The card says so and proposes wording that matches the measurement. It also records that there is no .describe() on that field to add a sentence to (packages/spec/src/ui/dataset.zod.ts:191-192 carries a JSDoc instead), which is a choice the spec lane should make deliberately rather than have made for it.


Generated by Claude Code

Sign up for freeto 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.

A dataset measure over a date field renders its raw ISO string — formatMeasure short-circuits before format is read

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(core,fields): render a dataset measure over a date field as a date - #7442

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-7178-formatmeasure-date
Sep 3, 2026
Merged

fix(core,fields): render a dataset measure over a date field as a date#7442
os-project-manager merged 2 commits into
mainfrom
claude/issue-7178-formatmeasure-date

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#7178

Implements the director-seat ruling in comment 5509025252 (summon #8, 2026-09-02, maintainer verbatim 7189 A 其他同意) — option A. min / max over a date or datetime field stays a legal measure; nothing in @objectstack/spec is touched, and DatasetMeasureSchema is not narrowed.

The defect

formatMeasure in packages/core/src/utils/dataset-format.ts opened with:

if(v==null)return'—';if(typeofv!=='number')returnString(v);

The second line runs beforeformat is read, so for any non-numeric value the function was String(v) and nothing else. A min over a datetime rendered as a raw 24-character ISO string in the KPI tile's text-2xl font-semibold, wrapping to two lines, and the format the four call sites carefully thread through was dead on this path.

A date-shaped value now routes to the date display path ahead of that short-circuit. Because the fix lands in formatMeasure, all four call sites are served at once — the metric tile (DatasetWidget.tsx:768), chart values (:876), dataset table cells (:1083) and the metadata-admin dataset preview (DatasetPreview.tsx:235). None of them changed. PivotTable.tsx takes a number outright and is untouched.

⭐ The shared date path — what I found, and what reusing it cost

File + symbol: formatDate / formatDateTime / formatRelativeDate, previously in packages/fields/src/index.tsx (@object-ui/fields), now in packages/core/src/utils/date-display.ts.

That is the path a list cell uses: DateCellRenderer (registered for field type date at packages/fields/src/index.tsx) calls formatDate(value, field.format || 'relative', { dueLike, locale, t }), and ObjectGrid's date cells, ObjectGantt's tooltips and plugin-dashboard's recordFields all call the same two functions.

Reusing it required lifting it, and that is the substance of this PR. @object-ui/fields is a React package and depends on @object-ui/core; core is the React-free engine, so it could not import from it. That is exactly the objectui#4576 situation one type over — there, formatDisplayNumber lived in @object-ui/i18n, dataset-format.ts could not reach it, the two implementations drifted, and a German session read 1.234,5 % from a list cell beside 1.234,5% from a dashboard measure. #4576 removed the boundary by moving the pure function down into core and re-exporting from above.

This PR applies that same remedy:

  • DateDisplayOptions, formatRelativeDays, formatRelativeDate, formatDate and formatDateTime move verbatim into packages/core/src/utils/date-display.ts. No logic changed.
  • @object-ui/fields re-exports all four public names, so no consumer's import path or behaviour changes (ObjectGrid, ObjectGantt, plugin-dashboard, @object-ui/react).
  • packages/fields/src/__tests__/date-display.reexport-identity.test.ts pins reference identity — the cell renderer's formatDate and the measure formatter's are the same function object. A second copy would pass every behavioural assertion in the repo and fail only there.

⛔ No date formatter was written in dataset-format.ts. The new code there is a 12-line route: two anchored ISO shape tests, a Date.parse guard, and a call.

The date/datetime split mirrors ObjectGantt.tsx's sniffed-ISO dispatch verbatim, because that site is already the repo's answer to this exact situation — an ISO string with no field type attached, which is what a measure is by construction. That is why the absolute locale form is used rather than DateCellRenderer's || 'relative' default, which is keyed on knowing the value is a datefield in a row.

⭐ The format reachability answer — measured

The shared date path accepts format as a named STYLE, not as a date pattern.

formatDate(value, style, options)'s vocabulary is 'short' and 'relative'; every other string falls to its default locale-medium branch. So, for a date measure:

formatreachable?renders
'short'✅ yesJul 4, '24 — the same word DateCellRenderer honours from field.format
'relative'✅ yes3 days ago
'YYYY-MM-DD'❌ nothe locale default (Jul 4, 2024)
absentn/athe locale default

Threading the measure's format into that parameter is the same mapping the list cell already makes, not a new one — and it is not novel behaviour for a pattern either: plugin-dashboard's recordFields.tsx already routes a date-shaped format (/[YMDHms]/) into the same style slot and gets the same locale default.

⇒ Per the ruling's contingency, the remaining gap is the pattern case, not the whole of format. If the spec seat files the one-line describe() follow-up, "applies to numeric measures; date measures render by locale" would be slightly too strong — 'short' and 'relative'are read. Suggested wording: "numeral patterns apply to numeric measures; a date measure reads format as a display style (short, relative)." That follow-up is the domain:ui seat's to file — it is not in this PR.

The limit is pinned by a test rather than left silent, so teaching the shared path a pattern grammar later has a case that must be updated deliberately.

Tests

pnpm exec vitest run from the repo root (⚠️ a package-directory vitest run is refused by the repo's own guard, objectui#3378).

  • packages/core/src/utils/__tests__/dataset-format.date.test.ts13 passed
  • packages/plugin-dashboard/src/__tests__/DatasetWidget.dateMeasure.test.tsx5 passed (the ruling's pin: metric tile + dataset table)
  • packages/fields/src/__tests__/date-display.reexport-identity.test.ts2 passed

Every expected date string in the rendered-surface tests is derived by calling formatDateTime from @object-ui/fields — the module the cell renderers call, by the path they import it by — so the cases assert "the tile shows what a list cell would show", not "the tile shows the string I typed". A literal would pass just as well against a second date convention.

⭐ A numeric measure is byte-identical

A verbatim git show copy of the pre-fix function was compared against the new one across the full argument matrix (value × format × currency × percentScale × locale):

#7178 MATRIX: compared=33696 forms; distinct values that moved=4
(2026-02-30, 2026-07-04, 2026-07-04T07:00:00.000Z, 2026-07-04 07:00:00);
moved forms=3240; expected movers that did NOT move=0

33,696 forms compared; the only values that moved were the four ISO-shaped, parseable ones. Numbers, numeric strings ('1751612400000', '2026', '1e21'), the nullish em dash, arbitrary prose ('Acme Corp', 'March 5, 2026', '2026/07/04') and non-strings are all byte-identical.

Two findings from that sweep, both now pinned:

  • Date.parse('2026-02-30') is notNaN — V8 rolls it to March 2. It routes to the date path and renders Mar 2, which is what a list cell shows for the same stored string. That is agreement, not a defect; rejecting it here is what would create a second convention.
  • An ISO-shaped but unparseable value ('2026-13-45', '2024-07-04T99:99') keeps falling through to String(v). Routing it to formatDate would replace a raw string an author can still debug with an em dash that says nothing.

Red-then-green ablation

Two mutations, each proven on disk by blob hash + anchored grep counts before the run, each restored with git checkout HEAD -- ABSOLUTE_PATH under trap ... EXIT INT TERM, and each restore proven by state (git diff HEAD empty and blob hash equal to the HEAD blob), never by exit code. Directions were written down before the run and all four matched.

Vitest aliases every workspace package to src (vitest.config.mts), so these legs read source directly — no rebuild leg applies, and none is claimed.

legmutationresult
1restore the pre-fix return String(v) short-circuitcore 8 failed / 5 passed, dashboard 3 failed / 2 passed — every date and format case red; ⭐ every numeric must-not-change case stayed green
2replace the re-export with a behaviour-identical local wrapperidentity 1 failed / 1 passed — identity red, behaviour green

Leg 2 is the one that matters for the ruling's constraint: it is a "second date formatter" that behaves identically, and it is caught only by the identity pin. That is the drift objectui#4576 paid for once.

Also run

type-check and lint clean (exit 0) on @object-ui/core, @object-ui/fields, @object-ui/plugin-dashboard, after building each dependency closure.

Bundle budget — framework chunk

⚠️This PR puts framework 402 bytes over its ceiling. The Bundle Analysis check will be red. Reported, not worked around — and ⛔ no ceiling, budget script or PER_CHUNK_* entry is touched in this PR.

Measured with CI's own recipe (pnpm turbo run build --filter='./packages/*'pnpm --filter @object-ui/console buildnode scripts/check-eager-closure-budget.mjs) on both legs: this branch at 5ad78fde, and the branch point 6411def25 as the baseline. Gzipped bytes, read from apps/console/dist/eager-closure.json:

chunkbaselinethis branchdelta
framework523,823524,402+579
ui-components396,598396,088−510
vendor-objectstack948,329948,3290
aggregate eager closure3,254,6043,254,620+16

Verdicts: baseline exit 0 (green), this branch exit 1

❌ framework 512.1 KB / 511.7 KB ceiling (OVER by 0.4 KB)
✅ aggregate 3178.3 KB / 3191.4 KB ceiling (headroom 13.1 KB)

What the bytes are. They are not new code. This change is a relocation: the date formatter family leaves packages/fields (ui-components, −510 B) and lands in packages/core (framework, +579 B), because framework is /packages/(core|react|types)/ by path. The net cost to the payload a browser actually fetches is +16 bytes across a 3.25 MB eager closure — the two chunk rows nearly cancel, and the aggregate ceiling keeps 13.1 KB of headroom.

So the red is chunk attribution, not payload growth: framework had 177 bytes of headroom before this branch, and moving a shared function into the package that group is defined by consumes it. The relocation is the ruled design — core must own the date path because it is the React-free engine that formatMeasure lives in — so shrinking it back means either a second date formatter in dataset-format.ts (⛔ forbidden by the ruling and by objectui#4576) or leaving the defect unfixed.

⇒ This lands on the same blocked ceiling as #7391 and #7400, pending the maintainer's decision in #7399. Routing that is the PM's call, not this PR's.

Changeset

@object-ui/core: patchand@object-ui/fields: patch. The ruling named a @object-ui/core patch; @object-ui/fields is added because the lift genuinely changes that package's source and its dependency on core for those symbols. Flagging it as a deliberate, minimal deviation rather than a silent one.


🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

`formatMeasure` opened with `if (typeof v !== 'number') return String(v)`,
placed before `format` was ever read. A `min` / `max` measure over a date or
datetime field therefore printed its stored value verbatim -- a 24-character
ISO string in the KPI tile's `text-2xl font-semibold`, wrapping to two lines --
and the `format` that `DatasetMeasureSchema` accepts could never be read on
that path. A date-shaped value now routes to the date display path ahead of
that short-circuit, so all four dataset-bound surfaces are served at once.
No second date formatter was written. `formatDate`, `formatDateTime`,
`formatRelativeDate` and `DateDisplayOptions` MOVED from `@object-ui/fields`'
barrel down into `@object-ui/core` (`utils/date-display.ts`) -- the same remedy
objectui#4576 applied to `formatDisplayNumber`, for the same reason: `core` is
the React-free engine and could not import from a React package, so the
alternative was a parallel date convention in `dataset-format.ts`, which is the
drift #4576 already paid for once in percent. `@object-ui/fields` re-exports all
four names unchanged and a reference-identity test pins that the cell renderer
and the measure formatter call the same function object.
Numeric measures are byte-identical: 33,696 argument forms compared against a
verbatim copy of the pre-fix function, and the only values that moved were the
four ISO-shaped, parseable ones.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.3 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CyXQUsBx.js
StatusFAIL

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 halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.14KB13.02KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.27KB64.20KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Blocked, deliberately: Bundle Analysis red on a governance decision this seat cannot make

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Stating the blocker here on the PR rather than only on the card, so a reviewer landing on this page sees why it is red and why nothing is being pushed.

Failing check:Bundle Analysis (100528606288) on 5ad78fde. Every other check is green or skipped.

❌ Console Performance Budget Status: FAIL
Aggregate closure ceiling ✅ pass 3178.3 KB / 3191.4 KB
Per-chunk ceilings ❌ over its ceiling
Ceiling sensitivity (headroom) ✅ pass
Ceiling freshness ✅ pass
chunkbeforeafterceiling
framework523,823524,402 (+579)524,000 — over by 402
ui-components396,598396,088 (−510)399,000
aggregate eager closure3,254,6043,254,620 (+16)passes

It is this PR's byte cost, and it is still not this PR's to fix

The failure is real and attributable — I am not claiming otherwise. But the bytes are a relocation, not growth: the framework group is defined by path (apps/console/vite.config.ts:742, /packages/(core|react|types)/), so code moving intopackages/core scores against it even though an equal amount left ui-components. This PR's true payload cost is +16 bytes across a 3.25 MB closure, and the aggregate ceiling passes. CI's own package table shows both halves: fields 61.71 → 61.26 KB gz, core 2.32 → 2.42 KB gz.

The relocation is not optional. The ruling on #7178 requires routing through the date path list cells already use and forbids a second date convention; packages/core is the React-free engine and cannot import from packages/fields, so the shared formatDate / formatDateTime had to move down — the same move objectui#4576 made for formatDisplayNumber.

What I am NOT doing, and why

Where it goes

Parked behind #7399 (the open framework ceiling decision), alongside #7391 and #7400 — this is now the third PR blocked there. New evidence posted to #7399 at 5520783051: this PR is the first case where the gate reds on a change whose aggregate cost is +16 bytes, which is a property of how the per-chunk ceiling attributes bytes rather than of the change.

Full review of the implementation — including the one place the ruling's pin is not literally satisfiable for datetime, and why option A was taken anyway — is on the card at 5520776537. The PR stays draft; auto-merge is not enabled and it has not been enqueued.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Unblocked — the ceiling this was parked behind is fixed. main merged in; landing once green.

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Retiring the "Blocked, deliberately" notice above (5520817103) — its blocker no longer holds.

#7399 was ruled (maintainer, 2026-09-03, option A′) and landed as 177afeba1 (PR #7489). The framework chunk was 78.7% @object-ui/i18n locale catalogue, because framework and infrastructure tied at priority 80 and framework was written first. The catalogue now has its own i18n-locales chunk and both ceilings were re-baselined in the same change.

⭐ This PR's own analysis was right, and the fix vindicates it

That notice argued the failure was attribution, not growth:

the bytes are a relocation, not growth … This PR's true payload cost is +16 bytes across a 3.25 MB closure, and the aggregate ceiling passes.

That was correct, and it was correct for a deeper reason than the notice knew: the ceiling it tripped was not budgeting core|react|types at all. framework now measures ~60 KB against a 71,000 ceiling (baseline 61,465), where before it measured 523,823 against 524,000 — 41 bytes of headroom for the whole repository. This PR's +579 B fits with room to spare.

⚠️ For the record, and against my own earlier reasoning: I later argued on several PRs that a red here could be diagnosed from the path rule (/packages/(core|react|types)/). That rule is false in this repo — it names the chunk but not its contents — and I published three wrong causal stories from it before the measurement settled the question. The full retraction is on #7399. This PR's notice reasoned better than I did about its own failure.

What I did

Merged main in so CI rebuilds against the new grouping. ⛔ No ceiling was raised, ⛔ nothing was shrunk to fit, ⛔ no re-run spent — exactly as that notice promised.

⇒ Once every check is green by name, this seat lands it. The implementation review stands on the card (#7178 comment 5520776537), including the one place the ruling's pin is not literally satisfiable for datetime and why option A was taken anyway.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3180.9 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-DrgUn1yZ.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.24KB117.82KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.19KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.53KB27.66KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-project-manager
os-project-manager marked this pull request as ready for review September 3, 2026 11:09
@os-project-manager
os-project-manager added this pull request to the merge queueSep 3, 2026
@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Landing — 32/32 green, ready + auto-merge

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Following the unblock notice above (5524649818).

Every check read by name, not in aggregate

32 total: 27 success, 5 skipped, on head 5daf008b9. The five skips are the jobs that do not run on this event (dependabot, Test (coverage), Test (coverage shard …) and siblings) — ⛔ none is a check that ran and was ignored.

The ones that were the actual question:

checkreading
Bundle AnalysisPASS — 3180.9 KB / 3191.4 KB, 50 chunks (was 48)
Test (shard 1/4)4/4✅ all four success (11:06:56 / 11:09:02 / 11:08:57 / 11:08:33)
Type Check✅ success 11:03:13
Lint✅ success
Governed Surface Queue Guard✅ success
README Export Check✅ success — the check that would object to the lift's new @object-ui/core exports

The landing authority, stated

Clause-② is no on this card, ruled explicitly: 「Execution: domain:ui lane, S. Clause-② no (renderer side, no declared surface moves).」 (5509025252).

⚠️ I re-derived that from the diff rather than resting on the ruling's prediction, because the gate's own rule is 「diff 是事实,卡片语义是预测」 — and this diff does move symbols between packages, which the ruling's "no declared surface moves" did not anticipate. It still reads no: the path limb does not fire (nothing under packages/spec/src/**, no *.zod.ts), and the content limb does not either — no contract accept/reject behaviour changes, DatasetMeasureSchema is untouched, and nothing an author may write is newly accepted or refused. The four lifted names land in @object-ui/core and are re-exported from @object-ui/fields, so no consumer's import path or behaviour moves. README Export Check is green over exactly that surface.

⇒ Seat lands it. The implementation review is the ACCEPT on the card (#7178 comment 5520776537).

The one flag carried into landing, deliberately

That ACCEPT recorded, and did not paper over, that the ruling's pin is not literally satisfiable for datetime: DateTimeCellRenderer inlines toLocaleDateString/toLocaleTimeString and never calls formatDateTime, which has zero call sites — so there is no shared datetime path to route through. Option A was taken (dispatch to formatDateTime, the module's own declared datetime function, sniffing date-vs-datetime the way ObjectGantt already does), and the ACCEPT said plainly: "If the director seat or the maintainer reads the pin's wording as binding on datetime specifically, this is the place to object."

That objection window has been open since 05:06 today and the director seat has been active in this repo since (it ruled #7402 at 08:44). No objection was entered, so the flag lands as recorded rather than silently. The reconciliation itself is filed as #7443 and is not closed by this PR.

Follow-up discharged

The ruling also owed a one-line spec-lane follow-up, "filed by the domain:ui seat when the measurement is in hand". The measurement is in hand, and it is filed: objectstack#14933.

⭐ It does not copy the ruling's suggested sentence. The ruling assumed a date measure cannot read format at all; measured, 'short' and 'relative'are read and only the pattern case is unreachable — so "date measures render by locale" would have replaced one inaccuracy with another. The card says so and proposes wording that matches the measurement. It also records that there is no .describe() on that field to add a sentence to (packages/spec/src/ui/dataset.zod.ts:191-192 carries a JSDoc instead), which is a choice the spec lane should make deliberately rather than have made for it.


Generated by Claude Code

Sign up for freeto 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.

A dataset measure over a date field renders its raw ISO string — formatMeasure short-circuits before format is read

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(core,fields): render a dataset measure over a date field as a date - #7442

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-7178-formatmeasure-date
Sep 3, 2026
Merged

fix(core,fields): render a dataset measure over a date field as a date#7442
os-project-manager merged 2 commits into
mainfrom
claude/issue-7178-formatmeasure-date

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#7178

Implements the director-seat ruling in comment 5509025252 (summon #8, 2026-09-02, maintainer verbatim 7189 A 其他同意) — option A. min / max over a date or datetime field stays a legal measure; nothing in @objectstack/spec is touched, and DatasetMeasureSchema is not narrowed.

The defect

formatMeasure in packages/core/src/utils/dataset-format.ts opened with:

if(v==null)return'—';if(typeofv!=='number')returnString(v);

The second line runs beforeformat is read, so for any non-numeric value the function was String(v) and nothing else. A min over a datetime rendered as a raw 24-character ISO string in the KPI tile's text-2xl font-semibold, wrapping to two lines, and the format the four call sites carefully thread through was dead on this path.

A date-shaped value now routes to the date display path ahead of that short-circuit. Because the fix lands in formatMeasure, all four call sites are served at once — the metric tile (DatasetWidget.tsx:768), chart values (:876), dataset table cells (:1083) and the metadata-admin dataset preview (DatasetPreview.tsx:235). None of them changed. PivotTable.tsx takes a number outright and is untouched.

⭐ The shared date path — what I found, and what reusing it cost

File + symbol: formatDate / formatDateTime / formatRelativeDate, previously in packages/fields/src/index.tsx (@object-ui/fields), now in packages/core/src/utils/date-display.ts.

That is the path a list cell uses: DateCellRenderer (registered for field type date at packages/fields/src/index.tsx) calls formatDate(value, field.format || 'relative', { dueLike, locale, t }), and ObjectGrid's date cells, ObjectGantt's tooltips and plugin-dashboard's recordFields all call the same two functions.

Reusing it required lifting it, and that is the substance of this PR. @object-ui/fields is a React package and depends on @object-ui/core; core is the React-free engine, so it could not import from it. That is exactly the objectui#4576 situation one type over — there, formatDisplayNumber lived in @object-ui/i18n, dataset-format.ts could not reach it, the two implementations drifted, and a German session read 1.234,5 % from a list cell beside 1.234,5% from a dashboard measure. #4576 removed the boundary by moving the pure function down into core and re-exporting from above.

This PR applies that same remedy:

  • DateDisplayOptions, formatRelativeDays, formatRelativeDate, formatDate and formatDateTime move verbatim into packages/core/src/utils/date-display.ts. No logic changed.
  • @object-ui/fields re-exports all four public names, so no consumer's import path or behaviour changes (ObjectGrid, ObjectGantt, plugin-dashboard, @object-ui/react).
  • packages/fields/src/__tests__/date-display.reexport-identity.test.ts pins reference identity — the cell renderer's formatDate and the measure formatter's are the same function object. A second copy would pass every behavioural assertion in the repo and fail only there.

⛔ No date formatter was written in dataset-format.ts. The new code there is a 12-line route: two anchored ISO shape tests, a Date.parse guard, and a call.

The date/datetime split mirrors ObjectGantt.tsx's sniffed-ISO dispatch verbatim, because that site is already the repo's answer to this exact situation — an ISO string with no field type attached, which is what a measure is by construction. That is why the absolute locale form is used rather than DateCellRenderer's || 'relative' default, which is keyed on knowing the value is a datefield in a row.

⭐ The format reachability answer — measured

The shared date path accepts format as a named STYLE, not as a date pattern.

formatDate(value, style, options)'s vocabulary is 'short' and 'relative'; every other string falls to its default locale-medium branch. So, for a date measure:

formatreachable?renders
'short'✅ yesJul 4, '24 — the same word DateCellRenderer honours from field.format
'relative'✅ yes3 days ago
'YYYY-MM-DD'❌ nothe locale default (Jul 4, 2024)
absentn/athe locale default

Threading the measure's format into that parameter is the same mapping the list cell already makes, not a new one — and it is not novel behaviour for a pattern either: plugin-dashboard's recordFields.tsx already routes a date-shaped format (/[YMDHms]/) into the same style slot and gets the same locale default.

⇒ Per the ruling's contingency, the remaining gap is the pattern case, not the whole of format. If the spec seat files the one-line describe() follow-up, "applies to numeric measures; date measures render by locale" would be slightly too strong — 'short' and 'relative'are read. Suggested wording: "numeral patterns apply to numeric measures; a date measure reads format as a display style (short, relative)." That follow-up is the domain:ui seat's to file — it is not in this PR.

The limit is pinned by a test rather than left silent, so teaching the shared path a pattern grammar later has a case that must be updated deliberately.

Tests

pnpm exec vitest run from the repo root (⚠️ a package-directory vitest run is refused by the repo's own guard, objectui#3378).

  • packages/core/src/utils/__tests__/dataset-format.date.test.ts13 passed
  • packages/plugin-dashboard/src/__tests__/DatasetWidget.dateMeasure.test.tsx5 passed (the ruling's pin: metric tile + dataset table)
  • packages/fields/src/__tests__/date-display.reexport-identity.test.ts2 passed

Every expected date string in the rendered-surface tests is derived by calling formatDateTime from @object-ui/fields — the module the cell renderers call, by the path they import it by — so the cases assert "the tile shows what a list cell would show", not "the tile shows the string I typed". A literal would pass just as well against a second date convention.

⭐ A numeric measure is byte-identical

A verbatim git show copy of the pre-fix function was compared against the new one across the full argument matrix (value × format × currency × percentScale × locale):

#7178 MATRIX: compared=33696 forms; distinct values that moved=4
(2026-02-30, 2026-07-04, 2026-07-04T07:00:00.000Z, 2026-07-04 07:00:00);
moved forms=3240; expected movers that did NOT move=0

33,696 forms compared; the only values that moved were the four ISO-shaped, parseable ones. Numbers, numeric strings ('1751612400000', '2026', '1e21'), the nullish em dash, arbitrary prose ('Acme Corp', 'March 5, 2026', '2026/07/04') and non-strings are all byte-identical.

Two findings from that sweep, both now pinned:

  • Date.parse('2026-02-30') is notNaN — V8 rolls it to March 2. It routes to the date path and renders Mar 2, which is what a list cell shows for the same stored string. That is agreement, not a defect; rejecting it here is what would create a second convention.
  • An ISO-shaped but unparseable value ('2026-13-45', '2024-07-04T99:99') keeps falling through to String(v). Routing it to formatDate would replace a raw string an author can still debug with an em dash that says nothing.

Red-then-green ablation

Two mutations, each proven on disk by blob hash + anchored grep counts before the run, each restored with git checkout HEAD -- ABSOLUTE_PATH under trap ... EXIT INT TERM, and each restore proven by state (git diff HEAD empty and blob hash equal to the HEAD blob), never by exit code. Directions were written down before the run and all four matched.

Vitest aliases every workspace package to src (vitest.config.mts), so these legs read source directly — no rebuild leg applies, and none is claimed.

legmutationresult
1restore the pre-fix return String(v) short-circuitcore 8 failed / 5 passed, dashboard 3 failed / 2 passed — every date and format case red; ⭐ every numeric must-not-change case stayed green
2replace the re-export with a behaviour-identical local wrapperidentity 1 failed / 1 passed — identity red, behaviour green

Leg 2 is the one that matters for the ruling's constraint: it is a "second date formatter" that behaves identically, and it is caught only by the identity pin. That is the drift objectui#4576 paid for once.

Also run

type-check and lint clean (exit 0) on @object-ui/core, @object-ui/fields, @object-ui/plugin-dashboard, after building each dependency closure.

Bundle budget — framework chunk

⚠️This PR puts framework 402 bytes over its ceiling. The Bundle Analysis check will be red. Reported, not worked around — and ⛔ no ceiling, budget script or PER_CHUNK_* entry is touched in this PR.

Measured with CI's own recipe (pnpm turbo run build --filter='./packages/*'pnpm --filter @object-ui/console buildnode scripts/check-eager-closure-budget.mjs) on both legs: this branch at 5ad78fde, and the branch point 6411def25 as the baseline. Gzipped bytes, read from apps/console/dist/eager-closure.json:

chunkbaselinethis branchdelta
framework523,823524,402+579
ui-components396,598396,088−510
vendor-objectstack948,329948,3290
aggregate eager closure3,254,6043,254,620+16

Verdicts: baseline exit 0 (green), this branch exit 1

❌ framework 512.1 KB / 511.7 KB ceiling (OVER by 0.4 KB)
✅ aggregate 3178.3 KB / 3191.4 KB ceiling (headroom 13.1 KB)

What the bytes are. They are not new code. This change is a relocation: the date formatter family leaves packages/fields (ui-components, −510 B) and lands in packages/core (framework, +579 B), because framework is /packages/(core|react|types)/ by path. The net cost to the payload a browser actually fetches is +16 bytes across a 3.25 MB eager closure — the two chunk rows nearly cancel, and the aggregate ceiling keeps 13.1 KB of headroom.

So the red is chunk attribution, not payload growth: framework had 177 bytes of headroom before this branch, and moving a shared function into the package that group is defined by consumes it. The relocation is the ruled design — core must own the date path because it is the React-free engine that formatMeasure lives in — so shrinking it back means either a second date formatter in dataset-format.ts (⛔ forbidden by the ruling and by objectui#4576) or leaving the defect unfixed.

⇒ This lands on the same blocked ceiling as #7391 and #7400, pending the maintainer's decision in #7399. Routing that is the PM's call, not this PR's.

Changeset

@object-ui/core: patchand@object-ui/fields: patch. The ruling named a @object-ui/core patch; @object-ui/fields is added because the lift genuinely changes that package's source and its dependency on core for those symbols. Flagging it as a deliberate, minimal deviation rather than a silent one.


🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

`formatMeasure` opened with `if (typeof v !== 'number') return String(v)`,
placed before `format` was ever read. A `min` / `max` measure over a date or
datetime field therefore printed its stored value verbatim -- a 24-character
ISO string in the KPI tile's `text-2xl font-semibold`, wrapping to two lines --
and the `format` that `DatasetMeasureSchema` accepts could never be read on
that path. A date-shaped value now routes to the date display path ahead of
that short-circuit, so all four dataset-bound surfaces are served at once.
No second date formatter was written. `formatDate`, `formatDateTime`,
`formatRelativeDate` and `DateDisplayOptions` MOVED from `@object-ui/fields`'
barrel down into `@object-ui/core` (`utils/date-display.ts`) -- the same remedy
objectui#4576 applied to `formatDisplayNumber`, for the same reason: `core` is
the React-free engine and could not import from a React package, so the
alternative was a parallel date convention in `dataset-format.ts`, which is the
drift #4576 already paid for once in percent. `@object-ui/fields` re-exports all
four names unchanged and a reference-identity test pins that the cell renderer
and the measure formatter call the same function object.
Numeric measures are byte-identical: 33,696 argument forms compared against a
verbatim copy of the pre-fix function, and the only values that moved were the
four ISO-shaped, parseable ones.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.3 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CyXQUsBx.js
StatusFAIL

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 halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.14KB13.02KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.27KB64.20KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Blocked, deliberately: Bundle Analysis red on a governance decision this seat cannot make

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Stating the blocker here on the PR rather than only on the card, so a reviewer landing on this page sees why it is red and why nothing is being pushed.

Failing check:Bundle Analysis (100528606288) on 5ad78fde. Every other check is green or skipped.

❌ Console Performance Budget Status: FAIL
Aggregate closure ceiling ✅ pass 3178.3 KB / 3191.4 KB
Per-chunk ceilings ❌ over its ceiling
Ceiling sensitivity (headroom) ✅ pass
Ceiling freshness ✅ pass
chunkbeforeafterceiling
framework523,823524,402 (+579)524,000 — over by 402
ui-components396,598396,088 (−510)399,000
aggregate eager closure3,254,6043,254,620 (+16)passes

It is this PR's byte cost, and it is still not this PR's to fix

The failure is real and attributable — I am not claiming otherwise. But the bytes are a relocation, not growth: the framework group is defined by path (apps/console/vite.config.ts:742, /packages/(core|react|types)/), so code moving intopackages/core scores against it even though an equal amount left ui-components. This PR's true payload cost is +16 bytes across a 3.25 MB closure, and the aggregate ceiling passes. CI's own package table shows both halves: fields 61.71 → 61.26 KB gz, core 2.32 → 2.42 KB gz.

The relocation is not optional. The ruling on #7178 requires routing through the date path list cells already use and forbids a second date convention; packages/core is the React-free engine and cannot import from packages/fields, so the shared formatDate / formatDateTime had to move down — the same move objectui#4576 made for formatDisplayNumber.

What I am NOT doing, and why

Where it goes

Parked behind #7399 (the open framework ceiling decision), alongside #7391 and #7400 — this is now the third PR blocked there. New evidence posted to #7399 at 5520783051: this PR is the first case where the gate reds on a change whose aggregate cost is +16 bytes, which is a property of how the per-chunk ceiling attributes bytes rather than of the change.

Full review of the implementation — including the one place the ruling's pin is not literally satisfiable for datetime, and why option A was taken anyway — is on the card at 5520776537. The PR stays draft; auto-merge is not enabled and it has not been enqueued.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Unblocked — the ceiling this was parked behind is fixed. main merged in; landing once green.

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Retiring the "Blocked, deliberately" notice above (5520817103) — its blocker no longer holds.

#7399 was ruled (maintainer, 2026-09-03, option A′) and landed as 177afeba1 (PR #7489). The framework chunk was 78.7% @object-ui/i18n locale catalogue, because framework and infrastructure tied at priority 80 and framework was written first. The catalogue now has its own i18n-locales chunk and both ceilings were re-baselined in the same change.

⭐ This PR's own analysis was right, and the fix vindicates it

That notice argued the failure was attribution, not growth:

the bytes are a relocation, not growth … This PR's true payload cost is +16 bytes across a 3.25 MB closure, and the aggregate ceiling passes.

That was correct, and it was correct for a deeper reason than the notice knew: the ceiling it tripped was not budgeting core|react|types at all. framework now measures ~60 KB against a 71,000 ceiling (baseline 61,465), where before it measured 523,823 against 524,000 — 41 bytes of headroom for the whole repository. This PR's +579 B fits with room to spare.

⚠️ For the record, and against my own earlier reasoning: I later argued on several PRs that a red here could be diagnosed from the path rule (/packages/(core|react|types)/). That rule is false in this repo — it names the chunk but not its contents — and I published three wrong causal stories from it before the measurement settled the question. The full retraction is on #7399. This PR's notice reasoned better than I did about its own failure.

What I did

Merged main in so CI rebuilds against the new grouping. ⛔ No ceiling was raised, ⛔ nothing was shrunk to fit, ⛔ no re-run spent — exactly as that notice promised.

⇒ Once every check is green by name, this seat lands it. The implementation review stands on the card (#7178 comment 5520776537), including the one place the ruling's pin is not literally satisfiable for datetime and why option A was taken anyway.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3180.9 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-DrgUn1yZ.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.24KB117.82KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.19KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.53KB27.66KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-project-manager
os-project-manager marked this pull request as ready for review September 3, 2026 11:09
@os-project-manager
os-project-manager added this pull request to the merge queueSep 3, 2026
@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Landing — 32/32 green, ready + auto-merge

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Following the unblock notice above (5524649818).

Every check read by name, not in aggregate

32 total: 27 success, 5 skipped, on head 5daf008b9. The five skips are the jobs that do not run on this event (dependabot, Test (coverage), Test (coverage shard …) and siblings) — ⛔ none is a check that ran and was ignored.

The ones that were the actual question:

checkreading
Bundle AnalysisPASS — 3180.9 KB / 3191.4 KB, 50 chunks (was 48)
Test (shard 1/4)4/4✅ all four success (11:06:56 / 11:09:02 / 11:08:57 / 11:08:33)
Type Check✅ success 11:03:13
Lint✅ success
Governed Surface Queue Guard✅ success
README Export Check✅ success — the check that would object to the lift's new @object-ui/core exports

The landing authority, stated

Clause-② is no on this card, ruled explicitly: 「Execution: domain:ui lane, S. Clause-② no (renderer side, no declared surface moves).」 (5509025252).

⚠️ I re-derived that from the diff rather than resting on the ruling's prediction, because the gate's own rule is 「diff 是事实,卡片语义是预测」 — and this diff does move symbols between packages, which the ruling's "no declared surface moves" did not anticipate. It still reads no: the path limb does not fire (nothing under packages/spec/src/**, no *.zod.ts), and the content limb does not either — no contract accept/reject behaviour changes, DatasetMeasureSchema is untouched, and nothing an author may write is newly accepted or refused. The four lifted names land in @object-ui/core and are re-exported from @object-ui/fields, so no consumer's import path or behaviour moves. README Export Check is green over exactly that surface.

⇒ Seat lands it. The implementation review is the ACCEPT on the card (#7178 comment 5520776537).

The one flag carried into landing, deliberately

That ACCEPT recorded, and did not paper over, that the ruling's pin is not literally satisfiable for datetime: DateTimeCellRenderer inlines toLocaleDateString/toLocaleTimeString and never calls formatDateTime, which has zero call sites — so there is no shared datetime path to route through. Option A was taken (dispatch to formatDateTime, the module's own declared datetime function, sniffing date-vs-datetime the way ObjectGantt already does), and the ACCEPT said plainly: "If the director seat or the maintainer reads the pin's wording as binding on datetime specifically, this is the place to object."

That objection window has been open since 05:06 today and the director seat has been active in this repo since (it ruled #7402 at 08:44). No objection was entered, so the flag lands as recorded rather than silently. The reconciliation itself is filed as #7443 and is not closed by this PR.

Follow-up discharged

The ruling also owed a one-line spec-lane follow-up, "filed by the domain:ui seat when the measurement is in hand". The measurement is in hand, and it is filed: objectstack#14933.

⭐ It does not copy the ruling's suggested sentence. The ruling assumed a date measure cannot read format at all; measured, 'short' and 'relative'are read and only the pattern case is unreachable — so "date measures render by locale" would have replaced one inaccuracy with another. The card says so and proposes wording that matches the measurement. It also records that there is no .describe() on that field to add a sentence to (packages/spec/src/ui/dataset.zod.ts:191-192 carries a JSDoc instead), which is a choice the spec lane should make deliberately rather than have made for it.


Generated by Claude Code

Sign up for freeto 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.

A dataset measure over a date field renders its raw ISO string — formatMeasure short-circuits before format is read

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(core,fields): render a dataset measure over a date field as a date - #7442

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-7178-formatmeasure-date
Sep 3, 2026
Merged

fix(core,fields): render a dataset measure over a date field as a date#7442
os-project-manager merged 2 commits into
mainfrom
claude/issue-7178-formatmeasure-date

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#7178

Implements the director-seat ruling in comment 5509025252 (summon #8, 2026-09-02, maintainer verbatim 7189 A 其他同意) — option A. min / max over a date or datetime field stays a legal measure; nothing in @objectstack/spec is touched, and DatasetMeasureSchema is not narrowed.

The defect

formatMeasure in packages/core/src/utils/dataset-format.ts opened with:

if(v==null)return'—';if(typeofv!=='number')returnString(v);

The second line runs beforeformat is read, so for any non-numeric value the function was String(v) and nothing else. A min over a datetime rendered as a raw 24-character ISO string in the KPI tile's text-2xl font-semibold, wrapping to two lines, and the format the four call sites carefully thread through was dead on this path.

A date-shaped value now routes to the date display path ahead of that short-circuit. Because the fix lands in formatMeasure, all four call sites are served at once — the metric tile (DatasetWidget.tsx:768), chart values (:876), dataset table cells (:1083) and the metadata-admin dataset preview (DatasetPreview.tsx:235). None of them changed. PivotTable.tsx takes a number outright and is untouched.

⭐ The shared date path — what I found, and what reusing it cost

File + symbol: formatDate / formatDateTime / formatRelativeDate, previously in packages/fields/src/index.tsx (@object-ui/fields), now in packages/core/src/utils/date-display.ts.

That is the path a list cell uses: DateCellRenderer (registered for field type date at packages/fields/src/index.tsx) calls formatDate(value, field.format || 'relative', { dueLike, locale, t }), and ObjectGrid's date cells, ObjectGantt's tooltips and plugin-dashboard's recordFields all call the same two functions.

Reusing it required lifting it, and that is the substance of this PR. @object-ui/fields is a React package and depends on @object-ui/core; core is the React-free engine, so it could not import from it. That is exactly the objectui#4576 situation one type over — there, formatDisplayNumber lived in @object-ui/i18n, dataset-format.ts could not reach it, the two implementations drifted, and a German session read 1.234,5 % from a list cell beside 1.234,5% from a dashboard measure. #4576 removed the boundary by moving the pure function down into core and re-exporting from above.

This PR applies that same remedy:

  • DateDisplayOptions, formatRelativeDays, formatRelativeDate, formatDate and formatDateTime move verbatim into packages/core/src/utils/date-display.ts. No logic changed.
  • @object-ui/fields re-exports all four public names, so no consumer's import path or behaviour changes (ObjectGrid, ObjectGantt, plugin-dashboard, @object-ui/react).
  • packages/fields/src/__tests__/date-display.reexport-identity.test.ts pins reference identity — the cell renderer's formatDate and the measure formatter's are the same function object. A second copy would pass every behavioural assertion in the repo and fail only there.

⛔ No date formatter was written in dataset-format.ts. The new code there is a 12-line route: two anchored ISO shape tests, a Date.parse guard, and a call.

The date/datetime split mirrors ObjectGantt.tsx's sniffed-ISO dispatch verbatim, because that site is already the repo's answer to this exact situation — an ISO string with no field type attached, which is what a measure is by construction. That is why the absolute locale form is used rather than DateCellRenderer's || 'relative' default, which is keyed on knowing the value is a datefield in a row.

⭐ The format reachability answer — measured

The shared date path accepts format as a named STYLE, not as a date pattern.

formatDate(value, style, options)'s vocabulary is 'short' and 'relative'; every other string falls to its default locale-medium branch. So, for a date measure:

formatreachable?renders
'short'✅ yesJul 4, '24 — the same word DateCellRenderer honours from field.format
'relative'✅ yes3 days ago
'YYYY-MM-DD'❌ nothe locale default (Jul 4, 2024)
absentn/athe locale default

Threading the measure's format into that parameter is the same mapping the list cell already makes, not a new one — and it is not novel behaviour for a pattern either: plugin-dashboard's recordFields.tsx already routes a date-shaped format (/[YMDHms]/) into the same style slot and gets the same locale default.

⇒ Per the ruling's contingency, the remaining gap is the pattern case, not the whole of format. If the spec seat files the one-line describe() follow-up, "applies to numeric measures; date measures render by locale" would be slightly too strong — 'short' and 'relative'are read. Suggested wording: "numeral patterns apply to numeric measures; a date measure reads format as a display style (short, relative)." That follow-up is the domain:ui seat's to file — it is not in this PR.

The limit is pinned by a test rather than left silent, so teaching the shared path a pattern grammar later has a case that must be updated deliberately.

Tests

pnpm exec vitest run from the repo root (⚠️ a package-directory vitest run is refused by the repo's own guard, objectui#3378).

  • packages/core/src/utils/__tests__/dataset-format.date.test.ts13 passed
  • packages/plugin-dashboard/src/__tests__/DatasetWidget.dateMeasure.test.tsx5 passed (the ruling's pin: metric tile + dataset table)
  • packages/fields/src/__tests__/date-display.reexport-identity.test.ts2 passed

Every expected date string in the rendered-surface tests is derived by calling formatDateTime from @object-ui/fields — the module the cell renderers call, by the path they import it by — so the cases assert "the tile shows what a list cell would show", not "the tile shows the string I typed". A literal would pass just as well against a second date convention.

⭐ A numeric measure is byte-identical

A verbatim git show copy of the pre-fix function was compared against the new one across the full argument matrix (value × format × currency × percentScale × locale):

#7178 MATRIX: compared=33696 forms; distinct values that moved=4
(2026-02-30, 2026-07-04, 2026-07-04T07:00:00.000Z, 2026-07-04 07:00:00);
moved forms=3240; expected movers that did NOT move=0

33,696 forms compared; the only values that moved were the four ISO-shaped, parseable ones. Numbers, numeric strings ('1751612400000', '2026', '1e21'), the nullish em dash, arbitrary prose ('Acme Corp', 'March 5, 2026', '2026/07/04') and non-strings are all byte-identical.

Two findings from that sweep, both now pinned:

  • Date.parse('2026-02-30') is notNaN — V8 rolls it to March 2. It routes to the date path and renders Mar 2, which is what a list cell shows for the same stored string. That is agreement, not a defect; rejecting it here is what would create a second convention.
  • An ISO-shaped but unparseable value ('2026-13-45', '2024-07-04T99:99') keeps falling through to String(v). Routing it to formatDate would replace a raw string an author can still debug with an em dash that says nothing.

Red-then-green ablation

Two mutations, each proven on disk by blob hash + anchored grep counts before the run, each restored with git checkout HEAD -- ABSOLUTE_PATH under trap ... EXIT INT TERM, and each restore proven by state (git diff HEAD empty and blob hash equal to the HEAD blob), never by exit code. Directions were written down before the run and all four matched.

Vitest aliases every workspace package to src (vitest.config.mts), so these legs read source directly — no rebuild leg applies, and none is claimed.

legmutationresult
1restore the pre-fix return String(v) short-circuitcore 8 failed / 5 passed, dashboard 3 failed / 2 passed — every date and format case red; ⭐ every numeric must-not-change case stayed green
2replace the re-export with a behaviour-identical local wrapperidentity 1 failed / 1 passed — identity red, behaviour green

Leg 2 is the one that matters for the ruling's constraint: it is a "second date formatter" that behaves identically, and it is caught only by the identity pin. That is the drift objectui#4576 paid for once.

Also run

type-check and lint clean (exit 0) on @object-ui/core, @object-ui/fields, @object-ui/plugin-dashboard, after building each dependency closure.

Bundle budget — framework chunk

⚠️This PR puts framework 402 bytes over its ceiling. The Bundle Analysis check will be red. Reported, not worked around — and ⛔ no ceiling, budget script or PER_CHUNK_* entry is touched in this PR.

Measured with CI's own recipe (pnpm turbo run build --filter='./packages/*'pnpm --filter @object-ui/console buildnode scripts/check-eager-closure-budget.mjs) on both legs: this branch at 5ad78fde, and the branch point 6411def25 as the baseline. Gzipped bytes, read from apps/console/dist/eager-closure.json:

chunkbaselinethis branchdelta
framework523,823524,402+579
ui-components396,598396,088−510
vendor-objectstack948,329948,3290
aggregate eager closure3,254,6043,254,620+16

Verdicts: baseline exit 0 (green), this branch exit 1

❌ framework 512.1 KB / 511.7 KB ceiling (OVER by 0.4 KB)
✅ aggregate 3178.3 KB / 3191.4 KB ceiling (headroom 13.1 KB)

What the bytes are. They are not new code. This change is a relocation: the date formatter family leaves packages/fields (ui-components, −510 B) and lands in packages/core (framework, +579 B), because framework is /packages/(core|react|types)/ by path. The net cost to the payload a browser actually fetches is +16 bytes across a 3.25 MB eager closure — the two chunk rows nearly cancel, and the aggregate ceiling keeps 13.1 KB of headroom.

So the red is chunk attribution, not payload growth: framework had 177 bytes of headroom before this branch, and moving a shared function into the package that group is defined by consumes it. The relocation is the ruled design — core must own the date path because it is the React-free engine that formatMeasure lives in — so shrinking it back means either a second date formatter in dataset-format.ts (⛔ forbidden by the ruling and by objectui#4576) or leaving the defect unfixed.

⇒ This lands on the same blocked ceiling as #7391 and #7400, pending the maintainer's decision in #7399. Routing that is the PM's call, not this PR's.

Changeset

@object-ui/core: patchand@object-ui/fields: patch. The ruling named a @object-ui/core patch; @object-ui/fields is added because the lift genuinely changes that package's source and its dependency on core for those symbols. Flagging it as a deliberate, minimal deviation rather than a silent one.


🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

`formatMeasure` opened with `if (typeof v !== 'number') return String(v)`,
placed before `format` was ever read. A `min` / `max` measure over a date or
datetime field therefore printed its stored value verbatim -- a 24-character
ISO string in the KPI tile's `text-2xl font-semibold`, wrapping to two lines --
and the `format` that `DatasetMeasureSchema` accepts could never be read on
that path. A date-shaped value now routes to the date display path ahead of
that short-circuit, so all four dataset-bound surfaces are served at once.
No second date formatter was written. `formatDate`, `formatDateTime`,
`formatRelativeDate` and `DateDisplayOptions` MOVED from `@object-ui/fields`'
barrel down into `@object-ui/core` (`utils/date-display.ts`) -- the same remedy
objectui#4576 applied to `formatDisplayNumber`, for the same reason: `core` is
the React-free engine and could not import from a React package, so the
alternative was a parallel date convention in `dataset-format.ts`, which is the
drift #4576 already paid for once in percent. `@object-ui/fields` re-exports all
four names unchanged and a reference-identity test pins that the cell renderer
and the measure formatter call the same function object.
Numeric measures are byte-identical: 33,696 argument forms compared against a
verbatim copy of the pre-fix function, and the only values that moved were the
four ISO-shaped, parseable ones.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.3 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CyXQUsBx.js
StatusFAIL

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 halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.14KB13.02KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.27KB64.20KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Blocked, deliberately: Bundle Analysis red on a governance decision this seat cannot make

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Stating the blocker here on the PR rather than only on the card, so a reviewer landing on this page sees why it is red and why nothing is being pushed.

Failing check:Bundle Analysis (100528606288) on 5ad78fde. Every other check is green or skipped.

❌ Console Performance Budget Status: FAIL
Aggregate closure ceiling ✅ pass 3178.3 KB / 3191.4 KB
Per-chunk ceilings ❌ over its ceiling
Ceiling sensitivity (headroom) ✅ pass
Ceiling freshness ✅ pass
chunkbeforeafterceiling
framework523,823524,402 (+579)524,000 — over by 402
ui-components396,598396,088 (−510)399,000
aggregate eager closure3,254,6043,254,620 (+16)passes

It is this PR's byte cost, and it is still not this PR's to fix

The failure is real and attributable — I am not claiming otherwise. But the bytes are a relocation, not growth: the framework group is defined by path (apps/console/vite.config.ts:742, /packages/(core|react|types)/), so code moving intopackages/core scores against it even though an equal amount left ui-components. This PR's true payload cost is +16 bytes across a 3.25 MB closure, and the aggregate ceiling passes. CI's own package table shows both halves: fields 61.71 → 61.26 KB gz, core 2.32 → 2.42 KB gz.

The relocation is not optional. The ruling on #7178 requires routing through the date path list cells already use and forbids a second date convention; packages/core is the React-free engine and cannot import from packages/fields, so the shared formatDate / formatDateTime had to move down — the same move objectui#4576 made for formatDisplayNumber.

What I am NOT doing, and why

Where it goes

Parked behind #7399 (the open framework ceiling decision), alongside #7391 and #7400 — this is now the third PR blocked there. New evidence posted to #7399 at 5520783051: this PR is the first case where the gate reds on a change whose aggregate cost is +16 bytes, which is a property of how the per-chunk ceiling attributes bytes rather than of the change.

Full review of the implementation — including the one place the ruling's pin is not literally satisfiable for datetime, and why option A was taken anyway — is on the card at 5520776537. The PR stays draft; auto-merge is not enabled and it has not been enqueued.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Unblocked — the ceiling this was parked behind is fixed. main merged in; landing once green.

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Retiring the "Blocked, deliberately" notice above (5520817103) — its blocker no longer holds.

#7399 was ruled (maintainer, 2026-09-03, option A′) and landed as 177afeba1 (PR #7489). The framework chunk was 78.7% @object-ui/i18n locale catalogue, because framework and infrastructure tied at priority 80 and framework was written first. The catalogue now has its own i18n-locales chunk and both ceilings were re-baselined in the same change.

⭐ This PR's own analysis was right, and the fix vindicates it

That notice argued the failure was attribution, not growth:

the bytes are a relocation, not growth … This PR's true payload cost is +16 bytes across a 3.25 MB closure, and the aggregate ceiling passes.

That was correct, and it was correct for a deeper reason than the notice knew: the ceiling it tripped was not budgeting core|react|types at all. framework now measures ~60 KB against a 71,000 ceiling (baseline 61,465), where before it measured 523,823 against 524,000 — 41 bytes of headroom for the whole repository. This PR's +579 B fits with room to spare.

⚠️ For the record, and against my own earlier reasoning: I later argued on several PRs that a red here could be diagnosed from the path rule (/packages/(core|react|types)/). That rule is false in this repo — it names the chunk but not its contents — and I published three wrong causal stories from it before the measurement settled the question. The full retraction is on #7399. This PR's notice reasoned better than I did about its own failure.

What I did

Merged main in so CI rebuilds against the new grouping. ⛔ No ceiling was raised, ⛔ nothing was shrunk to fit, ⛔ no re-run spent — exactly as that notice promised.

⇒ Once every check is green by name, this seat lands it. The implementation review stands on the card (#7178 comment 5520776537), including the one place the ruling's pin is not literally satisfiable for datetime and why option A was taken anyway.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3180.9 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-DrgUn1yZ.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.24KB117.82KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.19KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.53KB27.66KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-project-manager
os-project-manager marked this pull request as ready for review September 3, 2026 11:09
@os-project-manager
os-project-manager added this pull request to the merge queueSep 3, 2026
@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Landing — 32/32 green, ready + auto-merge

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Following the unblock notice above (5524649818).

Every check read by name, not in aggregate

32 total: 27 success, 5 skipped, on head 5daf008b9. The five skips are the jobs that do not run on this event (dependabot, Test (coverage), Test (coverage shard …) and siblings) — ⛔ none is a check that ran and was ignored.

The ones that were the actual question:

checkreading
Bundle AnalysisPASS — 3180.9 KB / 3191.4 KB, 50 chunks (was 48)
Test (shard 1/4)4/4✅ all four success (11:06:56 / 11:09:02 / 11:08:57 / 11:08:33)
Type Check✅ success 11:03:13
Lint✅ success
Governed Surface Queue Guard✅ success
README Export Check✅ success — the check that would object to the lift's new @object-ui/core exports

The landing authority, stated

Clause-② is no on this card, ruled explicitly: 「Execution: domain:ui lane, S. Clause-② no (renderer side, no declared surface moves).」 (5509025252).

⚠️ I re-derived that from the diff rather than resting on the ruling's prediction, because the gate's own rule is 「diff 是事实,卡片语义是预测」 — and this diff does move symbols between packages, which the ruling's "no declared surface moves" did not anticipate. It still reads no: the path limb does not fire (nothing under packages/spec/src/**, no *.zod.ts), and the content limb does not either — no contract accept/reject behaviour changes, DatasetMeasureSchema is untouched, and nothing an author may write is newly accepted or refused. The four lifted names land in @object-ui/core and are re-exported from @object-ui/fields, so no consumer's import path or behaviour moves. README Export Check is green over exactly that surface.

⇒ Seat lands it. The implementation review is the ACCEPT on the card (#7178 comment 5520776537).

The one flag carried into landing, deliberately

That ACCEPT recorded, and did not paper over, that the ruling's pin is not literally satisfiable for datetime: DateTimeCellRenderer inlines toLocaleDateString/toLocaleTimeString and never calls formatDateTime, which has zero call sites — so there is no shared datetime path to route through. Option A was taken (dispatch to formatDateTime, the module's own declared datetime function, sniffing date-vs-datetime the way ObjectGantt already does), and the ACCEPT said plainly: "If the director seat or the maintainer reads the pin's wording as binding on datetime specifically, this is the place to object."

That objection window has been open since 05:06 today and the director seat has been active in this repo since (it ruled #7402 at 08:44). No objection was entered, so the flag lands as recorded rather than silently. The reconciliation itself is filed as #7443 and is not closed by this PR.

Follow-up discharged

The ruling also owed a one-line spec-lane follow-up, "filed by the domain:ui seat when the measurement is in hand". The measurement is in hand, and it is filed: objectstack#14933.

⭐ It does not copy the ruling's suggested sentence. The ruling assumed a date measure cannot read format at all; measured, 'short' and 'relative'are read and only the pattern case is unreachable — so "date measures render by locale" would have replaced one inaccuracy with another. The card says so and proposes wording that matches the measurement. It also records that there is no .describe() on that field to add a sentence to (packages/spec/src/ui/dataset.zod.ts:191-192 carries a JSDoc instead), which is a choice the spec lane should make deliberately rather than have made for it.


Generated by Claude Code

Sign up for freeto 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.

A dataset measure over a date field renders its raw ISO string — formatMeasure short-circuits before format is read

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(core,fields): render a dataset measure over a date field as a date - #7442

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-7178-formatmeasure-date
Sep 3, 2026
Merged

fix(core,fields): render a dataset measure over a date field as a date#7442
os-project-manager merged 2 commits into
mainfrom
claude/issue-7178-formatmeasure-date

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#7178

Implements the director-seat ruling in comment 5509025252 (summon #8, 2026-09-02, maintainer verbatim 7189 A 其他同意) — option A. min / max over a date or datetime field stays a legal measure; nothing in @objectstack/spec is touched, and DatasetMeasureSchema is not narrowed.

The defect

formatMeasure in packages/core/src/utils/dataset-format.ts opened with:

if(v==null)return'—';if(typeofv!=='number')returnString(v);

The second line runs beforeformat is read, so for any non-numeric value the function was String(v) and nothing else. A min over a datetime rendered as a raw 24-character ISO string in the KPI tile's text-2xl font-semibold, wrapping to two lines, and the format the four call sites carefully thread through was dead on this path.

A date-shaped value now routes to the date display path ahead of that short-circuit. Because the fix lands in formatMeasure, all four call sites are served at once — the metric tile (DatasetWidget.tsx:768), chart values (:876), dataset table cells (:1083) and the metadata-admin dataset preview (DatasetPreview.tsx:235). None of them changed. PivotTable.tsx takes a number outright and is untouched.

⭐ The shared date path — what I found, and what reusing it cost

File + symbol: formatDate / formatDateTime / formatRelativeDate, previously in packages/fields/src/index.tsx (@object-ui/fields), now in packages/core/src/utils/date-display.ts.

That is the path a list cell uses: DateCellRenderer (registered for field type date at packages/fields/src/index.tsx) calls formatDate(value, field.format || 'relative', { dueLike, locale, t }), and ObjectGrid's date cells, ObjectGantt's tooltips and plugin-dashboard's recordFields all call the same two functions.

Reusing it required lifting it, and that is the substance of this PR. @object-ui/fields is a React package and depends on @object-ui/core; core is the React-free engine, so it could not import from it. That is exactly the objectui#4576 situation one type over — there, formatDisplayNumber lived in @object-ui/i18n, dataset-format.ts could not reach it, the two implementations drifted, and a German session read 1.234,5 % from a list cell beside 1.234,5% from a dashboard measure. #4576 removed the boundary by moving the pure function down into core and re-exporting from above.

This PR applies that same remedy:

  • DateDisplayOptions, formatRelativeDays, formatRelativeDate, formatDate and formatDateTime move verbatim into packages/core/src/utils/date-display.ts. No logic changed.
  • @object-ui/fields re-exports all four public names, so no consumer's import path or behaviour changes (ObjectGrid, ObjectGantt, plugin-dashboard, @object-ui/react).
  • packages/fields/src/__tests__/date-display.reexport-identity.test.ts pins reference identity — the cell renderer's formatDate and the measure formatter's are the same function object. A second copy would pass every behavioural assertion in the repo and fail only there.

⛔ No date formatter was written in dataset-format.ts. The new code there is a 12-line route: two anchored ISO shape tests, a Date.parse guard, and a call.

The date/datetime split mirrors ObjectGantt.tsx's sniffed-ISO dispatch verbatim, because that site is already the repo's answer to this exact situation — an ISO string with no field type attached, which is what a measure is by construction. That is why the absolute locale form is used rather than DateCellRenderer's || 'relative' default, which is keyed on knowing the value is a datefield in a row.

⭐ The format reachability answer — measured

The shared date path accepts format as a named STYLE, not as a date pattern.

formatDate(value, style, options)'s vocabulary is 'short' and 'relative'; every other string falls to its default locale-medium branch. So, for a date measure:

formatreachable?renders
'short'✅ yesJul 4, '24 — the same word DateCellRenderer honours from field.format
'relative'✅ yes3 days ago
'YYYY-MM-DD'❌ nothe locale default (Jul 4, 2024)
absentn/athe locale default

Threading the measure's format into that parameter is the same mapping the list cell already makes, not a new one — and it is not novel behaviour for a pattern either: plugin-dashboard's recordFields.tsx already routes a date-shaped format (/[YMDHms]/) into the same style slot and gets the same locale default.

⇒ Per the ruling's contingency, the remaining gap is the pattern case, not the whole of format. If the spec seat files the one-line describe() follow-up, "applies to numeric measures; date measures render by locale" would be slightly too strong — 'short' and 'relative'are read. Suggested wording: "numeral patterns apply to numeric measures; a date measure reads format as a display style (short, relative)." That follow-up is the domain:ui seat's to file — it is not in this PR.

The limit is pinned by a test rather than left silent, so teaching the shared path a pattern grammar later has a case that must be updated deliberately.

Tests

pnpm exec vitest run from the repo root (⚠️ a package-directory vitest run is refused by the repo's own guard, objectui#3378).

  • packages/core/src/utils/__tests__/dataset-format.date.test.ts13 passed
  • packages/plugin-dashboard/src/__tests__/DatasetWidget.dateMeasure.test.tsx5 passed (the ruling's pin: metric tile + dataset table)
  • packages/fields/src/__tests__/date-display.reexport-identity.test.ts2 passed

Every expected date string in the rendered-surface tests is derived by calling formatDateTime from @object-ui/fields — the module the cell renderers call, by the path they import it by — so the cases assert "the tile shows what a list cell would show", not "the tile shows the string I typed". A literal would pass just as well against a second date convention.

⭐ A numeric measure is byte-identical

A verbatim git show copy of the pre-fix function was compared against the new one across the full argument matrix (value × format × currency × percentScale × locale):

#7178 MATRIX: compared=33696 forms; distinct values that moved=4
(2026-02-30, 2026-07-04, 2026-07-04T07:00:00.000Z, 2026-07-04 07:00:00);
moved forms=3240; expected movers that did NOT move=0

33,696 forms compared; the only values that moved were the four ISO-shaped, parseable ones. Numbers, numeric strings ('1751612400000', '2026', '1e21'), the nullish em dash, arbitrary prose ('Acme Corp', 'March 5, 2026', '2026/07/04') and non-strings are all byte-identical.

Two findings from that sweep, both now pinned:

  • Date.parse('2026-02-30') is notNaN — V8 rolls it to March 2. It routes to the date path and renders Mar 2, which is what a list cell shows for the same stored string. That is agreement, not a defect; rejecting it here is what would create a second convention.
  • An ISO-shaped but unparseable value ('2026-13-45', '2024-07-04T99:99') keeps falling through to String(v). Routing it to formatDate would replace a raw string an author can still debug with an em dash that says nothing.

Red-then-green ablation

Two mutations, each proven on disk by blob hash + anchored grep counts before the run, each restored with git checkout HEAD -- ABSOLUTE_PATH under trap ... EXIT INT TERM, and each restore proven by state (git diff HEAD empty and blob hash equal to the HEAD blob), never by exit code. Directions were written down before the run and all four matched.

Vitest aliases every workspace package to src (vitest.config.mts), so these legs read source directly — no rebuild leg applies, and none is claimed.

legmutationresult
1restore the pre-fix return String(v) short-circuitcore 8 failed / 5 passed, dashboard 3 failed / 2 passed — every date and format case red; ⭐ every numeric must-not-change case stayed green
2replace the re-export with a behaviour-identical local wrapperidentity 1 failed / 1 passed — identity red, behaviour green

Leg 2 is the one that matters for the ruling's constraint: it is a "second date formatter" that behaves identically, and it is caught only by the identity pin. That is the drift objectui#4576 paid for once.

Also run

type-check and lint clean (exit 0) on @object-ui/core, @object-ui/fields, @object-ui/plugin-dashboard, after building each dependency closure.

Bundle budget — framework chunk

⚠️This PR puts framework 402 bytes over its ceiling. The Bundle Analysis check will be red. Reported, not worked around — and ⛔ no ceiling, budget script or PER_CHUNK_* entry is touched in this PR.

Measured with CI's own recipe (pnpm turbo run build --filter='./packages/*'pnpm --filter @object-ui/console buildnode scripts/check-eager-closure-budget.mjs) on both legs: this branch at 5ad78fde, and the branch point 6411def25 as the baseline. Gzipped bytes, read from apps/console/dist/eager-closure.json:

chunkbaselinethis branchdelta
framework523,823524,402+579
ui-components396,598396,088−510
vendor-objectstack948,329948,3290
aggregate eager closure3,254,6043,254,620+16

Verdicts: baseline exit 0 (green), this branch exit 1

❌ framework 512.1 KB / 511.7 KB ceiling (OVER by 0.4 KB)
✅ aggregate 3178.3 KB / 3191.4 KB ceiling (headroom 13.1 KB)

What the bytes are. They are not new code. This change is a relocation: the date formatter family leaves packages/fields (ui-components, −510 B) and lands in packages/core (framework, +579 B), because framework is /packages/(core|react|types)/ by path. The net cost to the payload a browser actually fetches is +16 bytes across a 3.25 MB eager closure — the two chunk rows nearly cancel, and the aggregate ceiling keeps 13.1 KB of headroom.

So the red is chunk attribution, not payload growth: framework had 177 bytes of headroom before this branch, and moving a shared function into the package that group is defined by consumes it. The relocation is the ruled design — core must own the date path because it is the React-free engine that formatMeasure lives in — so shrinking it back means either a second date formatter in dataset-format.ts (⛔ forbidden by the ruling and by objectui#4576) or leaving the defect unfixed.

⇒ This lands on the same blocked ceiling as #7391 and #7400, pending the maintainer's decision in #7399. Routing that is the PM's call, not this PR's.

Changeset

@object-ui/core: patchand@object-ui/fields: patch. The ruling named a @object-ui/core patch; @object-ui/fields is added because the lift genuinely changes that package's source and its dependency on core for those symbols. Flagging it as a deliberate, minimal deviation rather than a silent one.


🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

`formatMeasure` opened with `if (typeof v !== 'number') return String(v)`,
placed before `format` was ever read. A `min` / `max` measure over a date or
datetime field therefore printed its stored value verbatim -- a 24-character
ISO string in the KPI tile's `text-2xl font-semibold`, wrapping to two lines --
and the `format` that `DatasetMeasureSchema` accepts could never be read on
that path. A date-shaped value now routes to the date display path ahead of
that short-circuit, so all four dataset-bound surfaces are served at once.
No second date formatter was written. `formatDate`, `formatDateTime`,
`formatRelativeDate` and `DateDisplayOptions` MOVED from `@object-ui/fields`'
barrel down into `@object-ui/core` (`utils/date-display.ts`) -- the same remedy
objectui#4576 applied to `formatDisplayNumber`, for the same reason: `core` is
the React-free engine and could not import from a React package, so the
alternative was a parallel date convention in `dataset-format.ts`, which is the
drift #4576 already paid for once in percent. `@object-ui/fields` re-exports all
four names unchanged and a reference-identity test pins that the cell renderer
and the measure formatter call the same function object.
Numeric measures are byte-identical: 33,696 argument forms compared against a
verbatim copy of the pre-fix function, and the only values that moved were the
four ISO-shaped, parseable ones.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3178.3 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CyXQUsBx.js
StatusFAIL

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 halfVerdict
Aggregate closure ceiling✅ pass
Per-chunk ceilings❌ over its ceiling
Ceiling sensitivity (headroom)✅ pass
Ceiling freshness (checkout vs. base branch)✅ pass

📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.14KB13.02KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.27KB64.20KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Blocked, deliberately: Bundle Analysis red on a governance decision this seat cannot make

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Stating the blocker here on the PR rather than only on the card, so a reviewer landing on this page sees why it is red and why nothing is being pushed.

Failing check:Bundle Analysis (100528606288) on 5ad78fde. Every other check is green or skipped.

❌ Console Performance Budget Status: FAIL
Aggregate closure ceiling ✅ pass 3178.3 KB / 3191.4 KB
Per-chunk ceilings ❌ over its ceiling
Ceiling sensitivity (headroom) ✅ pass
Ceiling freshness ✅ pass
chunkbeforeafterceiling
framework523,823524,402 (+579)524,000 — over by 402
ui-components396,598396,088 (−510)399,000
aggregate eager closure3,254,6043,254,620 (+16)passes

It is this PR's byte cost, and it is still not this PR's to fix

The failure is real and attributable — I am not claiming otherwise. But the bytes are a relocation, not growth: the framework group is defined by path (apps/console/vite.config.ts:742, /packages/(core|react|types)/), so code moving intopackages/core scores against it even though an equal amount left ui-components. This PR's true payload cost is +16 bytes across a 3.25 MB closure, and the aggregate ceiling passes. CI's own package table shows both halves: fields 61.71 → 61.26 KB gz, core 2.32 → 2.42 KB gz.

The relocation is not optional. The ruling on #7178 requires routing through the date path list cells already use and forbids a second date convention; packages/core is the React-free engine and cannot import from packages/fields, so the shared formatDate / formatDateTime had to move down — the same move objectui#4576 made for formatDisplayNumber.

What I am NOT doing, and why

Where it goes

Parked behind #7399 (the open framework ceiling decision), alongside #7391 and #7400 — this is now the third PR blocked there. New evidence posted to #7399 at 5520783051: this PR is the first case where the gate reds on a change whose aggregate cost is +16 bytes, which is a property of how the per-chunk ceiling attributes bytes rather than of the change.

Full review of the implementation — including the one place the ruling's pin is not literally satisfiable for datetime, and why option A was taken anyway — is on the card at 5520776537. The PR stays draft; auto-merge is not enabled and it has not been enqueued.


Generated by Claude Code

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Unblocked — the ceiling this was parked behind is fixed. main merged in; landing once green.

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Retiring the "Blocked, deliberately" notice above (5520817103) — its blocker no longer holds.

#7399 was ruled (maintainer, 2026-09-03, option A′) and landed as 177afeba1 (PR #7489). The framework chunk was 78.7% @object-ui/i18n locale catalogue, because framework and infrastructure tied at priority 80 and framework was written first. The catalogue now has its own i18n-locales chunk and both ceilings were re-baselined in the same change.

⭐ This PR's own analysis was right, and the fix vindicates it

That notice argued the failure was attribution, not growth:

the bytes are a relocation, not growth … This PR's true payload cost is +16 bytes across a 3.25 MB closure, and the aggregate ceiling passes.

That was correct, and it was correct for a deeper reason than the notice knew: the ceiling it tripped was not budgeting core|react|types at all. framework now measures ~60 KB against a 71,000 ceiling (baseline 61,465), where before it measured 523,823 against 524,000 — 41 bytes of headroom for the whole repository. This PR's +579 B fits with room to spare.

⚠️ For the record, and against my own earlier reasoning: I later argued on several PRs that a red here could be diagnosed from the path rule (/packages/(core|react|types)/). That rule is false in this repo — it names the chunk but not its contents — and I published three wrong causal stories from it before the measurement settled the question. The full retraction is on #7399. This PR's notice reasoned better than I did about its own failure.

What I did

Merged main in so CI rebuilds against the new grouping. ⛔ No ceiling was raised, ⛔ nothing was shrunk to fit, ⛔ no re-run spent — exactly as that notice promised.

⇒ Once every check is green by name, this seat lands it. The implementation review stands on the card (#7178 comment 5520776537), including the one place the ruling's pin is not literally satisfiable for datetime and why option A was taken anyway.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3180.9 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-DrgUn1yZ.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.24KB117.82KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.27KB13.32KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.79KB34.62KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.19KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.18KB40.94KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.53KB27.66KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-project-manager
os-project-manager marked this pull request as ready for review September 3, 2026 11:09
@os-project-manager
os-project-manager added this pull request to the merge queueSep 3, 2026
@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ Landing — 32/32 green, ready + auto-merge

domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Following the unblock notice above (5524649818).

Every check read by name, not in aggregate

32 total: 27 success, 5 skipped, on head 5daf008b9. The five skips are the jobs that do not run on this event (dependabot, Test (coverage), Test (coverage shard …) and siblings) — ⛔ none is a check that ran and was ignored.

The ones that were the actual question:

checkreading
Bundle AnalysisPASS — 3180.9 KB / 3191.4 KB, 50 chunks (was 48)
Test (shard 1/4)4/4✅ all four success (11:06:56 / 11:09:02 / 11:08:57 / 11:08:33)
Type Check✅ success 11:03:13
Lint✅ success
Governed Surface Queue Guard✅ success
README Export Check✅ success — the check that would object to the lift's new @object-ui/core exports

The landing authority, stated

Clause-② is no on this card, ruled explicitly: 「Execution: domain:ui lane, S. Clause-② no (renderer side, no declared surface moves).」 (5509025252).

⚠️ I re-derived that from the diff rather than resting on the ruling's prediction, because the gate's own rule is 「diff 是事实,卡片语义是预测」 — and this diff does move symbols between packages, which the ruling's "no declared surface moves" did not anticipate. It still reads no: the path limb does not fire (nothing under packages/spec/src/**, no *.zod.ts), and the content limb does not either — no contract accept/reject behaviour changes, DatasetMeasureSchema is untouched, and nothing an author may write is newly accepted or refused. The four lifted names land in @object-ui/core and are re-exported from @object-ui/fields, so no consumer's import path or behaviour moves. README Export Check is green over exactly that surface.

⇒ Seat lands it. The implementation review is the ACCEPT on the card (#7178 comment 5520776537).

The one flag carried into landing, deliberately

That ACCEPT recorded, and did not paper over, that the ruling's pin is not literally satisfiable for datetime: DateTimeCellRenderer inlines toLocaleDateString/toLocaleTimeString and never calls formatDateTime, which has zero call sites — so there is no shared datetime path to route through. Option A was taken (dispatch to formatDateTime, the module's own declared datetime function, sniffing date-vs-datetime the way ObjectGantt already does), and the ACCEPT said plainly: "If the director seat or the maintainer reads the pin's wording as binding on datetime specifically, this is the place to object."

That objection window has been open since 05:06 today and the director seat has been active in this repo since (it ruled #7402 at 08:44). No objection was entered, so the flag lands as recorded rather than silently. The reconciliation itself is filed as #7443 and is not closed by this PR.

Follow-up discharged

The ruling also owed a one-line spec-lane follow-up, "filed by the domain:ui seat when the measurement is in hand". The measurement is in hand, and it is filed: objectstack#14933.

⭐ It does not copy the ruling's suggested sentence. The ruling assumed a date measure cannot read format at all; measured, 'short' and 'relative'are read and only the pattern case is unreachable — so "date measures render by locale" would have replaced one inaccuracy with another. The card says so and proposes wording that matches the measurement. It also records that there is no .describe() on that field to add a sentence to (packages/spec/src/ui/dataset.zod.ts:191-192 carries a JSDoc instead), which is a choice the spec lane should make deliberately rather than have made for it.


Generated by Claude Code

Sign up for freeto 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.

A dataset measure over a date field renders its raw ISO string — formatMeasure short-circuits before format is read

2 participants

@os-project-manager@claude