Skip to content

fix(plugin-timeline,core): the gantt axis draws a stored date-only day on that day in every zone, and the formula date functions do a calendar day's arithmetic on the UTC calendar (objectui#10866, slice 3) - #10957

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-10866-gantt-axis-d1-arithmetic
Sep 28, 2026

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Part of #10866
Clause-②: no

Slice 3 of the date-only zone family's carrier card: the timeline renderer's gantt variant and the D1 day arithmetic of core FormulaFunctions, per release note 5866905836, ruling 5864664470 (objectui#10903, its last execution parameter) and claim 5867569822. The card stays open for slice 1's owed items, listed under Acceptance notes.

Why Clause-②: no: stored date-only values read and compute as the stored day. No schema, published type or accepted set moves, and ruling 5864664470 states the arithmetic needs no statement.

What changed

Timeline, gantt variant (plugin-timeline renderer.tsx)

One read, readGanttDate: a date string goes through core toDisplayDate (the objectui#10183 convention, which the bar tooltip's formatDate has taken since slice 2), so a date-only 2026-10-05 is local midnight of the day it names and a value with a time part keeps its instant. A number or a Date is an instant and is read by new Date(value) as before; for a Date that is a fresh copy, so no method the authored object carries is called afterwards (objectui#7027). Every stop on the gantt branch now reads through it:

  • generateTimeScaleHeaders (exported): both ends. The walk moves and prints with local setters and getters, so a date-only end starts it on that day in every zone.
  • calculateDateRange: reduces with the same read and prints each end as the viewer's calendar day (toDateInputValue of a Date, local getters). toISOString()'s date part named the day before east of UTC once the read is local.
  • calculateBarDimensions: all four ends.
  • findUnusableGanttDate: its "does it parse" half asks the same read, so 2026-02-30 is refused with the existing timeline.gantt.unusableRange.malformedDate message (no new key), where the engine rolled it into March and the bar was drawn there. Kept on the engine parse, the gate would pass a value the reduce below it reads as an Invalid Date.
  • The min-over-max guard: both ends, as the headers read them.
  • emptyGanttDateRange: the viewer's today (toDateInputValue(new Date())), where it was the UTC day.

No import line of the renderer changed: toDisplayDate and toDateInputValue were already imported from @object-ui/core.

Core formula date functions (core evaluator/FormulaFunctions.ts)

  • readFormulaDate: the engine parse (a date-only value is UTC midnight of its day, as before), the same named Invalid date error, and a day flag from isRealCalendarDate, the shared judgement, imported inside core from utils/date-display (already on core's barrel).
  • DATEADD: a calendar day goes through addToDay, which uses UTC setters. Its months go through addMonthsUtc: step to the 1st, move the month, clamp the day to the target month's last day. That is the rule of objectstack @objectstack/formula's stdlib addMonthsUtc behind CEL addMonths(d, n). A year is twelve months on the same rule. A day moved by days, months or years comes back as YYYY-MM-DD; hours and minutes give an instant (UTC midnight of the day, moved). Anything else (an instant, or a date-only string naming a day its month does not have) keeps the base code path verbatim: local setters, setMonth with no clamp, an instant out.
  • DATEDIFF: the month and year units read a day's fields with UTC getters and an instant's with local getters (calendarFields), so two instants count exactly as on the base. Day, hour and minute units are millisecond differences, unchanged.
  • DATEFORMAT: a day's fields with UTC getters, an instant's with local getters (calendarFields), as on the base.
  • TODAY() and NOW() are untouched. The objectui#10915 docblock on TODAY() stands, and a new docblock above DATEADD states the three functions' rule.

Pins, docs, changeset

  • Two pins registered in PINS of scripts/__tests__/date-only-zone-pins-10183.test.ts, plus one header sentence: TimelineGantt.dateOnlyZone-10866 (plugin-timeline) and FormulaFunctions.dateOnlyZone-10866 (core). Each runs under America/Los_Angeles and Asia/Shanghai, each zone opens with a rig case (the gantt pin adds a fixture-validity case per zone), and each carries instant controls. The gantt pin reads the named producer, examples/schema-catalog/src/schemas/plugin-timeline/gantt-style-timeline.json, from disk. The formula pin's every-zone cases also run in the suite zone.
  • Docs: one paragraph in content/docs/plugins/plugin-timeline.mdx, "Time Scales (Gantt)".
  • Changeset .changeset/10866-gantt-axis-formula-day-arithmetic.md: patch for @object-ui/plugin-timeline and @object-ui/core.

Readings, base 3b469c8ea against head, in both zones

Base readings: the base sources with the two new pins, in a separate comparison worktree, forks pool with the driver's flag (Tests 27 failed | 24 passed (51), every red a row below), plus a direct probe of core FormulaFunctions under TZ. Head readings: the pins, green.

Gantt, en-US:

reading LA base LA head Shanghai base Shanghai head
producer gantt-style-timeline.json, month axis Dec 2023, Jan 2024, Mar 2024, Apr 2024 Jan 2024, Feb 2024, Mar 2024, Apr 2024 Jan 2024 … Apr 2024 same
computed day axis over bars 2026-10-05…2026-10-07 Oct 4, Oct 5, Oct 6 Oct 5, Oct 6, Oct 7 Oct 5, Oct 6, Oct 7 same
pinned 2026-10-05…2026-10-07, instant bar from 2026-10-06T04:00Z, its left axis Oct 4, Oct 5, Oct 6 (red before the bars) 43.75% (21:00 on the 5th) 58.333…% (the UTC hour) 75% (12:00 on the 6th)
pinned start 2026-10-06, pinned end 2026-10-06T03:00Z draws refused as inverted (the end is 20:00 on the 5th here) draws draws, axis Oct 6
empty plan at 2026-10-05T20:00Z, day scale Oct 4 Oct 5 Oct 5 Oct 6
bar starting 2026-02-30 (suite zone, UTC) no refusal: the gate let it through refused, naming items[0].items[0].startDate

Formula functions:

call base head, every zone (UTC, LA, Shanghai)
DATEADD('2026-01-31', 1, 'month') 2026-03-03T00:00:00.000Z in every zone 2026-02-28
DATEADD('2026-03-31', -1, 'months') 2026-03-03T00:00:00.000Z (UTC, Shanghai), 2026-03-03T01:00:00.000Z (LA) 2026-02-28
DATEADD('2024-02-29', 1, 'year') 2025-03-01T00:00:00.000Z in every zone 2025-02-28
DATEADD('2026-09-01', 1, 'day') 2026-09-02T00:00:00.000Z 2026-09-02
DATEADD('2026-11-01', 1, 'day') LA 2026-11-02T01:00:00.000Z; UTC and Shanghai 2026-11-02T00:00:00.000Z 2026-11-02
DATEADD('2026-03-08', 1, 'day') LA 2026-03-08T23:00:00.000Z; UTC and Shanghai 2026-03-09T00:00:00.000Z 2026-03-09
DATEADD('2026-09-01', 1, 'month') LA 2026-10-02T00:00:00.000Z; UTC and Shanghai 2026-10-01T00:00:00.000Z 2026-10-01
DATEADD('2026-01-31T12:00:00.000Z', 1, 'month') (instant) 2026-03-03T12:00:00.000Z in every zone same
DATEADD('2026-10-31T12:00:00.000Z', 1, 'day') (instant) LA 2026-11-01T13:00:00.000Z; UTC and Shanghai 2026-11-01T12:00:00.000Z same
DATEADD('2026-09-01', 3, 'hours') 2026-09-01T03:00:00.000Z same
DATEFORMAT('2026-09-01', 'YYYY-MM-DD') LA 2026-08-31 2026-09-01
DATEFORMAT('2026-09-01', 'DD/MM/YY HH:mm:ss') LA 31/08/26 17:00:00, Shanghai 01/09/26 08:00:00 01/09/26 00:00:00
DATEFORMAT('2026-09-01T03:00:00.000Z', 'YYYY-MM-DD HH:mm') (instant) LA 2026-08-31 20:00, Shanghai 2026-09-01 11:00, UTC 2026-09-01 03:00 same
DATEDIFF('2025-12-31', '2026-01-01', 'year') LA 0 1
DATEDIFF('2026-08-31', '2026-09-01', 'month') LA 0 1
TODAY() and DATEFORMAT(NOW(), 'YYYY-MM-DD') at 2026-09-28T05:30Z LA 2026-09-28 and 2026-09-27 same

Server parity, @objectstack/formula 17.4.0 as installed here (CEL celEngine.evaluate), the same in LA and Shanghai: addMonths('2026-01-31', 1) is 2026-02-28T00:00Z, addMonths('2024-02-29', 12) is 2025-02-28T00:00Z, addDays('2026-11-01', 1) is 2026-11-02T00:00Z, addDays('2026-10-31T12:00:00.000Z', 1) is 2026-11-01T12:00Z, addMonths('2026-01-31T12:00:00.000Z', 1) is 2026-02-28T12:00Z, and addDays('2026-02-30', 1) is 2026-03-03T00:00Z, which head DATEADD answers as the same instant string.

Tests and gates

Every heavy run went through objectstack's os-verify-lock.sh; each VERDICT line is quoted. The final head is 5b48d0292, the contract-review repair; its readings are in the first bullet below.

  • Repair round, at 5b48d0292 (contract review 5869002399, item 1): instants are back on the base path. A probe of base 3b469c8ea against head under UTC, Los Angeles and Shanghai reads every instant row the same on both: DATEFORMAT('2026-09-01T03:00:00.000Z', 'HH:mm') is 03:00 / 20:00 / 11:00; DATEADD('2026-10-31T12:00:00.000Z', 1, 'day') is 2026-11-01T12:00:00.000Z / 2026-11-01T13:00:00.000Z / 2026-11-01T12:00:00.000Z; DATEADD('2026-01-31T12:00:00.000Z', 1, 'month') is 2026-03-03T12:00:00.000Z in all three. Ten more rows over instants, epoch milliseconds, a zone-less date-time and an impossible day are also all equal, and the date-only rows still move. Under os-verify-lock: closure build exit 0; type-check for core and plugin-timeline echoes tsc --noEmit && tsc -p tsconfig.test.json and Done; pnpm exec vitest run packages/core/ packages/plugin-timeline/ scripts/__tests__/date-only-zone-pins-10183.test.ts --maxWorkers=2 gives Test Files 220 passed (220), Tests 4037 passed | 58 skipped (4095), VERDICT command-exit 0. Red on base, with the base FormulaFunctions.ts blob b5e63812fe3f proven on disk: the formula pin in the forks child gives Tests 15 failed | 24 passed (39). The date-only rows are red (the month and year clamp and the output shape in all three zones, the stored day's DATEFORMAT in Los Angeles and Shanghai, DATEDIFF in Los Angeles), and all six instant controls are green. The driver gives Tests 2 failed | 18 passed (20). Restored: blob edb61cdf519c equals HEAD, git diff HEAD empty. Ablation (an instant's hour read with getUTCHours): exactly the Los Angeles and Shanghai instant-format controls go red, Tests 2 failed | 37 passed (39); restored. Gates exit 0: check-changeset-presence, changeset:check, check-changeset-overwrite, check:pending-changeset-literals, check:changeset-claims, check:control-bytes, check:new-line-citations (0 new citations); NOT GOVERNED. ESLint on the two changed code files: 0 errors, FormulaFunctions.ts 26 warnings, as on base. The 11 changeset-reading scripts/__tests__ suites were not re-run locally (two lock queue-timeouts); CI's eight test shards, which include scripts/**, are green. CI on 5b48d0292: 43 check-runs, 40 success, 3 skipped, 0 failed.
  • Red first, base 3b469c8ea plus the two pins (comparison worktree, OBJECTUI_DATE_ZONE_CHILD=1 pnpm exec vitest run over the two pins --pool=forks): Tests 27 failed | 24 passed (51), VERDICT command-exit 1. Every red asserts a head value of the tables above (19 in the formula pin, over its three zones, and 8 in the gantt pin); every rig, fixture-validity and control case green.
  • Reverse validation after commit, at 2006ddb03 (the driver itself): the base blobs of renderer.tsx (445a11147770) and FormulaFunctions.ts (b5e63812fe3f) checked out and proven on disk (each equal to its base blob, not its head blob), then pnpm exec vitest run scripts/__tests__/date-only-zone-pins-10183.test.ts: Tests 3 failed | 17 passed (20), the two new pin rows and the child-exit row; the 17 other pins green. Restored by a trap with git checkout HEAD -- on absolute paths: each blob equal to HEAD (e0bc666ef587, 717f3bd2cdc7), git diff HEAD empty.
  • Head suites, at 2006ddb03: pnpm exec vitest run packages/plugin-timeline/ packages/core/ scripts/__tests__/date-only-zone-pins-10183.test.ts --maxWorkers=2: Test Files 220 passed (220), Tests 4035 passed | 54 skipped (4089), VERDICT command-exit 0. The driver row is green, so every zone case of every registered pin ran and passed in its forks child.
  • At 8dd4543cc (one commit later, which types readFormulaDate's argument instead of casting it): closure build pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-timeline^...' --filter '@object-ui/core^...' run build exit 0; pnpm --workspace-concurrency=2 --filter @object-ui/plugin-timeline --filter @object-ui/core run type-check echoes tsc --noEmit && tsc -p tsconfig.test.json and Done for each; pnpm type-check:scripts exit 0; pnpm exec vitest run packages/core/src/evaluator/ scripts/__tests__/date-only-zone-pins-10183.test.ts --maxWorkers=2: Test Files 18 passed (18), Tests 455 passed | 23 skipped (478) (the 23 are the formula pin's zone cases, run inside the driver's child). VERDICT command-exit 0. --listFiles on each package's test program lists its new pin once.
  • Two ablations, at 8dd4543cc (objectstack scripts/ablation-replace.mjs, wrap mode, each run through the pin on the forks pool with the driver's flag; no dist involved, the pins import ../renderer and ../FormulaFunctions relatively):
    • the extent's min end printed with toISOString() again, the parse left on the shared step (anchor minDate: toDateInputValue(new Date(minTimestamp)),, 1 to 0, blob e0bc666ef587 to 3265f47d94fa). Predicted and measured: exactly the two Shanghai rows that compute the extent red (the producer and the computed day axis), Tests 2 failed | 16 passed (18); every LA row green. So the east rows are the ones that catch a parse-only repair.
    • months moved with UTC setters but without the clamp (anchor addMonthsUtc(date, amount);, 1 to 0, blob 1695444571e2 to a50648d3900b). Predicted and measured: exactly the month-clamp and instant-clamp rows red in each of the three zones, Tests 6 failed | 27 passed (33).
    • Both restored by the tool: blob equal to HEAD, git diff HEAD empty.
  • Schema catalog render, suite zone: pnpm exec vitest run examples/schema-catalog/test/catalog-gallery-render.test.tsx examples/schema-catalog/test/smoke.test.tsx: Test Files 2 passed (2), Tests 1020 passed (1020).
  • Every scripts/__tests__ gate, at 8dd4543cc: pnpm exec vitest run scripts/__tests__/ --maxWorkers=2: Test Files 177 passed | 2 skipped (179), Tests 5334 passed | 2 skipped (5336), VERDICT command-exit 0.
  • Gates at 8dd4543cc, each exit 0: check-changeset-presence ("4 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)"), changeset:check (fixed group, no major), check-changeset-overwrite, check:pending-changeset-literals, check:changeset-claims ("No pending changeset names a file this change touches"), check:control-bytes, check:new-line-citations (VERDICT new-cross-file-line-citations: 0 new citation(s)), check:test-path-roots. A control-byte self-scan over the 7 changed files: no hits.
  • ESLint, narrowed to the 5 changed code files (root eslint.config.js, --no-inline-config --format json, 5 results): 0 errors. FormulaFunctions.ts 26 warnings and renderer.tsx 14, each equal to its base blob via --stdin; the two pins and the driver 0. The config sets no parserOptions.project or projectService, so the rules are not type-aware and this diff cannot move an untouched file's verdict. The repo-wide lint is CI's.
  • Server parity was read with @objectstack/formula 17.4.0 as installed here, above; the rule was read in objectstack packages/formula/src/stdlib.ts (addMonthsUtc, addDaysUtc) at objectstack origin/main 50e273fd.

Acceptance notes

  • Decisions taken here, for the contract reviewer.
    • The gantt gate now refuses a day its month does not have (2026-02-30), because the gate must read what the extent and the headers read: the shared step refuses it (objectui#10026), and a gate that let it through would hand the reduce an Invalid Date. The message is the existing "not a valid date" key.
    • An instant's gantt extent is the VIEWER's day of that instant, where it was its UTC day, so its bar sits at its local hour on an axis of the viewer's days, the day its tooltip names.
    • An instant in DATEADD / DATEDIFF / DATEFORMAT is unchanged from the base (contract review 5869002399, item 1). Only an argument that is a calendar day, a date-only value naming a day its month has, takes the UTC calendar. An instant is still moved and read with local setters and getters in the zone the formula runs in, so DATEFORMAT prints its clock in the viewer's zone: DATEFORMAT('2026-09-01T03:00:00.000Z', 'HH:mm') is 20:00 in Los Angeles, 11:00 in Shanghai and 03:00 in UTC, as on the base. DATEADD of an instant by a day follows the viewer's calendar across a DST change, and a month from January 31st on an instant still overflows into March. Triage 5861646819 says a datetime keeps its instant, and ruling 5864664470's parameter names only the date-only month overflow; putting instants on the UTC calendar, or clamping their months, would be a separate decision. So DATEFORMAT(NOW(), 'YYYY-MM-DD') still names the viewer's day, which can differ from TODAY()'s UTC day for part of every day, as on the base.
    • A date-only string naming a day its month does not have is not refused by the formula functions: the engine rolls it forward as the server's parse does, and DATEADD hands it back as an instant, as before. A date-time written without an offset is still read in the viewer's zone by the engine's parse.
  • DST. Two local midnights across a DST change are 23 or 25 hours apart, so on the gantt a bar's edge can sit up to one hour's share of the axis off its column's edge. The pins stay clear of DST dates on the gantt.
  • Not changed, pre-existing and zone-independent (noted, not filed): the gantt's percentage scale spans min to max midnight while the header row draws one equal-width column per bucket from min to max inclusive, so on a day axis a bar is placed on a scale one column narrower than the header row (the computed-axis pin's second bar starts at 50% under three columns). Measured: the style percentages; the column widths are inferred from flex-1, not measured.
  • Slice 1's owed items stay for a slice 4 (measured, not small):
    • The timeZone "persisted data stays real instants" prose sits in @object-ui/types (GanttConfig.timeZone's JSDoc), in plugin-gantt (GanttViewProps.timeZone's JSDoc, "Writes still persist real instants", and makeTzShift's JSDoc), and in the spec's view.zod.ts describe, which the @object-ui/types zod mirror reuses and which only objectstack can edit. That is two more packages here and a cross-repo half.
    • The calendar's DST-day move needs a drag measured across the November change before any edit.
    • The calendar no-schema fallback, the endDateField write and the gantt no-schema path off UTC each need a new zone pin.
  • Bundle. No new cross-package import: the renderer's import line is unchanged, and FormulaFunctions imports utils/date-display inside core, a module core's barrel already re-exports. Bundle Analysis is left to CI.
  • Governed surface. check-governed-queue-guard --test over the 7 paths: NOT GOVERNED (control: AGENTS.md exit 3).

Serial

  • Branch base 3b469c8ea. git merge-tree --write-tree of head 8dd4543cc against main 06a96e948: clean (exit 0), and nothing main gained since the base touches these 7 paths.

Implemented by the dev agent for domain:ui seat #1, session https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk.


Generated by Claude Code

Body amended by the domain:ui seat #1 after contract review 5869002399 and the repair commit 5b48d0292; the repair text is the dev's.

…y on that day, and the formula date functions compute on the UTC calendar (objectui#10866, slice 3)

The timeline renderer's gantt variant reads every date string through core
`toDisplayDate` (one helper, `readGanttDate`): the axis headers, the computed
extent (printed as the viewer's day), the bar geometry, the validity gate and
the min-over-max guard. The empty plan's axis is the viewer's today.

`DATEADD` / `DATEDIFF` / `DATEFORMAT` move and read values with UTC setters
and getters, clamp months like the server's `addMonths`, and hand a real
date-only value back as `YYYY-MM-DD`. `TODAY()` is unchanged.

Two zone pins registered in the date-only zone-pin driver.

Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
Co-authored-by: Claude <noreply@anthropic.com>
…he gate lets a rolled day through (objectui#10866, slice 3)

Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
Co-authored-by: Claude <noreply@anthropic.com>
…ing it to any (objectui#10866, slice 3)

Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3103.0 KB 3104.5 KB
Main entry chunk (gzip) 149.4 KB 350 KB
Entry file index-C3DMTjvF.js —
Status PASS —

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


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.58KB 6.17KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.70KB 2.23KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 558.56KB 133.87KB
core (index.js) 9.93KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.61KB 63.16KB
fields (index.js) 261.01KB 66.28KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.32KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.96KB 14.83KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 197.72KB 46.92KB
plugin-dashboard (index.js) 137.82KB 36.70KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 233.51KB 61.80KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 161.21KB 41.41KB
plugin-gantt (index.js) 170.35KB 42.19KB
plugin-grid (index.js) 228.33KB 62.59KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.86KB 28.64KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.17KB 12.20KB
plugin-timeline (index.js) 31.07KB 9.15KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 89.15KB 22.35KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.16KB 39.05KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 7.50KB 3.05KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.16KB 2.71KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.22KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.83KB 1.49KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.27KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8dd4543ccd4b6cd160b60623293346055adf4d41

Isolated adversarial review of PR objectui#10957 (card objectui#10866, slice 3), written 2026-09-28T11:31Z. Read: the card's body and all 16 comments (triage 5861646819, the slice-1 owed list 5862887370, the slice-2 ACCEPT 5865096208 and release 5866905836, the slice-3 claim 5867569822, the dev report 5868685874 as a claim, not evidence), ruling 5864664470 on objectui#10903, PR objectui#10915, PR objectui#10901, objectstack packages/formula/src/stdlib.ts, template-engine.ts and ADR-0053 at objectstack origin/main dbddf02c1, the diff origin/main... head (7 files, +709 / -49), and the head's 43 check-runs. Probes ran in two detached worktrees under the review's scratch directory (head 8dd4543cc, base 3b469c8ea, pnpm install --frozen-lockfile --offline), with @objectstack/formula 17.4.0 as installed there for the server side; both were removed afterwards.

① Derived judgments

1. Instants in FormulaFunctions — the decisive question. Answer: (b), an unruled, user-visible behaviour change.

Measured, DATEFORMAT('2026-09-01T03:00:00.000Z', 'HH:mm'):

UTC America/Los_Angeles Asia/Shanghai
base 3b469c8ea (local getters) 03:00 20:00 11:00
head 8dd4543cc (UTC getters) 03:00 03:00 03:00
server, template datetime face of the same instant, no reference zone (formatValue, @objectstack/formula 17.4.0) 8/31/26, 8:00 PM 9/1/26, 11:00 AM
server, the same with timeZone: 'America/New_York' 8/31/26, 11:00 PM 8/31/26, 11:00 PM

DATEADD('2026-10-31T12:00:00.000Z', 1, 'day') (Los Angeles leaves DST on November 1st): base LA 2026-11-01T13:00:00.000Z (one viewer calendar day, 25 h), head 2026-11-01T12:00:00.000Z in every zone (24 h), server CEL addDays 2026-11-01T12:00:00.000Z. DATEFORMAT(NOW(), 'YYYY-MM-DD') at 2026-09-28T05:30:00.000Z in LA: base 2026-09-27 (the viewer's day) against TODAY() 2026-09-28; head 2026-09-28, equal to TODAY().

Why (b), read against the four texts the brief names:

  • Triage 5861646819: "a datetime field keeps its instant", and site-5's note asks for the server's TODAY() / DATEADD "for a date-only context". The instant path is outside that mandate.
  • Ruling 5864664470's last parameter reads, in full: "Under every letter the D1 arithmetic slice for DATEADD / DATEDIFF / DATEFORMAT proceeds on objectui#10866, fixing the measured month overflow (client 2026-01-31 + 1 month = 2026-03-03; server 2026-02-28)". ADR-0053's D1 is the calendar-day decision (today() and friends return UTC midnight of the reference-tz calendar day; a date is never converted to an instant). It says nothing about how an instant's wall clock is printed.
  • Ruling A settles one "today" per execution context for TODAY() on the compute-tz axis. ADR-0053 keeps a second axis, render-tz: "show this UTC instant in the viewer's zone — datetime display", source the viewer, applied at the presentation boundary (item 7: "datetime rendering uses the reference timezone at the presentation boundary"). DATEFORMAT of an instant with a clock face is that presentation. The base's local getters sat on that axis; a fixed UTC wall clock is on neither axis.
  • Server parity does not carry it either. The server's stdlib has no formatting function at all, and the server's only instant-formatting path (the template datetime filter) renders in the reference zone when one is supplied and otherwise in the runtime zone, which is exactly what the base printed; its iso face alone is UTC. So "one answer per execution context" is achieved by the server through a reference zone, never by printing UTC. For DATEADD on an instant the server's addDays is 24 h, so that half is server-equal, but it is still not D1, not date-only, and not measured or ruled; ADR-0053 itself lists addDaysUtc's "keeps wall-clock time" under "the time functions disagree with each other".

The dev report and PR body ground the choice in "the ruling's parameter reads 'UTC setters and getters' with no split by shape". The ruling contains no such phrase (grep of comment 5864664470 for "setters": none); the phrase is in the decision card's body and the seat's own release note and claim, and there it follows "can follow ADR-0053 D1 as written" and "an instant in giving an instant out". Reading it as a licence to reformat instants in UTC is the dev's inference, not a ruling.

In-repo reach: no formula, example or test formats an instant's time of day (HH, mm); the census over origin/main finds DATEFORMAT / DATEADD / DATEDIFF only in packages/core/src/evaluator/__tests__/FormulaFunctions.test.ts (an instant with YYYY-MM-DD in the suite's UTC zone, unchanged) and the published guide skills/objectui/guides/schema-expressions.md (DATEFORMAT(createdAt, 'YYYY-MM-DD'), DATEFORMAT(data.timestamp, 'YYYY-MM-DD'), both instants, presented as "Preferred — formatted, and stable across locales"): their DAY changes for every viewer off UTC for part of every day (LA at 20:00 on August 31st: base 2026-08-31, head 2026-09-01).

Minimal repair: in packages/core/src/evaluator/FormulaFunctions.ts, take the UTC path only when readFormulaDate's day is true; an instant stays on the base path (local setters and getters, instant out). The month clamp on a date-only value is the ruled overflow fix and stays; a clamp on an instant is a separate decision and, if wanted, needs its own statement. The pin's instant rows (DATEFORMAT(INSTANT, 'YYYY-MM-DD HH:mm') as 2026-09-01 03:00, the 24-hour DATEADD row, the instant clamp row, and formattedNow equal to today), the DATEADD docblock's two instant bullets, the changeset's three instant sentences and the PR body's decision paragraph follow the code.

2. Month overflow and clamp. The head's private addMonthsUtc gives the server's addMonths answer in every probed case, in all three zones: 2026-01-31 + 1 2026-02-28; 2024-01-31 + 1 2024-02-29; 2024-02-29 + 1 year 2025-02-28 and + 4 years 2028-02-29; 2026-03-31 - 1 2026-02-28; 2026-03-31 - 13 2025-02-28; 2026-05-31 - 3 2026-02-28; 2025-12-31 + 2 2026-02-28; 2026-01-31 - 1 year 2025-01-31; the instant 2026-01-31T12:00:00.000Z + 1 2026-02-28T12:00:00.000Z. A year is twelve months on the same rule. DATEDIFF in days for date-only pairs across both DST changes (2026-10-31 to 2026-11-02, 2026-03-07 to 2026-03-09) is 2 in every zone on base and head, equal to the server's daysBetween; the month and year units now count between the stored days (LA base read 0 for 2026-08-31 to 2026-09-01).

3. Date-only in, YYYY-MM-DD out. True and consistent within the three functions: a real day moved by day, month or year comes back as YYYY-MM-DD in every zone; 2026-02-30 is not a real day, so DATEADD('2026-02-30', 1, 'day') gives 2026-03-03T00:00:00.000Z in every zone, which is the server's addDays answer, and DATEFORMAT('2026-02-30', 'YYYY-MM-DD') gives 2026-03-02 in every zone (base LA gave 2026-03-01). Note the declared split inside this one PR: the gantt refuses 2026-02-30 while the formula functions roll it; both are stated in the PR body and changeset.

4. The gantt variant. All six stops read through readGanttDate; no other new Date( read remains on the gantt branch. Measured with the pin and a direct probe of generateTimeScaleHeaders:

  • A date-only row draws on its stored day in both zones: computed day axis Oct 5, Oct 6, Oct 7 in LA and Shanghai (base LA Oct 4, Oct 5, Oct 6); the producer gantt-style-timeline.json reads Jan 2024 to Apr 2024 in both (base LA Dec 2023, Jan 2024, Mar 2024, Apr 2024).
  • An instant keeps its instant: the pinned instant bar sits at 43.75% in LA (21:00 on the 5th) and 75% in Shanghai (12:00 on the 6th); generateTimeScaleHeaders on the hour scale over 2026-10-06T04:00:00.000Z to 06:00Z and on the day scale over instant ends is byte-identical on head and base in both zones.
  • generateTimeScaleHeaders is exported (export * from './renderer'). In-repo callers are six test files in the suite's UTC zone, unchanged (CI green). For an external caller, a date-only end in a non-UTC zone now starts on its day (the fix, declared in the changeset), an instant end is unchanged, and an impossible day such as 2026-02-30 now yields [] where base gave ['Mar 2026'], which is the helper's existing answer for an unparsable end (pin 7) and is implied, not spelled, by "reads a date-only minDate / maxDate the same way".
  • The gate's refusal of 2026-02-30: a stored document with such a value showed a bar on March 2nd on base (the base run of the pin saw left: '0%', width: '100%') and now shows the existing malformedDate refusal. It is a behaviour change a stored document can hit, declared in the changeset and PR body in plain words, and it is the objectui#10026 convention the tooltip has followed since slice 2.
  • The empty plan's today is the viewer's day (Oct 5 in LA, Oct 6 in Shanghai at 2026-10-05T20:00:00.000Z), the same local day as the object timeline's "Today" bucket from slice 2, and the remainder the slice-2 ACCEPT named. Consistent.

5. Pins. Both new files sit in the driver's PINS. Run against base sources in the forks child: Tests 27 failed | 24 passed (51), every red one of the PR body's rows (month overflow 2026-03-03T00:00:00.000Z against 2026-02-28, LA 2026-08-31 against 2026-09-01, LA DATEDIFF 0, LA producer Dec 2023, LA day axis Oct 4, the Shanghai instant bar 58.333…%, the empty plan Oct 4 / Oct 5, the rolled day drawn); every rig, fixture-validity and control case green. Head in the forks child: 51 passed (51); the driver on head: 20 passed (20), exit 0. Ablations on head: the extent's min end printed with toISOString() reddens exactly the two Shanghai extent rows (2 failed | 16 passed, the Shanghai day axis reading Oct 4, Oct 5, Oct 6, Oct 7); months moved without the clamp reddens exactly the six clamp rows (6 failed | 27 passed). Both restored, git diff empty. The pins are sound for the date-only rows; their instant rows pin the item-1 defect and must move with it.

6. CI. Head 8dd4543cc: 43 check-runs, 40 success, 3 skipped (Test (coverage), dependabot, the coverage shard matrix), 0 failed. Bundle Analysis and Spec Main Shape Gate are both success.

7. Commits. Three commits on the branch; git log --format=%B origin/main.. carries Claude-Session and Co-authored-by: Claude trailers only. No model name.

② Semver level

patch for @object-ui/plugin-timeline is right: reads of stored days stop shifting, and the one new refusal is of an invalid value, declared. patch for @object-ui/core is right only once item 1's repair lands. As the diff stands, DATEFORMAT of an instant changes its printed clock for every viewer off UTC, a change to a published function that no defect names and no ruling covers; that does not ship under a patch line that says "as the server does".

Changeset and docs sentences: the plugin-timeline bullet is true in every clause (measured above). The plugin-timeline.mdx paragraph is true. The core bullet is true as a description of the head, including "An instant is formatted with UTC getters, so DATEFORMAT(NOW(), 'YYYY-MM-DD') names the same day as TODAY(), where it named the viewer's local day"; the defect is the behaviour that sentence describes, and the sentence goes with the repair. The changeset's opening line, "do their day arithmetic on the UTC calendar, as the server does", is true of addDays / addMonths and says nothing of formatting.

③ Boundary flags

  • Clause-②: no is honest in its own terms: no schema, published type, key or accept set moves. The instant change is a behaviour change, not a widening.
  • The dev's out-of-scope findings are true. (a) The "persisted data stays real instants" prose sits in objectstack packages/spec/src/ui/view.zod.ts (the GanttConfig timeZone describe, a cross-repo half), objectui packages/types/src/objectql.ts, packages/plugin-gantt/src/GanttView.tsx ("still persist real instants") and tzShift.ts; the calendar DST-day move and the uncovered no-schema paths are untouched here. Leaving them to slice 4 is consistent with the claim's "ride only if measured small". (b) The day-scale column misalignment is real, pre-existing and zone-independent: the computed pin draws three header columns over Oct 5 to Oct 7 while the bars span two days (0% / 50% and 50% / 50%), the same figures on base in the suite zone.
  • PR body: no file:line address, no angle bracket, no model name. One false sentence: "The ruling's parameter reads 'UTC setters and getters' with no split by shape". The ruling comment 5864664470 does not contain that phrase; it appears in the decision card's body and in the seat's release note and claim, beside "an instant in giving an instant out". The sentence must go with the repair.
  • Part of #10866 is correct: slice 1's owed items remain, and the card stays the family's carrier.

Implemented-by: claude/issue-10866-gantt-axis-d1-arithmetic
Reviewed-by: session_01DuWo5bdP9SdVebamn99GGk

VERDICT: FAIL


Generated by Claude Code

…endar day only; an instant keeps its base path (objectui#10866, slice 3, contract review 5869002399)

`DATEADD` / `DATEDIFF` / `DATEFORMAT` move and read an argument on the UTC
calendar only when it is a real date-only day. An instant (and a date-only
string naming a day its month does not have) is back on the base path:
local setters and getters, instant out, no month clamp. The pin's instant
rows follow: per-zone controls now hold that an instant formats, and counts
months, in the viewer's zone. The DATEADD docblock and the changeset follow
the code.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Sep 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3103.2 KB 3104.5 KB
Main entry chunk (gzip) 149.4 KB 350 KB
Entry file index-DGkfTk7p.js —
Status PASS —

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


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.58KB 6.17KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.70KB 2.23KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 558.56KB 133.87KB
core (index.js) 9.93KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.61KB 63.16KB
fields (index.js) 261.01KB 66.28KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.32KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.96KB 14.83KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.08KB 46.94KB
plugin-dashboard (index.js) 137.82KB 36.70KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 233.51KB 61.80KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 161.21KB 41.41KB
plugin-gantt (index.js) 170.35KB 42.19KB
plugin-grid (index.js) 228.33KB 62.59KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.86KB 28.64KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.17KB 12.20KB
plugin-timeline (index.js) 31.07KB 9.15KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 89.15KB 22.35KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.16KB 39.05KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 7.50KB 3.05KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.16KB 2.71KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.22KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.83KB 1.49KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.27KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@objectstack-fleet objectstack-fleet Bot changed the title fix(plugin-timeline,core): the gantt axis draws a stored date-only day on that day in every zone, and the formula date functions compute on the UTC calendar (objectui#10866, slice 3) fix(plugin-timeline,core): the gantt axis draws a stored date-only day on that day in every zone, and the formula date functions do a calendar day's arithmetic on the UTC calendar (objectui#10866, slice 3) Sep 28, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5b48d02921f4478fdd16760a9f7d63dde3d6641a

Delta record over 5869002399 (FAIL at 8dd4543cc), same isolation and reading rules, written 2026-09-28T12:30Z. The branch moved by one commit, 5b48d0292, which touches packages/core/src/evaluator/FormulaFunctions.ts, the formula pin and the changeset's core bullet; the PR body and title were patched by the seat. Probes ran again in two detached worktrees under the review's scratch directory (head 5b48d02, base 3b469c8ea, pnpm install --frozen-lockfile --offline), removed afterwards.

① Derived judgments

1. Instants are back on the base path — verified, and item 1 of 5869002399 is closed.

The diff: readFormulaDate is unchanged; a new addToDay takes the UTC path for DATEADD only when day is true (UTC setters, addMonthsUtc, a year as twelve months, YYYY-MM-DD out, hours and minutes an instant out); the non-day branch of DATEADD is the base code verbatim (setDate, setMonth with no clamp, setFullYear, setHours, setMinutes, toISOString() out, the same unsupported-unit error). A new calendarFields reads a day's fields with UTC getters and anything else's with local getters, and feeds DATEDIFF's month and year units and DATEFORMAT; the day, hour and minute units of DATEDIFF stay millisecond differences.

My own item-1 probe re-run, base 3b469c8ea against head 5b48d02, under UTC, America/Los_Angeles and Asia/Shanghai: every one of 25 rows over instants, epoch milliseconds, a zone-less date-time and an impossible day is byte-equal on head and base in all three zones. Among them: DATEFORMAT('2026-09-01T03:00:00.000Z', 'YYYY-MM-DD HH:mm:ss') is 2026-09-01 03:00:00 / 2026-08-31 20:00:00 / 2026-09-01 11:00:00; DATEADD('2026-10-31T12:00:00.000Z', 1, 'day') is 2026-11-01T12:00:00.000Z / 2026-11-01T13:00:00.000Z / 2026-11-01T12:00:00.000Z; DATEADD('2026-03-07T12:00:00.000Z', 1, 'days') is 12:00Z / 11:00Z / 12:00Z; DATEADD('2026-01-31T12:00:00.000Z', 1, 'month') is 2026-03-03T12:00:00.000Z everywhere; DATEADD('2024-02-29T12:00:00.000Z', 1, 'year') is 2025-03-01T12:00:00.000Z everywhere; DATEFORMAT(NOW(), 'YYYY-MM-DD') at 2026-09-28T05:30:00.000Z is 2026-09-27 in LA against TODAY() 2026-09-28, as on base; the epoch, zone-less, impossible-day (2026-02-30 in DATEADD by day and month and in DATEFORMAT) and two-instant DATEDIFF rows are equal too. The date-only rows still move as ruled: DATEADD('2026-01-31', 1, 'month') 2026-02-28, DATEADD('2024-02-29', 1, 'year') 2025-02-28, DATEADD('2026-03-31', -13, 'months') 2025-02-28, DATEADD('2026-11-01', 1, 'day') 2026-11-02, DATEFORMAT('2026-09-01', 'YYYY-MM-DD') 2026-09-01 in LA (base 2026-08-31), DATEDIFF('2026-08-31', '2026-09-01', 'month') 1 in LA (base 0), DATEDIFF('2026-10-31', '2026-11-02', 'days') 2 everywhere on both.

Pins. The head's formula pin against base sources in the forks child: Tests 15 failed | 24 passed (39), every red a date-only row (the month and year clamp, the YYYY-MM-DD shape and the DST day in all three zones, the stored day's DATEFORMAT in LA and Shanghai, DATEDIFF in LA), and all six instant controls green, as the PR body says. Head, both pins in the forks child: 57 passed (57); the driver on head: 20 passed (20); the existing FormulaFunctions.test.ts: 64 passed (64). The instant controls catch a regression to UTC: reading an instant's hours with getUTCHours reddens exactly the LA and Shanghai format controls (2 failed | 37 passed (39), the dev's figure); putting the whole instant branch of calendarFields on UTC getters (the 8dd4543cc behaviour) reddens three, both format controls and the LA months control (3 failed | 36 passed (39)). Both restored, git diff empty.

Changeset core bullet: each sentence is true against the probe. "A date-only value naming a day its month has is read as UTC midnight of that day, as before, and is now moved and read with UTC setters and getters" — true. "DATEADD hands such a day ... back as YYYY-MM-DD, where it gave an instant string" — true. The clamp examples, the LA DATEFORMAT, DATEDIFF and DST-day sentences — true. "A value with a time part is unchanged: it is still moved and read in the viewer's zone and comes back as an instant, so DATEFORMAT still prints an instant's clock in the viewer's zone. So is a date-only value naming a day its month does not have, which the engine rolls forward as before" — true (25 equal rows). "TODAY() is unchanged" — true. The docblock above DATEADD says the same and is true.

Commit: one commit, trailers Co-Authored-By: Claude and Claude-Session only; no model name on any of the branch's four commits. CI on 5b48d02: 43 check-runs, 40 success, 3 skipped, 0 failed; Bundle Analysis, Spec Main Shape Gate, Type Check, Lint, Build & E2E, Changeset Bump Policy all success.

2. The PR body and title. Diffed against the body I read at 8dd4543cc. The false sentence ("The ruling's parameter reads 'UTC setters and getters' ...") is gone; a grep of the new body for that phrase finds nothing. Every amended sentence checked:

  • DATEADD bullet: addToDay with UTC setters for a day, addMonthsUtc as the server's rule, a year as twelve months, YYYY-MM-DD out, sub-day units an instant out; "anything else ... keeps the base code path verbatim: local setters, setMonth with no clamp, an instant out" — true, read in the diff and measured.
  • DATEDIFF bullet: a day's fields with UTC getters, an instant's with local getters, "so two instants count exactly as on the base" — true (DATEDIFF('2026-08-31T12:00:00.000Z', '2026-09-01T03:00:00.000Z', 'month') 1 / 0 / 1 on both). DATEFORMAT bullet — true.
  • The four table rows now reading "same" for the head — true, each reproduced.
  • The final-head line and the repair-round bullet: its base pin figure 15 failed | 24 passed (39) and its ablation figure 2 failed | 37 passed (39) are reproduced exactly here; its CI figure matches the check-runs; the closure build, type-check, the 4037-test package run and the gate list were not re-run here (gate families are CI's) and are not contradicted by anything measured.
  • The decision bullet: 20:00 / 11:00 / 03:00, the viewer-calendar DST day, the unclamped instant month, the triage and ruling readings, and "DATEFORMAT(NOW(), 'YYYY-MM-DD') still names the viewer's day, which can differ from TODAY()'s UTC day ... as on the base" — every clause true.
  • Title: "do a calendar day's arithmetic on the UTC calendar" — true, and it no longer claims more than the diff does.
  • Nothing else changed in meaning: the body diff is confined to those hunks plus one attribution line naming the seat's amendment; no file:line address, no angle bracket, no model name.

3. Mixed DATEDIFF, one day and one instant. Measured: DATEDIFF('2026-09-01', '2026-09-01T03:00:00.000Z', 'month') is 0 on base everywhere; on head it is 0 in UTC and Shanghai and -1 in LA, the one row involving an instant that moved. That is the ruled date-only fix acting on the day argument, not a change to the instant: on base LA read the day 2026-09-01 as August 31st (the defect), so both halves said August; on head the day is its stored September while the instant is the viewer's August 31st at 20:00. Each argument is read by its own kind, which is the family's convention (toDisplayDate: a day is its day, an instant renders in the viewer's zone), the rule the gantt's min-over-max guard applies to a mixed pinned pair in this same PR, and the rule the slice-2 timeline sort applies. The days unit of the same pair stays a millisecond floor (0 in every zone), a unit-to-unit disagreement of the same class base already had for two instants an hour apart across a month end. Sound; no statement needed. A one-sentence note in the DATEADD docblock naming the mixed pair ("a day's month is its stored month, an instant's is the viewer's") would spare the next reader the probe; recorded as a wish, not a defect.

4. Gantt half, CI and commits are as recorded at 5869002399 (unchanged files; the gantt pin green in the forks child on this head).

② Semver level

patch for @object-ui/core is now right: the only behaviour that moves is a calendar day's (the ruled zone fix and the ruled month overflow), and every instant, epoch, zone-less and impossible-day reading is byte-equal to base in three zones. patch for @object-ui/plugin-timeline stands as before. The changeset's opening line, "do their day arithmetic on the UTC calendar, as the server does", is now exactly what ships.

③ Boundary flags

  • Clause-②: no stays honest: no schema, published type, key or accept set moves, and the one public behaviour change beyond the ruled fix has been removed.
  • The dev's out-of-scope findings and the slice-4 items are unchanged from 5869002399 and remain true.
  • PR body: no file:line address, no angle bracket, no model name; the false attribution is gone.
  • Part of #10866 remains correct: slice 1's owed items stay on the card.

No defect found on 5b48d02.

Implemented-by: claude/issue-10866-gantt-axis-d1-arithmetic
Reviewed-by: session_01DuWo5bdP9SdVebamn99GGk

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 12:31
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit 9e6619f Sep 28, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10866-gantt-axis-d1-arithmetic branch September 28, 2026 12:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation package: core plugin tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants