Repository navigation
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
Conversation
…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>
✅ Console Performance Budget
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
Size Limits
|
Contract reviewServed-tier: 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 ① Derived judgments1. Instants in Measured,
Why (b), read against the four texts the brief names:
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 In-repo reach: no formula, example or test formats an instant's time of day ( Minimal repair: in 2. Month overflow and clamp. The head's private 3. Date-only in, 4. The gantt variant. All six stops read through
5. Pins. Both new files sit in the driver's 6. CI. Head 7. Commits. Three commits on the branch; ② Semver level
Changeset and docs sentences: the plugin-timeline bullet is true in every clause (measured above). The ③ Boundary flags
Implemented-by: 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
✅ Console Performance Budget
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
Size Limits
|
Contract reviewServed-tier: Delta record over ① Derived judgments1. Instants are back on the base path — verified, and item 1 of The diff: My own item-1 probe re-run, base Pins. The head's formula pin against base sources in the forks child: 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. " Commit: one commit, trailers 2. The PR body and title. Diffed against the body I read at
3. Mixed 4. Gantt half, CI and commits are as recorded at ② Semver level
③ Boundary flags
No defect found on Implemented-by: VERDICT: PASS Generated by Claude Code |
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 note5866905836, ruling5864664470(objectui#10903, its last execution parameter) and claim5867569822. 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 ruling5864664470states the arithmetic needs no statement.What changed
Timeline, gantt variant (
plugin-timelinerenderer.tsx)One read,
readGanttDate: a date string goes through coretoDisplayDate(the objectui#10183 convention, which the bar tooltip'sformatDatehas taken since slice 2), so a date-only2026-10-05is local midnight of the day it names and a value with a time part keeps its instant. A number or aDateis an instant and is read bynew Date(value)as before; for aDatethat 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 (toDateInputValueof aDate, 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, so2026-02-30is refused with the existingtimeline.gantt.unusableRange.malformedDatemessage (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.emptyGanttDateRange: the viewer's today (toDateInputValue(new Date())), where it was the UTC day.No import line of the renderer changed:
toDisplayDateandtoDateInputValuewere already imported from@object-ui/core.Core formula date functions (
coreevaluator/FormulaFunctions.ts)readFormulaDate: the engine parse (a date-only value is UTC midnight of its day, as before), the same namedInvalid dateerror, and adayflag fromisRealCalendarDate, the shared judgement, imported inside core fromutils/date-display(already on core's barrel).DATEADD: a calendar day goes throughaddToDay, which uses UTC setters. Its months go throughaddMonthsUtc: 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 stdlibaddMonthsUtcbehind CELaddMonths(d, n). A year is twelve months on the same rule. A day moved by days, months or years comes back asYYYY-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,setMonthwith 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()andNOW()are untouched. The objectui#10915 docblock onTODAY()stands, and a new docblock aboveDATEADDstates the three functions' rule.Pins, docs, changeset
PINSofscripts/__tests__/date-only-zone-pins-10183.test.ts, plus one header sentence:TimelineGantt.dateOnlyZone-10866(plugin-timeline) andFormulaFunctions.dateOnlyZone-10866(core). Each runs underAmerica/Los_AngelesandAsia/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.content/docs/plugins/plugin-timeline.mdx, "Time Scales (Gantt)"..changeset/10866-gantt-axis-formula-day-arithmetic.md:patchfor@object-ui/plugin-timelineand@object-ui/core.Readings, base
3b469c8eaagainst head, in both zonesBase 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 coreFormulaFunctionsunderTZ. Head readings: the pins, green.Gantt,
en-US:gantt-style-timeline.json, month axis2026-10-05…2026-10-072026-10-05…2026-10-07, instant bar from2026-10-06T04:00Z, itsleft2026-10-06, pinned end2026-10-06T03:00Z2026-10-05T20:00Z, day scale2026-02-30(suite zone, UTC)items[0].items[0].startDateFormula functions:
DATEADD('2026-01-31', 1, 'month')2026-03-03T00:00:00.000Zin every zone2026-02-28DATEADD('2026-03-31', -1, 'months')2026-03-03T00:00:00.000Z(UTC, Shanghai),2026-03-03T01:00:00.000Z(LA)2026-02-28DATEADD('2024-02-29', 1, 'year')2025-03-01T00:00:00.000Zin every zone2025-02-28DATEADD('2026-09-01', 1, 'day')2026-09-02T00:00:00.000Z2026-09-02DATEADD('2026-11-01', 1, 'day')2026-11-02T01:00:00.000Z; UTC and Shanghai2026-11-02T00:00:00.000Z2026-11-02DATEADD('2026-03-08', 1, 'day')2026-03-08T23:00:00.000Z; UTC and Shanghai2026-03-09T00:00:00.000Z2026-03-09DATEADD('2026-09-01', 1, 'month')2026-10-02T00:00:00.000Z; UTC and Shanghai2026-10-01T00:00:00.000Z2026-10-01DATEADD('2026-01-31T12:00:00.000Z', 1, 'month')(instant)2026-03-03T12:00:00.000Zin every zoneDATEADD('2026-10-31T12:00:00.000Z', 1, 'day')(instant)2026-11-01T13:00:00.000Z; UTC and Shanghai2026-11-01T12:00:00.000ZDATEADD('2026-09-01', 3, 'hours')2026-09-01T03:00:00.000ZDATEFORMAT('2026-09-01', 'YYYY-MM-DD')2026-08-312026-09-01DATEFORMAT('2026-09-01', 'DD/MM/YY HH:mm:ss')31/08/26 17:00:00, Shanghai01/09/26 08:00:0001/09/26 00:00:00DATEFORMAT('2026-09-01T03:00:00.000Z', 'YYYY-MM-DD HH:mm')(instant)2026-08-31 20:00, Shanghai2026-09-01 11:00, UTC2026-09-01 03:00DATEDIFF('2025-12-31', '2026-01-01', 'year')DATEDIFF('2026-08-31', '2026-09-01', 'month')TODAY()andDATEFORMAT(NOW(), 'YYYY-MM-DD')at2026-09-28T05:30Z2026-09-28and2026-09-27Server parity,
@objectstack/formula17.4.0 as installed here (CELcelEngine.evaluate), the same in LA and Shanghai:addMonths('2026-01-31', 1)is2026-02-28T00:00Z,addMonths('2024-02-29', 12)is2025-02-28T00:00Z,addDays('2026-11-01', 1)is2026-11-02T00:00Z,addDays('2026-10-31T12:00:00.000Z', 1)is2026-11-01T12:00Z,addMonths('2026-01-31T12:00:00.000Z', 1)is2026-02-28T12:00Z, andaddDays('2026-02-30', 1)is2026-03-03T00:00Z, which headDATEADDanswers 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 is5b48d0292, the contract-review repair; its readings are in the first bullet below.5b48d0292(contract review5869002399, item 1): instants are back on the base path. A probe of base3b469c8eaagainst head under UTC, Los Angeles and Shanghai reads every instant row the same on both:DATEFORMAT('2026-09-01T03:00:00.000Z', 'HH:mm')is03:00/20:00/11:00;DATEADD('2026-10-31T12:00:00.000Z', 1, 'day')is2026-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')is2026-03-03T12:00:00.000Zin 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-checkfor core and plugin-timeline echoestsc --noEmit && tsc -p tsconfig.test.jsonandDone;pnpm exec vitest run packages/core/ packages/plugin-timeline/ scripts/__tests__/date-only-zone-pins-10183.test.ts --maxWorkers=2givesTest Files 220 passed (220),Tests 4037 passed | 58 skipped (4095), VERDICT command-exit 0. Red on base, with the baseFormulaFunctions.tsblobb5e63812fe3fproven on disk: the formula pin in the forks child givesTests 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'sDATEFORMATin Los Angeles and Shanghai,DATEDIFFin Los Angeles), and all six instant controls are green. The driver givesTests 2 failed | 18 passed (20). Restored: blobedb61cdf519cequals HEAD,git diff HEADempty. Ablation (an instant's hour read withgetUTCHours): 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.ts26 warnings, as on base. The 11 changeset-readingscripts/__tests__suites were not re-run locally (two lock queue-timeouts); CI's eight test shards, which includescripts/**, are green. CI on5b48d0292: 43 check-runs, 40 success, 3 skipped, 0 failed.3b469c8eaplus the two pins (comparison worktree,OBJECTUI_DATE_ZONE_CHILD=1 pnpm exec vitest runover 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.2006ddb03(the driver itself): the base blobs ofrenderer.tsx(445a11147770) andFormulaFunctions.ts(b5e63812fe3f) checked out and proven on disk (each equal to its base blob, not its head blob), thenpnpm 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 withgit checkout HEAD --on absolute paths: each blob equal to HEAD (e0bc666ef587,717f3bd2cdc7),git diff HEADempty.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.8dd4543cc(one commit later, which typesreadFormulaDate's argument instead of casting it): closure buildpnpm --workspace-concurrency=2 --filter '@object-ui/plugin-timeline^...' --filter '@object-ui/core^...' run buildexit 0;pnpm --workspace-concurrency=2 --filter @object-ui/plugin-timeline --filter @object-ui/core run type-checkechoestsc --noEmit && tsc -p tsconfig.test.jsonandDonefor each;pnpm type-check:scriptsexit 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.--listFileson each package's test program lists its new pin once.8dd4543cc(objectstackscripts/ablation-replace.mjs, wrap mode, each run through the pin on the forks pool with the driver's flag; nodistinvolved, the pins import../rendererand../FormulaFunctionsrelatively):toISOString()again, the parse left on the shared step (anchorminDate: toDateInputValue(new Date(minTimestamp)),, 1 to 0, blobe0bc666ef587to3265f47d94fa). 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.addMonthsUtc(date, amount);, 1 to 0, blob1695444571e2toa50648d3900b). Predicted and measured: exactly the month-clamp and instant-clamp rows red in each of the three zones,Tests 6 failed | 27 passed (33).git diff HEADempty.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).scripts/__tests__gate, at8dd4543cc: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.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.config.js,--no-inline-config --format json, 5 results): 0 errors.FormulaFunctions.ts26 warnings andrenderer.tsx14, each equal to its base blob via--stdin; the two pins and the driver 0. The config sets noparserOptions.projectorprojectService, so the rules are not type-aware and this diff cannot move an untouched file's verdict. The repo-wide lint is CI's.@objectstack/formula17.4.0 as installed here, above; the rule was read in objectstackpackages/formula/src/stdlib.ts(addMonthsUtc,addDaysUtc) at objectstackorigin/main50e273fd.Acceptance notes
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.DATEADD/DATEDIFF/DATEFORMATis unchanged from the base (contract review5869002399, 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, soDATEFORMATprints its clock in the viewer's zone:DATEFORMAT('2026-09-01T03:00:00.000Z', 'HH:mm')is20:00in Los Angeles,11:00in Shanghai and03:00in UTC, as on the base.DATEADDof 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. Triage5861646819says adatetimekeeps its instant, and ruling5864664470's parameter names only the date-only month overflow; putting instants on the UTC calendar, or clamping their months, would be a separate decision. SoDATEFORMAT(NOW(), 'YYYY-MM-DD')still names the viewer's day, which can differ fromTODAY()'s UTC day for part of every day, as on the base.DATEADDhands 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.flex-1, not measured.timeZone"persisted data stays real instants" prose sits in@object-ui/types(GanttConfig.timeZone's JSDoc), inplugin-gantt(GanttViewProps.timeZone's JSDoc, "Writes still persist real instants", andmakeTzShift's JSDoc), and in the spec'sview.zod.tsdescribe, which the@object-ui/typeszod mirror reuses and which only objectstack can edit. That is two more packages here and a cross-repo half.endDateFieldwrite and the gantt no-schema path off UTC each need a new zone pin.FormulaFunctionsimportsutils/date-displayinside core, a module core's barrel already re-exports.Bundle Analysisis left to CI.check-governed-queue-guard --testover the 7 paths: NOT GOVERNED (control:AGENTS.mdexit 3).Serial
3b469c8ea.git merge-tree --write-treeof head8dd4543ccagainstmain06a96e948: clean (exit 0), and nothingmaingained since the base touches these 7 paths.Implemented by the dev agent for
domain:uiseat #1, sessionhttps://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk.Generated by Claude Code
Body amended by the
domain:uiseat #1 after contract review5869002399and the repair commit5b48d0292; the repair text is the dev's.