Repository navigation
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
Description
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.
- plugin-calendar.
- Reads:
ObjectCalendarbuildsnew Date(startDate)andnew Date(endDate)from the record's date fields;calendar-view-rendererbuildsstart: new Date(record[startField]);resolveAuthoredCurrentDateparsesnew Date(raw).CalendarView.getEventsForDatethen 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 writesquickCreate.start.toISOString(). That is a UTC instant written into aField.date. Once the read is local midnight,toISOString()names the previous day east of UTC. The read and the write must be decided together.
- Reads:
- plugin-gantt.
- Reads:
ObjectGanttmapsstart/endwithnew Date(…)(falling back tonew Date()), and baselines withnew Date(baselineStartRaw);GanttViewparsesstartDate,endDateand each markerm.date, then re-bases everyDatethroughtzShiftinto a configured zone. - Write-back: the drag handler patches
patch[startDateField] = changes.start.toISOString(). Same coupling as the calendar, plus thetzShiftre-base.
- Reads:
No named date-only producer found in the repos (reach not established; listed so the family's census stays enumerated).
- plugin-timeline.
ObjectTimeline.dateBucketreadsstartOfDay(new Date(raw)), so west of UTC an item dated today lands in Overdue; the sort readsnew Date(a.startDate).getTime(); the renderer'sformatDateuses local getters forshort/longbuttoISOString().split('T')[0]by default; the axis extent parses then printstoISOString(). Three reads across two files must agree. The showcase binds the timeline tocreated_at, a datetime. - plugin-charts.
AdvancedChartImpl.formatTickparses a^\d{4}-\d{2}-\d{2}category withnew Date(str)thentoLocaleDateString, so2026-09-01readsAug 31west of UTC. Its fallback printstoISOString().slice(0, 10), which would name the previous day east of UTC once the parse is local. A pin needs a rendered axis. - core
FormulaFunctions.DATEADD,DATEDIFFandDATEFORMATparsenew Date(dateStr)and read local getters, whileTODAY(),NOW()andDATEADD's result are UTCtoISOString(). Routing only the parse would makeDATEADD's output name the previous day east of UTC: the formula date functions need one zone decision. - i18n helpers. The published
formatDate,formatDateTime,formatRelativeTime(formatting.ts) andformatDateSpec(spec-formatters.ts) accept a string and parse it withnew 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.tsxWRITESnew Date(v + 'T00:00:00').toISOString()for adatefield: local midnight as a UTC instant, whose date part is the previous day east of UTC. - components
custom/date-picker.tsx(DatePicker, exported) is typeddate?: Dateand 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 localYYYY-MM-DD(the dashboard range'stoIsoDateshape) rather thantoISOString(), and register a zone pin per site inPINS. DecidetzShift'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/srcstill answers.git -C objectui grep -n "toISOString()" origin/main -- packages/plugin-calendar/src packages/plugin-gantt/srcstill names the write-backs.- objectstack
examples/app-showcase/src/data/objects/task.object.tsstill declaresdue_date/start_date/end_dateasField.date.
Activity
objectstack-fleet commentedon Sep 28, 2026
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 cardTriage: lands in
plugin-calendar,plugin-gantt,plugin-timeline,plugin-charts, coreFormulaFunctionsand the i18n date helpers ⇒domain:ui. Rationale:toISOString()) into aField.date.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.dateholds a calendar day, the spec's four-digitYYYY-MM-DDform (the storage ruletemporalStorageForm). So:toDisplayDate;yyyy-MM-dd, ⛔ nevertoISOString();datetimefield 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.
PINSand pin each write's payload underAmerica/Los_AngelesandAsia/Shanghai.FormulaFunctions) needs one zone decision, which triage does not guess. Measure what the server formula engine'sTODAY()/DATEADDreturn for a date-only context and match it. If the two cannot agree without a spec statement, stop and report here.toDisplayDatein the same PR as any other site.objectstack-fleet commentedon Sep 28, 2026
Claim: PM loop round 10 —
domain:uiexecution seat 2Session:
session_014mXUNuFomfj24w7s1pZzhNBranch:
claude/issue-10866-calendar-gantt-date-only-daysWorktree:
objectui-issue-10866Domain:
domain:uiSeat:
domain:ui#2File surface: slice 1 of the closure card (
Part of #10866), sites 1 and 2 per triage5861646819item 1.packages/plugin-calendarandpackages/plugin-gantt. Every read of a stored date-onlyField.dategoes through core'stoDisplayDate. Every write of a date-only field (the calendar's drop and quick-create, the gantt's drag) sends the local calendar day asyyyy-MM-dd, ⛔ nevertoISOString(). Adatetimefield keeps its instant.scripts/__tests__/date-only-zone-pins-10183.test.ts: each repaired read is registered in itsPINSlist.America/Los_AngelesandAsia/Shanghai, and tests beside these..changeset/10866-…md(patchfor the two plugins).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:
origin/mainb93e245f9. None touchespackages/plugin-calendar,packages/plugin-ganttor the zone-pin driver. PR objectui#10861 (objectui#10844), the family's previous editor, has merged.objectstack-fleet commentedon Sep 28, 2026
Claim: PM loop round 2 —
domain:uiexecution seat 4 · slice 1 (site 1, plugin-calendar)Session:
session_01MCg3z5cGrV51xEddpGcxEYAccount:
os-litantBranch:
claude/issue-10866-calendar-date-onlyWorktree:
objectui-issue-10866Domain:
domain:uiSeat:
domain:ui#4File 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(eventstart/endreads; the drop and quick-create write-backs of a date-only field as the localyyyy-MM-dd, ⛔ nevertoISOString(); adatetimefield keeps its instant),calendar-view-renderer.tsx(theschema.dataevents,resolveAuthoredCurrentDate),CalendarView.tsx(getEventsForDate), reading throughtoDisplayDatefrom@object-ui/core;PINSlist ofscripts/__tests__/date-only-zone-pins-10183.test.ts(reads, and each write's payload underAmerica/Los_AngelesandAsia/Shanghai); tests beside them; one.changeset/10866-…md(patchfor@object-ui/plugin-calendar).⛔ Not plugin-gantt (site 2, the next slice: it registers in the same
PINSlist, so serial after this PR), timeline, charts,FormulaFunctionsor the i18n helpers. Stop on breach; explain in the report.Container & model:
M,mode:subagent,model: opus(TIER_DEFAULT,scripts/pm/dispatch-gates.mjs:12534on objectstack17bd3187;--tierrefuses objectui paths).Clause-②: no
Thread-read: 5861646819
Serial constraints cleared: open-PR file lists read 2026-09-28T01:49Z at objectui
origin/mainb93e245(10 open PRs): none touchespackages/plugin-calendar, the zone-pin driver ordate-display.ts. No in-flight lane claim names them. The family's previous editor, PR objectui#10861, merged asa60539b.Clause-②: no: reads and writes of an existing field change zone handling; no declared key, export or accept set moves.Generated by Claude Code
69 remaining items
objectstack-fleet commentedon Sep 29, 2026
Claim: PM loop round 2 —
domain:uiexecution seat 2 · the ruling's docs PRSession:
session_011p7ikEivgXefNDaE5S5UecAccount:
os-support-ai(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-10866-gantt-shift-band-docsWorktree:
objectui-issue-10866Domain:
domain:uiSeat:
domain:ui#2File surface (ruling
5890704219, letter A, execution parameters):content/docs/plugins/plugin-gantt.mdx: the "Drag-and-drop rescheduling" bullet's clause "keeps each date's time of day across a daylight-saving change" gains the shift-band exception. WithtimeSegments, a day-view drag moves by bands and keeps the bar's elapsed length. That is the qualifierpackages/plugin-gantt/README.mdalready carries (review5887794165①8).Stop on breach; explain in the report.
Container & model:
S,mode:subagent,model: sonnet(dispatch-gates --tier --repo objectstack-ai/objectuiover this surface: no path-derived mandate, at objectstackcd901d7a; a one-sentence docs edit, so the floor tier)Clause-②: no
Why no: one docs sentence is made true of the shipped behaviour; no schema, prop, type, export or behaviour moves.
Thread-read: 5890704219
Serial constraints cleared: read 2026-09-29T13:30Z at objectui
origin/main5b2ea1757. By their file lists, open PRs objectui#11099, fix(components,plugin-list,plugin-detail): the page root reads its nested aria bag, and ListView and useRecordAriaProps map through the shared reader (objectui#11083) #11102, fix(sdui-parser): a registration's declared every-node member wins with its type in the generated JSX types, so the .d.ts over the public manifest compiles (objectui#11075) #11103, fix(react): one Ctrl+Z pops one undo, however many useGlobalUndo instances are mounted, and a focused field keeps its own undo (objectui#11081) #11104 and chore(deps): resolve @objectstack/* 17.5.0, and the zod 4.6.5 it requires, in pnpm-lock.yaml (objectui#11073) #11086 touch noplugin-ganttpath. The in-flight objectui#11079 branch editspackages/plugin-timelineonly, by its claim. Closing: line 1Fixes #10866. The ruling closes the cardcompletedwhen this lands, with the re-entry condition recorded in the closing comment.Generated by Claude Code
objectstack-fleet commentedon Sep 29, 2026
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." ] }objectstack-fleet commentedon Sep 29, 2026
✅ ACCEPT: PR objectui#11110 at
1860a3011; ready and auto-merge follow this ACCEPTdomain:uiseat 2,session_011p7ikEivgXefNDaE5S5Uec. Checked against the diff and the head's check-runs, not against the report's own account.5891317548, on maintainer ruling5890704219(letter A).5891748134.5891833739, atCONTRACT_REVIEW_TIER: PASS on this head.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 viatimeSegments, 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 qualifierGanttView.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)content/docsis not a package (check-changeset-presence: "no changeset is owed").Clause-②: no1860a3011: 39 check-runs, every one success or an expected skip, 0 failureFixes #10866. The ruling closes the cardcompletedwhen this lands, with the re-entry condition in the closing commentAcceptance 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:uiseat 2 · ACCEPT · 2026-09-29T14:05ZGenerated by Claude Code
objectstack-fleet commentedon Sep 29, 2026
Closed
completed: ruling5890704219executed ·domain:uiseat 2 · 2026-09-29T15:18Zsession_011p7ikEivgXefNDaE5S5Uec. PR objectui#11110 (Fixes #10866) merged through the queue asd1e683fa1. Verified by content onorigin/main: its one file is blob-identical to the PR head1860a3011, and the squash commit's only co-author trailer is the model-freeClaude. With it,plugin-gantt.mdxnames 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-check:
git grep -l timeSegments origin/main -- examples appsin 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
dayStartacross midnight are unmeasured. They matter only when B is built.domain:uiseat 2 · close · 2026-09-29T15:18ZGenerated by Claude Code
fix(spec): correct GanttConfig.timeZone describe — date fields don't …
fix(plugin-timeline,core): the gantt axis draws a stored date-only da…
fix(plugin-calendar): a day event moved across a DST change writes th…
fix(plugin-gantt): a zoned chart reads and writes a stored day as tha…
fix(plugin-gantt): a gantt move shifts a task's end by the calendar d…
docs(plugin-gantt): the drag's time-of-day sentence names the shift-b…