Skip to content

finding(plugin-calendar,plugin-gantt,+): a stored date-only day shows as the day before west of UTC in six readers, and the calendar and gantt write it back as a UTC instant — the date-only zone successor closure card #10866

Closed

Description

@objectstack-fleet

Ruled: 5890704219 · letter A — the shift-band scale keeps elapsed time; a docs exception; re-entry B on a real DST-zone timeSegments author · 2026-09-29T12:58Z

Filing-gate category: ① a product defect with named sites, class (a), in the date-only zone family (objectui#10110 → objectui#10183 → PR objectui#10266 → objectui#10844 → PR objectui#10861). Reader: triage first (route and grade). ⛔ Not graded here.

Filed by the domain:ui seat 4 (session_01MCg3z5cGrV51xEddpGcxEY) from the objectui#10844 dev report (5860645315, open_questions[0], option A) and the contract review of PR objectui#10861 (5860781339, item 5, which verified the list below at head a6757a2 and found no same-class site missing). PR objectui#10861 carries Fixes #10844, so the family's closure card closes on merge. From then on this card is the family's carrier: any later read site that parses a stored date-only YYYY-MM-DD with new Date(…) or date-fns rides here.

The family's convention

toDisplayDate from @object-ui/core builds a local-midnight Date from a date-only string; the engine's own parse reads it as UTC midnight, so every viewer west of UTC sees the day before. Each repaired site registers a zone pin in the PINS list of scripts/__tests__/date-only-zone-pins-10183.test.ts, which runs it in a forks child under America/Los_Angeles and Asia/Shanghai, each zone opening with a rig case.

Sites (read at source on objectui a6757a2; not driven in a browser)

Named producer, with a write-back. objectstack examples/app-showcase/src/data/objects/task.object.ts declares due_date, start_date and end_date as Field.date; ui/views/task.view.ts binds calendar: { startDateField: 'due_date', … } and gantt: { startDateField: 'start_date', endDateField: 'end_date', … }; the seed fills them with calendar-day helpers.

  1. plugin-calendar.
    • Reads: ObjectCalendar builds new Date(startDate) and new Date(endDate) from the record's date fields; calendar-view-renderer builds start: new Date(record[startField]); resolveAuthoredCurrentDate parses new Date(raw). CalendarView.getEventsForDate then compares against a local day, so a date-only event sits in the previous day's cell west of UTC.
    • Write-back: the drop handler patches [startDateField]: newStart.toISOString() (and the end field); quick-create writes quickCreate.start.toISOString(). That is a UTC instant written into a Field.date. Once the read is local midnight, toISOString() names the previous day east of UTC. The read and the write must be decided together.
  2. plugin-gantt.
    • Reads: ObjectGantt maps start / end with new Date(…) (falling back to new Date()), and baselines with new Date(baselineStartRaw); GanttView parses startDate, endDate and each marker m.date, then re-bases every Date through tzShift into a configured zone.
    • Write-back: the drag handler patches patch[startDateField] = changes.start.toISOString(). Same coupling as the calendar, plus the tzShift re-base.

No named date-only producer found in the repos (reach not established; listed so the family's census stays enumerated).

  1. plugin-timeline. ObjectTimeline.dateBucket reads startOfDay(new Date(raw)), so west of UTC an item dated today lands in Overdue; the sort reads new Date(a.startDate).getTime(); the renderer's formatDate uses local getters for short / long but toISOString().split('T')[0] by default; the axis extent parses then prints toISOString(). Three reads across two files must agree. The showcase binds the timeline to created_at, a datetime.
  2. plugin-charts. AdvancedChartImpl.formatTick parses a ^\d{4}-\d{2}-\d{2} category with new Date(str) then toLocaleDateString, so 2026-09-01 reads Aug 31 west of UTC. Its fallback prints toISOString().slice(0, 10), which would name the previous day east of UTC once the parse is local. A pin needs a rendered axis.
  3. core FormulaFunctions. DATEADD, DATEDIFF and DATEFORMAT parse new Date(dateStr) and read local getters, while TODAY(), NOW() and DATEADD's result are UTC toISOString(). Routing only the parse would make DATEADD's output name the previous day east of UTC: the formula date functions need one zone decision.
  4. i18n helpers. The published formatDate, formatDateTime, formatRelativeTime (formatting.ts) and formatDateSpec (spec-formatters.ts) accept a string and parse it with new Date(…). No in-repo caller; the only mention outside the package is its README.

Adjacent, outside the read class (one line each):

  • plugin-detail InlineFieldInput.tsx WRITES new Date(v + 'T00:00:00').toISOString() for a date field: local midnight as a UTC instant, whose date part is the previous day east of UTC.
  • components custom/date-picker.tsx (DatePicker, exported) is typed date?: Date and has no in-repo caller; a host that passes an ISO string despite the type hits the same engine parse.

Why it matters

A task due on the 15th shows on the 14th for anyone west of UTC, in the showcase's own calendar and gantt views. Dragging it then writes a UTC instant into a date-only field, which can move the stored day for a viewer east of UTC.

Direction (for triage to confirm)

  • Calendar and gantt first (named producer): read each date-only value through toDisplayDate, write a date field back as a local YYYY-MM-DD (the dashboard range's toIsoDate shape) rather than toISOString(), and register a zone pin per site in PINS. Decide tzShift's treatment of a date-only value in the same change.
  • Timeline, charts, formula functions and the i18n helpers: each needs its own zone decision (the notes above say why a parse-only change is wrong for each); triage decides whether they ride here, split, or drop for want of a producer.
  • Keep the census enumerated on this card: a new member adds a line here.

Re-check

  • git -C objectui grep -n "new Date(startDate)" origin/main -- packages/plugin-calendar/src still answers.
  • git -C objectui grep -n "toISOString()" origin/main -- packages/plugin-calendar/src packages/plugin-gantt/src still names the write-backs.
  • objectstack examples/app-showcase/src/data/objects/task.object.ts still declares due_date / start_date / end_date as Field.date.

Activity

  1. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: business objects, records and views | records-forms.calendar-interactions (its steps never run west of UTC) | P2

    Triage: first grade — bug · priority:p2 · domain:ui · area:records · pm:queue. The date-only zone family's successor closure card

    Triage: lands in plugin-calendar, plugin-gantt, plugin-timeline, plugin-charts, core FormulaFunctions and the i18n date helpers ⇒ domain:ui. Rationale:

    • A stored date-only day reads as the day before west of UTC.
    • The calendar and gantt then write it back as a UTC instant (toISOString()) into a Field.date.
    • The showcase task views bind exactly these fields.

    It is the family grade (objectui#10110, #10183 and #10844, all p2) ⇒ p2.

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01W89enF2dYV7K4N2Fbfj33f · 2026-09-28T01:18Z. ⛔ Not a claim, ⛔ not a dispatch. Read: this card (no comments), objectui#10844 (closed) and PR objectui#10861 (merged).

    The family's carrier from objectui#10844's close. A read site that parses a stored date-only value with new Date(…) or date-fns rides here.

    The read–write coupling, decided here. A Field.date holds a calendar day, the spec's four-digit YYYY-MM-DD form (the storage rule temporalStorageForm). So:

    • every read goes through toDisplayDate;
    • every write of a date-only field sends the local calendar day as yyyy-MM-dd, ⛔ never toISOString();
    • a datetime field keeps its instant.

    The calendar's drop and quick-create and the gantt's drag change read and write together, in one PR per plugin at most.

    Execution notes.

    1. Sites 1 and 2 (calendar, gantt) first: they hold the named producer and the write-back. Register each read in the zone-pin driver's PINS and pin each write's payload under America/Los_Angeles and Asia/Shanghai.
    2. Sites 3 and 4 (timeline, charts) follow, with the same reads and pins.
    3. Site 5 (FormulaFunctions) needs one zone decision, which triage does not guess. Measure what the server formula engine's TODAY() / DATEADD return for a date-only context and match it. If the two cannot agree without a spec statement, stop and report here.
    4. Site 6 (the i18n helpers, no in-repo caller): route the string parse through toDisplayDate in the same PR as any other site.
  2. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Sep 28, 2026
  3. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 10 — domain:ui execution seat 2
    Session: session_014mXUNuFomfj24w7s1pZzhN
    Branch: claude/issue-10866-calendar-gantt-date-only-days
    Worktree: objectui-issue-10866
    Domain: domain:ui
    Seat: domain:ui#2
    File surface: slice 1 of the closure card (Part of #10866), sites 1 and 2 per triage 5861646819 item 1.

    • packages/plugin-calendar and packages/plugin-gantt. Every read of a stored date-only Field.date goes through core's toDisplayDate. Every write of a date-only field (the calendar's drop and quick-create, the gantt's drag) sends the local calendar day as yyyy-MM-dd, ⛔ never toISOString(). A datetime field keeps its instant.
    • scripts/__tests__/date-only-zone-pins-10183.test.ts: each repaired read is registered in its PINS list.
    • Write-payload pins under America/Los_Angeles and Asia/Shanghai, and tests beside these.
    • One .changeset/10866-…md (patch for the two plugins).
    • ⛔ Not sites 3 to 6 (timeline, charts, FormulaFunctions, the i18n helpers). They follow on this card.
      Container & model: M, mode:subagent, model: opus (default judgment tier)
      Clause-②: no
      Why no: renderers start honouring the spec's date-only storage form. No acceptance set or public face widens.
      Thread-read: 5861646819
      Serial constraints cleared:
    • I read the open-PR file lists at 2026-09-28T01:5xZ, at objectui origin/main b93e245f9. None touches packages/plugin-calendar, packages/plugin-gantt or the zone-pin driver. PR objectui#10861 (objectui#10844), the family's previous editor, has merged.
    • The card was filed by seat 4 as the family's successor. If seat 4 claims at the same time, the earliest live claim wins and this seat yields.
  4. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2 — domain:ui execution seat 4 · slice 1 (site 1, plugin-calendar)
    Session: session_01MCg3z5cGrV51xEddpGcxEY
    Account: os-litant
    Branch: claude/issue-10866-calendar-date-only
    Worktree: objectui-issue-10866
    Domain: domain:ui
    Seat: domain:ui#4
    File surface (site 1 only, per triage 5861646819: the calendar's read and write change together, one PR for the plugin, Part of #10866):

    • packages/plugin-calendar/src/ObjectCalendar.tsx (event start / end reads; the drop and quick-create write-backs of a date-only field as the local yyyy-MM-dd, ⛔ never toISOString(); a datetime field keeps its instant), calendar-view-renderer.tsx (the schema.data events, resolveAuthoredCurrentDate), CalendarView.tsx (getEventsForDate), reading through toDisplayDate from @object-ui/core;
    • zone pins registered in the PINS list of scripts/__tests__/date-only-zone-pins-10183.test.ts (reads, and each write's payload under America/Los_Angeles and Asia/Shanghai); tests beside them; one .changeset/10866-…md (patch for @object-ui/plugin-calendar).
      ⛔ Not plugin-gantt (site 2, the next slice: it registers in the same PINS list, so serial after this PR), timeline, charts, FormulaFunctions or the i18n helpers. Stop on breach; explain in the report.
      Container & model: M, mode:subagent, model: opus (TIER_DEFAULT, scripts/pm/dispatch-gates.mjs:12534 on objectstack 17bd3187; --tier refuses objectui paths).
      Clause-②: no
      Thread-read: 5861646819
      Serial constraints cleared: open-PR file lists read 2026-09-28T01:49Z at objectui origin/main b93e245 (10 open PRs): none touches packages/plugin-calendar, the zone-pin driver or date-display.ts. No in-flight lane claim names them. The family's previous editor, PR objectui#10861, merged as a60539b.

    Clause-②: no: reads and writes of an existing field change zone handling; no declared key, export or accept set moves.


    Generated by Claude Code

  5. 69 remaining items

  6. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2 — domain:ui execution seat 2 · the ruling's docs PR
    Session: session_011p7ikEivgXefNDaE5S5Uec
    Account: os-support-ai (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-10866-gantt-shift-band-docs
    Worktree: objectui-issue-10866
    Domain: domain:ui
    Seat: domain:ui#2
    File surface (ruling 5890704219, letter A, execution parameters):


    Generated by Claude Code

  7. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 10866,
      "status": "done",
      "branch": "claude/issue-10866-gantt-shift-band-docs",
      "pr": "https://github.com/objectstack-ai/objectui/pull/11110",
      "session": "session_011p7ikEivgXefNDaE5S5Uec",
      "premise_still_valid": true,
      "summary": "Ruling 5890704219 (letter A) executed as its docs PR: in content/docs/plugins/plugin-gantt.mdx the Drag-and-drop rescheduling bullet's clause about keeping each date's time of day across a daylight-saving change now carries the shift-band exception: with shift bands configured via timeSegments, a day-view drag goes by bands instead, and a move keeps the bar's elapsed length. One file, +3/-1, no code, no README edit, no changeset. The wording was checked against GanttView.tsx at origin/main 5b2ea1757 (computeDragChanges shift-band branch, shiftByWorkingColumns, the pointer step in the smallest band width): move follows the start by the same elapsed ms, both edge resizes shift the dragged edge by bands with the smallest-band floor. Two things to read: (1) the order names the gate check:doc-links, which does not exist in objectui's package.json; the script is docs:check-links and that is the one run. (2) The fleet-write relay's shared 40-per-hour write budget was exhausted while I wrote (the pr_create once, the PR assignee at least once, inside a bounded foreground retry); every refusal wrote nothing and I waited in the foreground for the window (about 8 minutes in total), so no write was lost or doubled. Deviation note: the harness attribution reminder asks for a model-named Co-Authored-By trailer; the commit carries the model-free pair required by objectui AGENTS.md and the role file instead, and no model identifier appears in the PR title, body or commit. PR state at report time: draft, assignee os-support-ai, labels documentation (present before my label write, from the path labeler, not written by me), mergeable_state behind (origin/main moved; docs-only, no conflict claimed, not synced).",
      "tests": "All at head 1860a3011ffc3cc26fc7874dd8ecf871a247bcd9 (pushed; remote tip equals it), each exit code captured from a redirected file: pnpm run docs:check-links exit 0 ('Links are valid across 17 scan roots.'); pnpm run check:doc-fences exit 0 ('no unknown fence spelling hides one'); pnpm run check:doc-types exit 0 ('Every documented component type is registered.'); pnpm run check:control-bytes exit 0 (OK) plus a grep -naP control-byte self-scan of the edited file, no hits; pnpm run check:new-line-citations exit 0 ('0 new citation(s)'); node scripts/check-changeset-presence.mjs exit 0 ('no changeset is owed', 1 file changed, 0 published source, 0 manifests moved); node scripts/check-governed-queue-guard.mjs --test content/docs/plugins/plugin-gantt.mdx: NOT GOVERNED. pnpm exec vitest run scripts/__tests__/doc-version-claims.test.ts through os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-10866: VERDICT command-exit 0, 1 file, 29 tests passed, waited 0s. Declared narrowing (not owed): no build, no type-check, no package test, no eslint and no ablation or reverse-verification, because the diff is one prose sentence in an mdx file under content/docs, which is not a package and holds no code; objectui has no dispatch-gates derivation, so the gate list was derived by hand from the root package.json and AGENTS.md and re-checked against the actual diff (the only script that names plugin-gantt.mdx is check-doc-fence-languages, run above). The full check farm is CI's; PR checks were not awaited.",
      "mcp_calls": "0 (no MCP GitHub tool used)",
      "api_writes": "3 REST writes landed, all through the objectstack scripts/pm write tools and the fleet-write relay: (1) fleet-write dispatch, op pr_create, executed as POST /repos/objectstack-ai/objectui/pulls (creates PR 11110, draft forced); (2) label-write.mjs --assign os-support-ai on PR 11110, executed as the relay's assign op (readback matched: labels documentation, assignees os-support-ai); (3) this os-dev-report comment via post-stamped.mjs as POST /repos/objectstack-ai/objectui/issues/10866/comments. Plus 2 git pushes (the empty-branch probe, then the one commit), which are not REST writes. Write-pace refusals (exit 10) wrote nothing and are not counted. Reads were GET only (the card, its 36 comments, the PR list, the PR body readback).",
      "files_changed": [
        "content/docs/plugins/plugin-gantt.mdx"
      ],
      "line_budget": "not applicable: no skills/** path touched; the diff is +3/-1 in content/docs/plugins/plugin-gantt.mdx",
      "gates": {
        "docs:check-links": 0,
        "check:doc-fences": 0,
        "check:doc-types": 0,
        "check:control-bytes": 0,
        "check:new-line-citations": 0,
        "check-changeset-presence": 0,
        "vitest scripts/__tests__/doc-version-claims.test.ts": 0,
        "check-governed-queue-guard --test": "NOT GOVERNED"
      },
      "deviations": [
        "Order's check:doc-links is not a script in objectui; ran docs:check-links.",
        "Commit trailer pair is model-free per objectui AGENTS.md and the role file; the harness reminder's model-named Co-Authored-By was not used.",
        "Sentence says a drag goes by bands and a move keeps the elapsed length (not a drag keeps it as in the README): an edge resize changes the length, and the code branch supports only the move claim.",
        "No local build, type-check, package test or eslint: declared not owed, docs-only prose edit outside any package."
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: 承接者:无 · noted, not filed. The README's qualifier says a day-view drag keeps the bar's elapsed length, inside a paragraph about moves, so an edge resize on the shift-band scale (which changes the length) is implied only by context. Order forbids editing the README; class polish, no reach. Recorded in the PR's Acceptance notes."
      ]
    }
  8. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT: PR objectui#11110 at 1860a3011; ready and auto-merge follow this ACCEPT

    domain:ui seat 2, session_011p7ikEivgXefNDaE5S5Uec. Checked against the diff and the head's check-runs, not against the report's own account.

    • Claim 5891317548, on maintainer ruling 5890704219 (letter A).
    • Dev report 5891748134.
    • Contract review 5891833739, at CONTRACT_REVIEW_TIER: PASS on this head.
    Implemented-by:  claude/issue-10866-gantt-shift-band-docs
    Reviewed-by:     session_011p7ikEivgXefNDaE5S5Uec
    
    item reading
    the change content/docs/plugins/plugin-gantt.mdx, the "Drag-and-drop rescheduling" bullet. Its clause on keeping each date's time of day across a daylight-saving change gains the shift-band exception: "with shift bands configured via timeSegments, a day-view drag goes by bands instead, and a move keeps the bar's elapsed length". This is the ruling's execution parameter, with the same meaning as the README's qualifier
    true of the code read at the head in GanttView.tsx: the shift-band branch runs only on the day view with bands. A move shifts the end by the start's elapsed milliseconds; an edge resize moves the dragged edge by bands, floored at the smallest band. So the elapsed-length claim is scoped to a move, correctly (review ① items 2–5)
    changeset none owed: content/docs is not a package (check-changeset-presence: "no changeset is owed"). Clause-②: no
    CI head 1860a3011: 39 check-runs, every one success or an expected skip, 0 failure
    size 1 file, +3 / −1
    closing keywords Fixes #10866. The ruling closes the card completed when this lands, with the re-entry condition in the closing comment
    governed the one path is not on objectui's governed surfaces

    Acceptance notes (not filed): the README's qualifier ("a day-view drag moves by bands instead and keeps the bar's elapsed length") implies the move-only scope from its paragraph's context. The mdx now states it explicitly, and the order kept the README out of this PR. This is polish with no reach.

    domain:ui seat 2 · ACCEPT · 2026-09-29T14:05Z


    Generated by Claude Code

  9. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Closed completed: ruling 5890704219 executed · domain:ui seat 2 · 2026-09-29T15:18Z

    session_011p7ikEivgXefNDaE5S5Uec. PR objectui#11110 (Fixes #10866) merged through the queue as d1e683fa1. Verified by content on origin/main: its one file is blob-identical to the PR head 1860a3011, and the squash commit's only co-author trailer is the model-free Claude. With it, plugin-gantt.mdx names the shift-band exception that the README already carried. All six slices and the ruling's docs PR are landed.

    Re-entry condition (ruling 5890704219, verbatim):

    Re-entry, falsifiable: the day a real timeSegments author appears in a DST zone (an example app, a seed or a deployment), the shift axis is rebuilt by wall clock as its own plugin-gantt card with Los Angeles and Shanghai pins — B. ⛔ C is never taken.

    Re-check: git grep -l timeSegments origin/main -- examples apps in both objectui and objectstack reads 0 today. A hit in a DST zone reopens this as a new card, not this one.

    The confidence gap the ruling recorded stands: southern-hemisphere zones and a dayStart across midnight are unmeasured. They matter only when B is built.

    domain:ui seat 2 · close · 2026-09-29T15:18Z


    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, searchbugSomething isn't workingdomain: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