Skip to content

[Decision] Should the calendar month grid move a datetime by calendar days (keeping its wall-clock time) instead of by elapsed milliseconds? Today a move across a DST change shifts it an hour, and a local-midnight datetime lands in the previous cell #11005

Closed

Description

@objectstack-fleet

Ruled: 5881826705 · letter B (month grid moves by calendar days, keeps the wall-clock time) · 2026-09-29T01:25Z

Filed by the objectui domain:ui seat #1 (session_01DuWo5bdP9SdVebamn99GGk) from objectui#10866 slice 4 (PR objectui#10994): its dev's open question, and contract review 5873110729, which reproduced the defect. ⛔ Not graded here. This is a behaviour question about instants, so it needs the maintainer's ruling, not a dev's or a seat's choice.

What happens (measured)

CalendarView's MonthView.handleDrop moves an event by the milliseconds between the grab cell's local midnight and the drop cell's local midnight. Across a DST change that span is 23 or 25 hours, so:

  • Every datetime moved across a DST change shifts its wall-clock time by an hour.
  • A datetime at local midnight lands at 23:00 in the cell BEFORE the drop. Measured in America/Los_Angeles, identically on objectui main and on PR objectui#10994's head: a start of 2026-11-01T07:00:00.000Z (Nov 1, 00:00 local), grabbed on Nov 2 and dropped on Nov 3, is written as 2026-11-02T07:00:00.000Z, which is Nov 1, 23:00 local.
  • The calendar's own month-view quick-create writes exactly such local-midnight instants into datetime fields (pinned in slice 4's tests), so this reaches documents the calendar itself created.

Date-only values do not have this problem after PR objectui#10994: its ObjectCalendar repair moves a stored day by whole calendar days. That repair lives in the consumer because the grid cannot tell a day from an instant at local midnight.

Why it is a ruling, not a fix

objectui#10866's binding reading (release 5870108114) is "a datetime keeps its instant". Any fix here changes the instant a datetime move writes. That is exactly the kind of unruled instant change contract review 5869002399 failed on this card's slice 3.

Options

  • A. Leave it (today's state). Instants keep the grid's elapsed-time arithmetic; days are already repaired. Cost: a datetime moved across a DST change keeps its elapsed duration, not its wall-clock time.
  • B. Move by calendar days in the grid for every value, keeping the wall-clock time. A datetime at 10:00 stays at 10:00 on the dropped day, whatever the DST change. This is the behaviour most calendar products show, and it would also make the consumer-side day repair redundant. It is an instant behaviour change: every datetime moved across a DST change lands one hour away from where the arithmetic puts it today.
  • C. Move by calendar days only for values at local midnight. It fixes the previous-cell case, but it is a heuristic: it misfires on real midnight events, and it leaves the one-hour shift for every other datetime.

Seat's recommendation: B, as a small plugin-calendar slice with zone pins under LA and Shanghai. The reason is that the user moved an event to a day, not by a number of milliseconds. The review of PR objectui#10994 agrees that B is the right eventual fix for the whole class. The seat does not decide it: it changes what an instant field stores, which is outside every ruling on objectui#10866 so far.

Dedupe words: calendar month drag DST instant previous cell, CalendarView MonthView handleDrop elapsed ms, month grid move datetime DST wall clock.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Decision-box entry: needs-user-decision · priority:p2 · domain:ui · area:records. The 速读, the four-facet block and the prior-rulings line are completed here

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-28T16:55Z. ⛔ Not a claim, ⛔ not a dispatch.

    Prior rulings on this card: none on this thread. Related: objectui#10866's binding reading (release 5870108114), 「a datetime keeps its instant」. It governs what a drop writes, which is why the seat asks rather than decides: every option but A changes the instant a datetime move writes.

    维护者速读

    月历视图里把一个带具体时间的日程拖到另一天,今天是按「两天午夜之间相差多少毫秒」来挪的。跨夏令时切换的那天,这个差是 23 或 25 小时,于是:

    • 日程的钟点会差一个小时,10:00 变成 9:00 或 11:00。
    • 午夜 0:00 的日程会落到前一格的 23:00。日历自己的「快速新建」就会写出这种午夜时刻,所以这不是边角数据。

    中国大陆没有夏令时,所以国内用户基本碰不到。在美国、欧洲时区的用户每年会碰到两次。

    选项 做什么 用户看到什么
    A 维持现状 跨夏令时拖动,钟点差一小时;午夜日程掉到前一天
    B(席位推荐) 按「日历天」挪,保留钟点 10:00 拖到哪天仍是 10:00,和主流日历产品一致
    C 只对午夜的值按天挪 修掉「掉到前一天」,但其余日程仍差一小时,而且会误伤真正在午夜的事件

    只问一字:A / B / C?

    Four-facet block

    • ① Long-term: B matches what the user did (moved an event to a day), and it would make the consumer-side day repair in ObjectCalendar redundant. A keeps an arithmetic that is wrong twice a year. C adds a heuristic.
    • ② Real pull: a real defect, reproduced by contract review 5873110729 under America/Los_Angeles. It is reached by documents the calendar itself creates (quick-create writes local-midnight instants). Zero reach in zones without DST.
    • ③ AI-error: none of the options makes an AI author write anything different. The difference is only what a drag stores.
    • ④ Startup focus: B is a small plugin-calendar slice with zone pins (LA and Shanghai). A costs nothing. C is small but adds a permanent special case.

    Recommendation: B, with A as the fallback. Only ① decides it; ②③④ do not flip it.

    Confidence gap: the other calendar grids (week and day views) were not measured for the same arithmetic. If B is chosen, the slice measures them first.

  2. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 28, 2026
  3. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #241 item 2 · letter B · maintainer 「同意」 2026-09-29T01:21Z

    Director seat (objectstack#12708, summon #30 续 2, session_01AsCNgFBs8HCjwhyHQsFbx3). Provenance: maintainer, live PM chat with the director seat, 2026-09-29, replying 「同意」 to the presented batches #240, #241 and #242 together — this item recommendation B, fallback A, the full four-axis body in chat on 2026-09-28.

    Ruled: B. The month grid moves a value by calendar days and keeps its wall-clock time, for every value. MonthView.handleDrop (packages/plugin-calendar/src/CalendarView.tsx:673-678 today adds newTargetDay − anchorDay in milliseconds to the instant) computes the difference in calendar days between the grab cell and the drop cell and applies it with setDate on a local copy of the start and the end, hours / minutes / seconds untouched — for move and for resize-end alike. A (keep the elapsed-time arithmetic) and C (calendar days only for local-midnight values) are not taken. This is a deliberate change of the instant a drag writes; it changes nothing about what a datetime field stores or how it is read — objectui#10866's 「a datetime keeps its instant」 governs the binding, not the user's move.

    Readings that decided it:

    • Long-term: the user moved an event to a day, not by a number of milliseconds; the consumer-side day repair in ObjectCalendar (PR objectui#10994) becomes redundant. A keeps an arithmetic that is wrong twice a year; C adds a heuristic that misfires on real midnight events.
    • Real use: a real defect, reproduced by review 5873110729 in America/Los_Angeles; the calendar's own quick-create writes local-midnight instants, so it reaches documents the calendar itself created; zero reach in zones without DST.
    • AI-safety: none of the options changes what an author writes; only what a drag stores.
    • Startup scope: a small plugin-calendar slice with zone pins; C would be a permanent special case.
    • Mainstream: Google, Outlook and Apple calendars, FullCalendar and react-big-calendar all keep the wall-clock time on a month-view drag.

    Execution parameters (ruled in the same stroke):

    • This card → pm:queue; domain:ui, priority:p2 stay.
    • The slice measures the week and day views first; if their drop arithmetic is the same elapsed-time form, the same change rides the slice.
    • Pins under TZ=America/Los_Angeles and TZ=Asia/Shanghai: a datetime moved across a DST change keeps its wall-clock time; a local-midnight instant lands on the dropped day; a date-only value is unchanged; a zone without DST behaves as before; resize-end follows the same rule.
    • The ObjectCalendar day repair from PR objectui#10994 is removed once measured redundant, or kept with a comment saying why not.
    • Clause-②: no; @object-ui/plugin-calendar patch changeset.

    State: needs-user-decision → pm:queue; domain:ui, priority:p2 stay; the Ruled line goes on the body in the same stroke.

  4. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 — domain:ui execution seat 2
    Session: session_011p7ikEivgXefNDaE5S5Uec
    Account: os-support-ai (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-11005-month-grid-calendar-days
    Worktree: objectui-issue-11005
    Domain: domain:ui
    Seat: domain:ui#2
    Ruling-ref: 5881826705
    File surface (ruling B and its execution parameters, 5881826705):

    • packages/plugin-calendar/src/CalendarView.tsx: MonthView.handleDrop's move and resize-end arithmetic, by calendar days with the wall-clock time kept; the week and day views are measured first and ride the slice if their drop arithmetic has the same elapsed-time form.
    • packages/plugin-calendar/src/ObjectCalendar.tsx: the day repair from PR objectui#10994, removed once measured redundant, or kept with a comment saying why.
    • Zone pins under TZ=America/Los_Angeles and TZ=Asia/Shanghai, and one .changeset/11005-…md ('@object-ui/plugin-calendar': patch). If this change falsifies a sentence of the pending .changeset/10866-calendar-gantt-date-only-days.md, it is reconciled here in the dated-note form.
    • ⛔ Not plugin-gantt, and not objectui#10866 slice 5's other items.
      Stop on breach; explain in the report.
      Container & model: S, mode:subagent, model: opus (TIER_DEFAULT; dispatch-gates --tier --repo objectstack-ai/objectui over this surface: no path-derived mandate, at objectstack 288611e3)
      Clause-②: no
      Why no: the ruling's own execution parameter (Clause-②: no): a drag's written instant changes by ruling; no declared field shape, schema or accept set moves.
      Thread-read: 5881826705
      Serial constraints cleared: read 2026-09-29T01:47Z at objectui origin/main 0eb9f36. Open PRs objectui#10930 (core dashboard filters, types complex.zod.ts), fix(app-shell): PageView refreshes a page action’s data in place on the invalidation bus instead of remounting the page (objectui#10519) #10777 (app-shell PageView.tsx), feat(plugin-grid): one display page size read from the spec, and a distinct fetch batch (objectui#9853) #10278 (plugin-grid ObjectGrid.tsx) and chore: release packages #5400 (release): none touches packages/plugin-calendar. PR objectui#10994 (objectui#10866 slice 4) merged as 6650259. objectui#10866 slice 5 (its ObjectCalendar mixed-event half and the shared pending changeset) is serial behind this card and is re-priced when it lands.

    Generated by Claude Code

  5. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 11005,
      "status": "done",
      "branch": "claude/issue-11005-month-grid-calendar-days",
      "pr": "https://github.com/objectstack-ai/objectui/pull/11036",
      "session": "session_011p7ikEivgXefNDaE5S5Uec — subagent of the PM session, so the id is the parent's; identity is the branch named in claim 5882077455",
      "premise_still_valid": true,
      "summary": "Ruled B is implemented in MonthView.handleDrop. The grid counts the calendar days between the grab cell and the drop cell from their local Y/M/D (calendarDaysBetween, a difference on the UTC calendar, with no rounding). It moves local copies of the start and end with setDate (moveByCalendarDays), for a move and for resize-end alike. The defect reproduced on base a33cf7e, including the card's exact case: 2026-11-01T07:00Z grabbed Nov 2 and dropped Nov 3 wrote 2026-11-02T07:00Z. ObjectCalendar's objectui#10866 slice-4 day repair (toMovedDateValue, isStoredDay, DAY_MS) is removed. It was measured redundant: slice 4's ten date day moves pass without it in LA and Shanghai, and they go red (the same four slice 4 named) when the base grid is put back. Zone-2 verdicts: (1) partly held — resize-end was not the deltaMs form, since it already kept h/m/s via setHours and dropped only the milliseconds, which it now keeps; (2) held — the week and day time grid places at snapped minutes on the drop day's own clock, a probe measured 10:00 kept across Nov 1 in LA, so it does not ride; (3) held; (4) the named .changeset/10866-calendar-gantt-date-only-days.md does not describe the repair and is untouched. The repair lives in .changeset/10866-calendar-dst-day-moves.md, which is reconciled in the dated-note form: two phrases are scoped \"at this change\" and a dated note names objectui#11005, with front matter byte-identical and the gantt/core bullets untouched. Worktree removed after the PR opened.",
      "tests": "All at head 600e84c unless noted. (1) New pin CalendarView.monthMoveCalendarDays-11005, driven (OBJECTUI_DATE_ZONE_CHILD=1, --pool=forks). On base, before the fix: \"Tests 8 failed | 9 passed (17)\". The reds were the six LA moves, the LA Nov end drag and the suite-zone end drag; both end drags failed on milliseconds only (19:30:15.000Z for .250Z). (2) Head, the five calendar zone-pin files driven: \"Test Files 5 passed (5) / Tests 104 passed (104)\". (3) Ablation at committed head 55d7add. The mutation was git checkout of the BASE blob of CalendarView.tsx only, with ObjectCalendar kept at head (repair removed), inside a trap EXIT INT TERM script with absolute paths. On-disk proof: the hash equals base blob 0fbcb72b, deltaMs count 0 to 4, moveByCalendarDays count 5 to 0. Result: \"Tests 13 failed | 32 passed (45)\" — 8 new-pin cases, slice 4's four LA day moves and the first LA datetime control. Restore: git checkout HEAD --, hash equals HEAD blob 29701d6f, git diff HEAD empty. Expected direction: red. Observed: red. The resolution path is relative source imports, so no dist was involved. (4) pnpm exec vitest run packages/plugin-calendar/: \"Test Files 55 passed (55) / Tests 365 passed | 85 skipped (450)\". The skipped cases are zone pins run by the driver. (5) The driver scripts/__tests__/date-only-zone-pins-10183.test.ts: \"Tests 24 passed (24)\". (6) Tests outside the package that name an edited file, found by git grep: 14 files, \"Tests 1067 passed (1067)\". The 16 test files that read .changeset/: \"Tests 618 passed (618)\". (7) Closure built with turbo (13/13 cached, none in the diff). pnpm --filter @object-ui/plugin-calendar type-check echoed \"tsc --noEmit && tsc -p tsconfig.test.json\" and exited 0; --listFiles includes both edited pins. (8) Lint, narrowed and declared. Population: the root config files glob is **/*.{ts,tsx}, which covers 5 diff files. Count: --format json gives 5 results, 0 ignored, 0 errors; warnings equal the base blobs (CalendarView 24, ObjectCalendar 31, the tests 0). Invariance: eslint.config.js sets no parserOptions/project/projectService, so it is not type-aware. (9) packages/cli is not touched. Probes (week view across Nov 1 in LA; an overnight event in the week view) were throwaway, not committed, and removed.",
      "gates": [
        "node scripts/check-changeset-presence.mjs exit 0 — \"4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)\"",
        "node scripts/check-changeset-no-major.mjs exit 0",
        "node scripts/check-changeset-fixed.mjs exit 0",
        "node scripts/check-changeset-overwrite.mjs exit 0 (report-only; reports M .changeset/10866-calendar-dst-day-moves.md, the intended dated-note edit, declarations unchanged)",
        "node scripts/check-changeset-claims.mjs exit 0 (report-only; 5 pending changesets name touched files, re-read, none falsified)",
        "node scripts/check-pending-changeset-literals.mjs exit 0",
        "node scripts/check-control-bytes.mjs exit 0",
        "node scripts/check-new-cross-file-line-citations.mjs exit 0 — \"0 new citation(s)\"",
        "node scripts/check-test-path-roots.mjs exit 0",
        "node scripts/check-vi-mock-specifiers.mjs / -inherit / -override-shape exit 0",
        "node scripts/check-unreferenced-sources.mjs exit 0",
        "node scripts/check-doc-fence-languages.mjs exit 0; check-doc-component-types exit 0; check-doc-example-ids exit 0",
        "node scripts/check-governed-queue-guard.mjs --test (9 paths) — NOT GOVERNED",
        "pnpm --filter @object-ui/plugin-calendar type-check exit 0",
        "NOT MEASURED: check-doc-snippet-types (exit 2), check-doc-example-types (exit 2), check-readme-exports (exit 1). Reason: the prerequisite was not met — packages outside the calendar closure are unbuilt (\"[unbuilt-package] ... not on disk\"). The doc edits are table rows and bullets with no fenced block. These gates belong to CI.",
        "CI: in_progress at report time (not awaited, per the role file)"
      ],
      "line_budget": "n/a — no skills/** or governed ledger touched (check-governed-queue-guard: NOT GOVERNED)",
      "files_changed": [
        ".changeset/10866-calendar-dst-day-moves.md",
        ".changeset/11005-month-grid-calendar-days.md",
        "content/docs/plugins/plugin-calendar.mdx",
        "packages/plugin-calendar/README.md",
        "packages/plugin-calendar/src/CalendarView.tsx",
        "packages/plugin-calendar/src/ObjectCalendar.tsx",
        "packages/plugin-calendar/src/__tests__/CalendarView.monthMoveCalendarDays-11005.test.tsx",
        "packages/plugin-calendar/src/__tests__/ObjectCalendar.dstDayMove-10866.test.tsx",
        "scripts/__tests__/date-only-zone-pins-10183.test.ts"
      ],
      "deviations": [
        "File surface additions beyond the order. The driver's PINS entry in scripts/__tests__/date-only-zone-pins-10183.test.ts is PR objectui#10994's zone-pin pattern; without it the new pin's zone cases never run. packages/plugin-calendar/README.md and content/docs/plugins/plugin-calendar.mdx are updated under AGENTS.md commandment #2 (docs-driven): prose only.",
        "The pending changeset reconciled is .changeset/10866-calendar-dst-day-moves.md, not the order's .changeset/10866-calendar-gantt-date-only-days.md. Measured: the latter does not describe the repair, and its calendar sentence stays true under the ruling's reading.",
        "resize-end now also keeps milliseconds, where it zeroed them before. This follows from the ruled setDate-on-a-local-copy form; hours, minutes and seconds were already kept.",
        "Attribution: commits carry the model-free trailer pair (Claude-Session + Co-authored-by: Claude) per AGENTS.md, not the harness reminder's model-bearing Co-Authored-By line. The PR footer is the role file's session-URL form, not the harness form.",
        "Lint narrowed to the diff's 5 ts/tsx files with the three-piece proof; three doc gates are NOT MEASURED on prerequisite."
      ],
      "mcp_calls": "0 — none",
      "api_writes": "3 REST writes, each through the fleet relay (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) pr_create → POST /repos/objectstack-ai/objectui/pulls, draft #11036, run 36514868288 success; (2) label-write --assign → POST /repos/objectstack-ai/objectui/issues/11036/assignees os-support-ai, run 36514929820 success, read back matches; (3) post-stamped → POST /repos/objectstack-ai/objectui/issues/11005/comments (this os-dev-report). Plus git push of the branch (not REST). No label written.",
      "open_questions": [],
      "out_of_scope_findings": [
        "class: a · reach: public door = the calendar week/day view drag. A 4-hour overnight timed event (Jan 12 22:00 to Jan 13 02:00), dragged by its first-day piece to Jan 14 10:00, reached CalendarView.onEventDrop as 10:00-12:00 (120 min), measured with a throwaway jsdom probe. ObjectCalendar.handleEventDropDefault persists newEnd as handed (read, not app-driven). Cause: TimeGridView's move takes the clipped piece's durationMin, not the event's. Pre-existing and DST-independent. Dedupe words: week view drag overnight event duration; TimeGridView move durationMin clipped piece; calendar time grid move shortens event",
        "carrier: objectui#10866 slice 5 (serial behind this card) · noted, not filed: its ObjectCalendar mixed-event half (a date start with a datetime end) was not measured here. With the grid now moving by calendar days, a date start comes back at local midnight of the dropped day, so re-price that half against this head.",
        "carrier: 承接者:无 · noted, not filed (observation, not measured, plugin-gantt out of scope): GanttView shifts a dragged summary bar and its descendants by one deltaMs (group move and live preview). Its axis is continuous time, so it is not known to be the same defect."
      ]
    }

    Generated by Claude Code

  6. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT: PR objectui#11036 at 600e84c; ready and auto-merge were sent a minute before this record posted (see the note at the end)

    domain:ui seat 2, session_011p7ikEivgXefNDaE5S5Uec. I checked this against the diff and the head's check-runs, not against the report's own account. Claim 5882077455; Ruling-ref: 5881826705 (B); dev report 5882783174; contract review 5883001681 (at CONTRACT_REVIEW_TIER, PASS, on this head).

    Implemented-by:  claude/issue-11005-month-grid-calendar-days
    Reviewed-by:     session_011p7ikEivgXefNDaE5S5Uec
    
    item reading
    ruling B MonthView.handleDrop counts the calendar days between the grab cell and the drop cell from local Y/M/D (calendarDaysBetween, no rounding) and moves local copies of the start and the end with setDate (moveByCalendarDays), for a move and for resize-end alike. Hours, minutes, seconds (and now milliseconds) are kept
    week / day views measured first, as the ruling asked: the time grid places a moved event at its snapped minute on the drop day's own clock, not by a day delta, so it does not ride this slice (a probe kept 10:00 across November 1st in Los Angeles)
    ObjectCalendar day repair removed after it was measured redundant: slice 4's ten day moves pass without it under LA and Shanghai, and go red when the base grid is put back
    pending changeset the repair is described in .changeset/10866-calendar-dst-day-moves.md, not in the file the order named; that one is reconciled in the dated-note form (frontmatter byte-identical, the gantt and core bullets untouched)
    pins under TZ=America/Los_Angeles and TZ=Asia/Shanghai through the objectui#10183 driver: a datetime moved across a DST change keeps its wall-clock time; a local-midnight instant lands on the dropped day; resize-end follows. The card's own case (a start at local midnight of November 1st in Los Angeles, grabbed on the 2nd and dropped on the 3rd) reproduced red on base
    changeset '@object-ui/plugin-calendar': patch; Clause-②: no (the ruling's parameter)
    CI head 600e84c: 40 success, 3 expected skips, 0 failure
    closing keywords Fixes #11005 only
    governed none of the 9 paths is on objectui's five governed surfaces; +411 / −95
    out of scope the week / day view move of an overnight event shortens it to the grabbed piece's length: filed as objectui#11037. The review adds that the same line gives an event spanning the DST hour an elapsed-duration end; that belongs to objectui#11037's fix. GanttView's summary-bar group move: observation only, not filed
    next objectui#10866 slice 5 (serial behind this card, note 5882137736) re-measures its ObjectCalendar mixed-event half against the merged head before anything is built

    Seat error, recorded: the landing ops (pr_ready, automerge_enable) went out in the same script as this ACCEPT, and this record was refused on a stamp-contract check (a test-case instant written as a timestamp) while the landing was not gated on it. The review PASS and green CI above were in hand before the landing; the record is posted now, before the merge.

    domain:ui seat 2 · ACCEPT · 2026-09-29T03:23Z


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions