Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

[repo:objectui] Pure date fields shift one day for viewers west of UTC — '2026-08-01' renders 7月31日 in PDT; the display formatter parses the date-only string via new Date('YYYY-MM-DD') (UTC midnight) then reads local getters #10110

Closed

Description

@baozhoutao

Summary

A pure date field stored as 2026-08-01 is displayed as 7月31日 in a browser whose local timezone is west of UTC (observed: macOS in PDT, UTC-7). The date-only ISO string is parsed with new Date('2026-08-01') (UTC midnight) and then read back with local getters, so every date-only value shifts one calendar day for any user west of UTC — and the relative branch in the same formatter (#14885) is off by one for the same reason (2026-08-31 reads 4天前 on 2026-09-03).

The stored value is correct; only the display is wrong. Same formatter as objectstack-ai/objectstack#14885, different defect (value correctness vs. presentation choice).

Minimal reproduction

Platform 17.2.0 console. Machine timezone America/Los_Angeles (PDT, UTC-7); date → Thu Sep 3 03:35 PDT 2026.

Object kpi_plan with period_start: Field.date({ label: '周期开始' }), seeded period_start: '2026-08-01', period_end: '2026-08-31'.

GET /api/v1/data/kpi_plan → "period_start":"2026-08-01","period_end":"2026-08-31"

List cell 周期开始 → 7月31日     ← expected 8月1日
List cell 周期结束 → 4天前       ← expected 3天前 (or, per objectstack-ai/objectstack#14885, 8月31日)

Same page opened from a UTC+8 browser earlier the same day shows 8月1日 / 3天前 — the display depends on the viewer's timezone although the value has no time component.

Where

ui-components chunk, date display formatter:

function Ltt(e, t) {
  ...
  let n = e instanceof Date ? e : new Date(e);     // '2026-08-01' → 2026-08-01T00:00:00Z
  let r = new Date(),
      i = new Date(r.getFullYear(), r.getMonth(), r.getDate()),
      a = new Date(n.getFullYear(), n.getMonth(), n.getDate()).getTime() - i.getTime();  // local getters → Jul 31 in UTC-7
  let o = Math.round(a / 864e5);
  if (o < -7 || o > 7) return k9(n, void 0, t);   // k9 → toLocaleDateString(...) on the shifted Date
  ...

Per ECMAScript, a date-only ISO form is parsed as UTC while date-time forms without offset are parsed as local — so new Date('YYYY-MM-DD') is exactly the wrong constructor for a calendar date.

Expected

  • A date value (no time component) renders the same calendar date for every viewer, in every timezone. Parse YYYY-MM-DD into local components (new Date(y, m - 1, d)) or format the string directly; never through a UTC-midnight Date.
  • Same care in the date input/editor and in export, if they share the helper (not checked).
  • datetime fields keep timezone conversion — that is the case where it is wanted.

Environment

@objectstack/console 17.2.0 · objectstack dev · Chrome on macOS, tz PDT. App: objectstack-ai/kpi, object kpi_plan. Related: objectstack-ai/objectstack#14885 (relative rendering of the same field).

Activity

  1. added theissue type on Sep 10, 2026
  2. huangyiirene commented on Sep 10, 2026

    @huangyiirene
    Collaborator

    Triage: lands in objectstack-ai/objectui (the list/detail date formatter) ⇒ repo:objectui; type Bug; priority:p2 unchanged; pm:queue.

    Rationale: the card measures the API returning "period_start":"2026-08-01" and the console cell rendering 7月31日 on a UTC-7 machine ⇒ the stored value is right and the display is wrong, so the defect is entirely on the objectui side. Already repo:objectui; the missing piece was a state label.

    Type Bug, and this is the correctness half of the pair — ⭐ see the note on #14885, which shares the formatter and must ⛔ not be folded into one acceptance: suppressing relative rendering (#14885's surface) would hide this defect rather than fix it.

    priority:p2 stands and is correctly one step above its sibling: a business date that reads as the wrong day for every viewer west of UTC is a value the user will act on, not a styling preference.

    Triage seat · session_013hshVTmHY5F7rhpNtYHa3m · R+165 · 2026-09-10T01:0xZ


    Generated by Claude Code

  3. huangyiirene commented on Sep 16, 2026

    @huangyiirene
    Collaborator

    Still present on @objectstack/console 17.4.0, and on two more surfaces than the list cell — measured today in HotCRM (objectstack-ai/hotcrm).

    Machine America/Los_Angeles (PDT, UTC-7), date → Wed Sep 16 08:05 PDT 2026; console locale zh-CN; objectstack dev on the SQLite driver.

    object · field (Field.date) stored (SQLite date column, typeof = text) rendered surface
    crm_cost_plan_month.period_month 2026-09-01 / 2026-10-01 / 2026-11-01 8月31日 / 9月30日 / 10月31日 related-list grid on the crm_cost_plan record page
    crm_presales_project.planned_end 2026-11-16 11月15日 record detail field
    crm_presales_project.planned_start 2026-09-16 昨天 (on 2026-09-16, PDT morning) record detail field — the relative branch (#14885) on top of the same shift

    The wire value was not re-measured on this box (no API session); driver-sql 17.4.0 reads a date column back through toDateOnly ("formatOutput repairs those to canonical on read"), so it is the bare YYYY-MM-DD the card measured on 17.2.0.

    Where it is in 17.4.0. The helper the card names has moved from the ui-components chunk into framework-*.js, unchanged in shape — bo():

    let n = e instanceof Date ? e : new Date(e);                                          // '2026-09-01' → UTC midnight
    let r = new Date, i = new Date(r.getFullYear(), r.getMonth(), r.getDate()),
        a = new Date(n.getFullYear(), n.getMonth(), n.getDate()).getTime() - i.getTime(),  // local getters → Aug 31 in UTC-7
        o = Math.round(a / 864e5);
    if (o < -7 || o > 7) return xo(n, void 0, t);   // xo → toLocaleDateString(locale, { year: sameYear ? undefined : 'numeric', month: 'short', day: 'numeric' }) → 8月31日
    …
    return yo(o, t?.locale);                        // yo → Intl.RelativeTimeFormat(locale, { numeric: 'auto' }).format(o, 'day') → 昨天

    Both surfaces in the table render through it. Two more new Date(dateOnly) sites in the same bundle would stay shifted if only bo() is fixed:

    • RecordDetailView-*.js, the field-value formatter: if (r === 'date' || r === 'datetime') { let e = new Date(t); … r === 'date' ? e.toLocaleDateString(n.locale) : … }
    • ui-components-*.js, the readonly face of the date field widget (B9e): new Date(e).toLocaleDateString(o) — its datetime sibling is objectui#8209's surface.

    Expected is as the card states: a date value renders the same calendar date for every viewer — parse YYYY-MM-DD into local components (or format the string directly), never through a UTC-midnight Date; datetime keeps its conversion.

    HotCRM authors no workaround for this (AGENTS.md §2 — wait for the platform fix); this card is the reference.

    Generated by Claude Code

  4. self-assigned this
    on Sep 21, 2026
  5. os-elon-musk commented on Sep 21, 2026

    @os-elon-musk
    Collaborator

    Claim: PM loop round 2 — domain:ui execution seat
    Session: session_01Xr7APep6jm1Zta3KUzPzZf
    Branch: claude/issue-10110-date-only-utc-shift
    Worktree: objectui-issue-10110
    Domain: domain:ui
    Seat: domain:ui#1
    File surface: packages/core/src/utils/ (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default judgement tier — ⚠️ dispatch-gates.mjs refuses for this repo ⇒ ⛔ no path-derived mandate; tier is this seat's judgement. Default judgement tier, ⛔ not the mechanical floor: a date-parsing repair has a wrong fix that also makes the symptom go away, and telling them apart is judgement.
    Clause-②: no
    Thread-read: 5749945743
    Serial constraints cleared: ⭐ checked by branch diff, not by declaration. Seat #2's objectui#10132 touches packages/core/src/utils/chart-presentation.ts and packages/core/src/utils/dashboard-filters.ts — same directory, different files from this card's expected landing site, date-display.ts. ⚠️ If the repair needs dataset-format.ts or either of those two files, stop and report: that is a live collision, ⛔ not a judgement call for the dev. Same-batch siblings objectui#10112 and objectui#10107 are both packages/plugin-detail/src/ — disjoint. packages/fields/ is held by the unmerged objectui#10172 — also a stop-and-report if needed.

    Why Clause-②: no

    A date-only value that renders as the wrong calendar day is a defect in presentation, not a contract question: the stored value is already correct and the card says so. Nothing is declared, widened or retired. ⛔ Stop and report if the route reaches for a new exported helper or a published option.

    domain:ui seat #1 · session_01Xr7APep6jm1Zta3KUzPzZf · claim · 2026-09-21T01:30Z


    Generated by Claude Code

  6. os-elon-musk commented on Sep 21, 2026

    @os-elon-musk
    Collaborator

    os-dev-report

    {
      "issue": 10110,
      "status": "done",
      "branch": "claude/issue-10110-date-only-utc-shift",
      "pr": "https://github.com/objectstack-ai/objectui/pull/10182",
      "session": "session_01Xr7APep6jm1Zta3KUzPzZf",
      "premise_still_valid": true,
      "clause_2": "no — held. No new export, no new published option, no signature change. `toDisplayDate` is module-private; `@object-ui/fields` re-exports the same four names it always did.",
      "summary": "The defect was exactly where the card said: `packages/core/src/utils/date-display.ts` parsed every string value with one `new Date(value)`, which ECMAScript reads as UTC for a date-only form and as local for a date-time form without an offset. A single module-private parse step, `toDisplayDate`, now rebuilds a date-only value at LOCAL midnight of the day it names; all four entry points (`formatDate`, `formatRelativeDate`, `formatDateTime`, `formatDateTimeCompactParts`) go through it, so the absolute and relative symptoms close together. No hours are added or subtracted anywhere. A value carrying a time is untouched, and the accepted/rejected boundary is unchanged: the engine's own parse still decides validity, and a well-shaped impossible day still rolls. One changeset (`@object-ui/core: patch`) declares the behaviour change.",
      "timezones_driven": "Four real zones, two west of UTC and two east, plus a UTC control inside the ablation probes: America/Los_Angeles (UTC-7, the card's machine), America/Santiago (UTC-4/-3, and 2026-09-06 has NO local midnight there), Asia/Shanghai (UTC+8), Pacific/Kiritimati (UTC+14). The datetime controls are asserted in the west and the east and must disagree about the calendar day, which is what keeps assumption C measurable.",
      "tests": "All from the repo root, exit codes captured to files and read back. (1) the new pin `pnpm exec vitest run packages/core/src/utils/__tests__/date-display.dateOnlyZone-10110.test.ts` -> exit 0, 'Test Files 1 passed (1) / Tests 17 passed (17)'. (2) 25 date-family files across core/components/fields/plugin-grid/plugin-gantt/plugin-dashboard -> exit 0, 'Test Files 25 passed (25) / Tests 438 passed (438)'. (3) under the shared lock, `pnpm --filter '@object-ui/core^...' build && tsc --noEmit && tsc -p tsconfig.test.json && pnpm exec vitest run packages/core/` -> 'VERDICT command-exit 0', tsc silent, 'Test Files 160 passed (160) / Tests 3367 passed (3367)'. A first attempt at the typecheck WITHOUT the closure build reported TS6305 ('packages/types/dist not built') — read as PREREQUISITE NOT MET, not a red, and re-run after the closure build. ABLATION (three legs, each: mutate -> prove it reached disk by anchor counts in both directions plus a blob-hash change -> run the pin -> restore with `git checkout HEAD -- path` -> prove `git diff HEAD` empty; a trap restores on any exit; all three printed 'RESTORE OK'). No build is involved: vitest runs the source, so there is no dist leg. A = revert `toDisplayDate` to the plain parse -> 9 failed / 8 passed. B = the fake repair (twelve hours added to the parsed instant) -> 2 failed / 15 passed: EVERY case the card reported, west of UTC, stays green, and the only reds are Pacific/Kiritimati and the all-zones-agree case. C = drop `local.setFullYear(year)` -> 1 failed / 16 passed, the two-digit-year case alone. The measured matrix is in the test header; two predictions were falsified and are corrected there rather than quietly dropped.",
      "gates": "Run locally, all exit 0: check:control-bytes; check:test-path-roots; check:changeset-claims (report-only — it asks the seat to re-read the objectui#7178 move changeset, whose claim is that the MOVE changed no behaviour; re-read, still true, left alone); check:pending-changeset-literals; check:new-line-citations (0 new citations); check:unreferenced-sources; check:vi-mock-specifiers; `node scripts/check-changeset-presence.mjs` (1 published source file, 1 changeset). `tsc --noEmit` + `tsc -p tsconfig.test.json` for @object-ui/core: exit 0. eslint --no-inline-config on the two changed source files: exit 0, 2 files linted, 0 errors, 1 warning (`no-explicit-any` on the one surviving cast, which the four lines it replaces already carried). DECLARED NARROWING on lint: the repo-wide `pnpm lint` (turbo) was not run locally — CI's required `Lint` owns it. Invariance: eslint.config.js configures no type-aware linting (no `project` / `projectService` / `parserOptions` key in it), so this diff cannot move the verdict on any file it does not touch. The receiving population was NOT counted, so this is a declared narrowing and not a measurement of the full run. NOT MEASURED locally, declared to CI: the 9 merge-queue required checks — Lint, Type Check, Test shards 1/4..4/4, Build & E2E, Build Docs, Changeset Declaration. `dispatch-gates.mjs` does not exist in this repo, so the list above is hand-derived from the diff (one published source file, one test file, one changeset).",
      "line_budget": "n/a — no `skills/**` file in the diff, so no published-skill line budget applies.",
      "files_changed": [
        "packages/core/src/utils/date-display.ts (+67/-4: one module-private `toDisplayDate` plus its docblock, the shared `ISO_DATE_ONLY_RE`, four call sites, one header paragraph)",
        "packages/core/src/utils/__tests__/date-display.dateOnlyZone-10110.test.ts (new, 17 cases across four zones)",
        ".changeset/date-only-calendar-day-10110.md (new, '@object-ui/core': patch)"
      ],
      "api_writes": "2 REST proxy writes — POST /repos/objectstack-ai/objectui/pulls (draft 10182), POST /repos/objectstack-ai/objectui/issues/10110/comments (this report). No label, assignee or draft-state write; needs:contract-review not touched. Three `git push` calls over https are not REST writes.",
      "mcp_calls": "0 — no MCP GitHub tool was called, read or write.",
      "deviations": "(1) The claim comment lists `dataset-format.ts` alongside seat #2's two files as a stop-and-report, while ZONE 2 A of the dispatch says to say so if the defect is there too. No conflict arose in practice: `dataset-format.ts` contains NO `new Date(` at all — it sniffs the date-only spelling with its own ISO_DATE_ONLY_RE and delegates every render to date-display, so the one repair covers its measure path. Nothing in it was edited, and neither of seat #2's files was opened for edit. (2) The repair touches all FOUR entry points, including the two datetime faces, not only the two with measured symptoms: the defect is in the shared parse step, and leaving two entry points on the broken constructor would recreate the two-conventions drift this module exists to prevent. It cannot disturb a real datetime — a datetime string never matches the date-only regex — and the datetime controls in the pin assert the conversion still happens, in both directions from UTC. (3) Lint narrowed (see `gates`). (4) CI was not waited on; the report is delivered at the end of local verification, as the contract requires.",
      "open_questions": [],
      "out_of_scope_findings": [
        "to file — class: a (reproducible defect, same root cause, outside the ruled directory). `packages/components/src/renderers/complex/data-table.tsx` pre-parses a date-only cell value (`Date.parse` then `new Date(ts)`) and hands `formatDate` a Date, so the value arrives already shifted and the repaired shared path cannot see it. Probe with the repaired module, TZ=America/Los_Angeles: formatDate(new Date(Date.parse('2026-08-01'))) renders 'Jul 31' while formatDate('2026-08-01') renders 'Aug 1'; both render 'Aug 1' under TZ=UTC, which is why its convention pin (data-table-date-convention-7620.test.tsx) cannot see it either. Dedupe words: data-table, formatCellValue, date-only, UTC midnight, pre-parsed Date.",
        "to file — class: a. `packages/plugin-detail/src/DetailView.tsx` summary-chip `date` branch parses and formats on its own (`new Date(val)` then `toLocaleDateString(locale, { dateStyle: 'medium' })`). Probe, TZ=America/Los_Angeles, zh-CN: '2026-08-01' renders '2026年7月31日'; under TZ=UTC '2026年8月1日'. Dedupe words: summary chip, DetailView, date branch, dateStyle medium, timezone shift.",
        "to file — class: a. `packages/app-shell/src/utils/auditHistoryDisplay.ts` `date` branch, same shape (`new Date(value).toLocaleDateString(locale)`). Probe, TZ=America/Los_Angeles, en-US: '2026-08-01' renders '7/31/2026'; under TZ=UTC '8/1/2026'. Dedupe words: audit history, field value formatter, date branch, toLocaleDateString, timezone shift.",
        "noted, not filed: `packages/fields/src/index.tsx` DateCellRenderer sets `title` from the parsed instant (`toISOString`) and feeds the same instant to its overdue test, so a date-only value now has a tooltip naming a UTC instant the cell no longer displays. Cosmetic, and `packages/fields/` is held by the unmerged objectui#10172 — carrier: whoever lands next in that file.",
        "noted, not filed: `packages/plugin-detail/src/InlineFieldInput.tsx` converts a date input back through `new Date(v + 'T00:00:00')` on WRITE. Ruling 3 puts the write path out of scope and the card declares the stored value correct — carrier: whoever takes the date editor next.",
        "noted, not filed (mechanism, no card): writing `process.env.TZ` inside a test in this repo does NOT move the zone and fails silently — the root config runs `pool: 'threads'` and a worker thread's `process.env` has no native setter behind it, so `Intl` keeps reporting UTC and every date-only assertion passes for the wrong reason. The first draft of this pin had that shape and went green against the UNFIXED code. Written up in the new test's header; the working mechanism is a child process started with TZ in its environment. Carrier: the next test that needs a non-UTC zone."
      ]
    }

    Generated by Claude Code

  7. os-elon-musk commented on Sep 21, 2026

    @os-elon-musk
    Collaborator

    ✅ ACCEPT — PR objectui#10182, reviewed against the tree and CI at its head

    domain:ui seat #1, session_01Xr7APep6jm1Zta3KUzPzZf, 2026-09-21T02:13Z. ⛔ Reviewed against the delivered diff, ⛔ not the report's self-description.

    Implemented-by:  claude/issue-10110-date-only-utc-shift
    Reviewed-by:     session_01Xr7APep6jm1Zta3KUzPzZf
    

    Checklist

    item reading
    scope held — 3 files, all packages/core/src/utils/ + changeset. Seat #2's chart-presentation.ts / dashboard-filters.ts never opened
    size / path surface 429 lines; ⛔ no governed path
    CI 40 success / 3 skipped / 0 red / 0 running
    clause ② no, verified: toDisplayDate declared at :146 without export; no signature, export or option added
    changeset @object-ui/core: patch; the 7178 claim re-read and answered at 5754471...

    ⭐⭐ The measurement that makes this PR worth more than its diff

    ZONE 1 ruling 4 said a fix that adds or subtracts hours is wrong even when the symptom disappears. The dev did not just comply — it built that wrong fix and measured it. Ablation leg B, twelve hours added to the parsed instant:

    2 failed / 15 passed — every case the card reported, west of UTC, stays green. The only reds are Pacific/Kiritimati (UTC+14) and the all-zones-agree case.

    ⇒ the plausible wrong repair passes every piece of evidence the card contains. A pin written from the card's own reproduction would have certified it, and the defect would have returned at a DST boundary or east of UTC with no one able to say why. ⭐ That is the strongest argument I have seen in this lane for driving a real zone east as well as west, and it is now pinned.

    Four real zones driven — America/Los_Angeles, America/Santiago (which has no local midnight on 2026-09-06), Asia/Shanghai, Pacific/Kiritimati — plus datetime controls asserted in both directions that must disagree about the calendar day, which is what keeps the 「datetime must not move」 requirement measurable rather than asserted.

    ⭐ A green-on-green the dev caught and disclosed

    Writing process.env.TZ inside a test in this repo does not move the zone and fails silently: the root config runs pool: 'threads', and a worker thread's process.env has no native setter behind it, so Intl keeps reporting UTC. The first draft of this pin had that shape and went green against the unfixed code. The working mechanism is a child process with TZ in its environment. Written into the new test's header, where the next person to need a zone will find it.

    Verified independently, ⛔ not taken from the report

    • toDisplayDate at :146, no export ⇒ module-private.
    • All four entry points route through it: formatRelativeDate :215 · formatDate :273 · formatDateTimeCompactParts :328 · formatDateTime :375.
    • Added lines contain no getTimezoneOffset, no * 60, no 3600000, no setHours( ⇒ ruling 4 honoured in the literal sense as well as the measured one.

    ⭐ And this is why the wider repair was right rather than scope creep: before, four entry points each built their own new Date(value) and agreed only by convention. Now there is one parse step. Repairing only the two with measured symptoms would have left two on the old constructor — the two-conventions drift this module exists to prevent, reintroduced inside a single file.

    out_of_scope_findings

    • Three renderers that pre-parse or re-implement and so cannot be reached by this repair → filed objectui#10183, one card because they are one mechanism with one remedy, each carrying its own west-vs-UTC probe. ⚠️ Its most useful line: data-table already has a date-convention pin and it is green, because it runs in UTC where the defect is invisible.
    • fields DateCellRenderer tooltip · InlineFieldInput write path · the process.env.TZ mechanism → noted, not filed, each with a carrier. ⛔ Correct dispositions; the write path is out of scope by ruling 3 and the card declares the stored value correct.

    Deviations — all three sound

    1. dataset-format.ts was named a stop-and-report; no conflict arose because it contains no new Date( at all — it sniffs the date-only spelling and delegates to date-display, so the one repair covers its measure path. Nothing in it, or in seat Add automated testing infrastructure and CI/CD workflows #2's two files, was opened for edit.
    2. The repair touches all four entry points, not only the two with symptoms — justified above and pinned in both directions.
    3. Lint narrowed to the changed package with the invariance argument stated (no type-aware linting configured, so the diff cannot move a verdict on a file it does not touch) and the receiving population declared not counted. ⭐ That is a declared narrowing, ⛔ not a measurement dressed as one.

    ⇒ going ready and arming auto-merge SQUASH. The card stays open on Part of, released back to the queue on merge with the residual named.

    domain:ui seat #1 · session_01Xr7APep6jm1Zta3KUzPzZf · ACCEPT · taken 2026-09-21T02:13Z


    Generated by Claude Code

  8. os-elon-musk commented on Sep 21, 2026

    @os-elon-musk
    Collaborator

    🟢 Partial landing — the ruled half is on main. Released back to the queue, ⛔ not closed.

    domain:ui seat #1, session_01Xr7APep6jm1Zta3KUzPzZf, 2026-09-21T02:28Z. PR objectui#10182 merged; squash 516583b54.

    Verified by CONTENT with a control taken on the pre-merge base, ⛔ not by sha and ⛔ not by an ancestor test:

    probe over packages/core/src/utils/date-display.ts pre-merge 7725c10a0 merged 516583b54
    toDisplayDate 0 6

    What landed

    One module-private parse step; all four entry points (formatDate, formatRelativeDate, formatDateTime, formatDateTimeCompactParts) funnel through it, so the absolute and relative symptoms close together and the module goes from four parse sites to one. ⛔ No hours added or subtracted anywhere — verified by grep over the added lines. Pinned in four real zones, two west and two east, with datetime controls that must disagree about the calendar day.

    ⭐ The measurement worth remembering: an ablation of the plausible wrong fix (twelve hours added to the parsed instant) left every case this card reports green — only Pacific/Kiritimati and the all-zones-agree case caught it. A pin written from this card's own reproduction would have certified the wrong repair.

    Release: partial landing — session `session_01Xr7APep6jm1Zta3KUzPzZf`; reason: PR objectui#10182 carried `Part of`, not a closing keyword; destination: `pm:queue`, unassigned, this lane.
    

    ⚠️ What remains — and the first thing the next taker must check

    The residual is carried by objectui#10183: three renderers that pre-parse or re-implement a date-only value and so cannot be reached by the shared repair — components/.../data-table.tsx, plugin-detail/.../DetailView.tsx's summary chip, and app-shell/.../auditHistoryDisplay.ts.

    ⭐ This is not merely adjacent, and I am flagging it rather than letting the next taker rediscover it: this card's own reported reproduction is a list cell (「List cell 周期开始 → 7月31日」), and objectui#10183's first site is data-table pre-parsing before it calls the shared formatter. ⇒ the symptom this card was filed for may still be live on the very surface it was observed on, even though the module it named is now correct.

    ⛔ I have not measured which renderer draws that particular list cell, so this is a lead, ⛔ not a finding. ⇒ the first act on this card is to re-run its own reproduction on merged main. If it is fixed, close this card and let objectui#10183 carry the rest; if it still shifts, this card's remaining work is objectui#10183's first site and should be tracked there rather than duplicated here.

    ⚠️ ⛔ Do not close this card on the strength of the merge alone.

    domain:ui seat #1 · session_01Xr7APep6jm1Zta3KUzPzZf · release · readings taken 2026-09-21T02:28Z


    Generated by Claude Code

  9. removed their assignment
    on Sep 21, 2026
  10. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 1 — domain:ui execution seat
    Session: session_01BA3nKVUwKQJf8DBxrSVtNC
    Branch: claude/issue-10110-date-only-residual-measure
    Worktree: objectui-issue-10110
    Domain: domain:ui
    Seat: domain:ui#1
    File surface: measurement only — throwaway probes in the worktree, ⛔ no repair, no PR unless a pin is owed for a renderer outside objectui#10183's three sites (then packages/** of that renderer + its test + one .changeset/10110-…md) (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: opus (default judgement tier) — dispatch-gates.mjs --repo objectstack-ai/objectui --tier … answers verbatim 「dispatch-gates: REFUSING — asked for 'objectstack-ai/objectui', but this checkout is 'objectstack-ai/objectstack'.」 ⇒ no path-derived mandate; tier is this seat's judgement: naming which renderer draws each reported surface is judgement, and the zone trap (process.env.TZ in a thread pool is a silent no-op) has already produced one green-for-the-wrong-reason pin on this card.
    Clause-②: no
    Thread-read: 5754617994
    Serial constraints cleared: measurement writes nothing to the tree. Open PRs read 2026-09-24T06:07Z: objectui#8941 holds packages/plugin-detail/src/DetailView.tsx (one of objectui#10183's three sites) ⇒ fenced; siblings objectui#10180 / objectui#9909 hold plugin-form upload files and the machine-locale sites ⇒ fenced. objectui#10183's three sites (components/.../data-table.tsx, plugin-detail/.../DetailView.tsx summary chip, app-shell/src/utils/auditHistoryDisplay.ts) are ⛔ not repaired here: that card's own body rules them one mechanism with one repair.
    Earlier claims on this thread: 5754250232 (seat #1, session_01Xr7APep…) — released by its own Release: at 5754617994 after PR objectui#10182 merged as 516583b54; ⛔ no live claim is taken over.

    What this dispatch answers

    The predecessor's release says 「⛔ Do not close this card on the strength of the merge alone」 and asks for this card's own reproduction to be re-run on merged main. That is the whole deliverable: each of the three surfaces the card and its 2026-09-16 comment report (list cell, record-detail field, related-list grid), driven west of UTC on main, with the renderer that draws it named. ⇒ fixed everywhere ⇒ this seat closes the card; still shifting on an objectui#10183 site ⇒ this card waits on objectui#10183; still shifting elsewhere ⇒ that renderer is new, and this card owes it.

    Why Clause-②: no

    A measurement publishes nothing. The one conditional write (a renderer outside objectui#10183's list) is a presentation fix with no new export or option; ⛔ stop and report if it would need one.

    domain:ui seat #1 · session_01BA3nKVUwKQJf8DBxrSVtNC · claim · 2026-09-24T06:07Z


    Generated by Claude Code

  11. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 10110,
      "status": "done",
      "branch": "claude/issue-10110-date-only-residual-measure",
      "pr": null,
      "session": "session_01BA3nKVUwKQJf8DBxrSVtNC (subagent run: the parent PM session's id)",
      "premise_still_valid": false,
      "summary": "Measurement only, no repair and no PR. On today's origin/main (62597c5, unchanged at the end of the run), I drove the three surfaces the card reports through the console's own route components (app-shell ObjectView for the list, app-shell RecordDetailView for the record page). The data source serves the object schema, as ObjectStackAdapter does. The date-only values rendered the same calendar day in America/Los_Angeles and in UTC on every surface, including the relative branch. Every one of those cells is drawn by the shared DateCellRenderer, which passes the raw string to the repaired formatDate. Nothing is left on this card's three surfaces. One related-list case still shifts: when the child object's schema is unavailable, the related list falls back to data-table's formatCellValue, and that function belongs to objectui#10183 (site 1). I also found one side defect of the same mechanism outside objectui#10183's list: DateCellRenderer marks a due date of TODAY as overdue west of UTC. The text is right but it is coloured red. The fix needs a new core export, so I left it unrepaired and report it below for the seat to file. I recommend the seat close this card, with objectui#10183 carrying the rest.",
      "surfaces": [
        {
          "surface": "1 · list-view cell of a date field (the card's list cell for 周期开始 / 周期结束)",
          "host_chain": "app-shell ObjectView → plugin-view ObjectView → plugin-list ListView → object-grid (plugin-grid ObjectGrid) → data-table, taking its `col.cell` branch",
          "renderer": "packages/fields/src/index.tsx · DateCellRenderer. ObjectGrid attaches it as the column `cell` through `resolveGridCellRendering` (plugin-grid cellRendererResolution). It hands the RAW string to the shared formatDate with style 'relative'",
          "cases": "L1 string columns, L2 object columns, L3 no listViews (derived columns), L4 adapter schema read rejects. The same renderer drew all four",
          "west_text": "period_start 2026-08-01 → 'Aug 1'; relative: period_end 2026-08-31 on clock 2026-09-03T10:35Z → '3 days ago'",
          "utc_text": "'Aug 1'; relative '3 days ago'",
          "shifted": false,
          "owner": "none — correct on main"
        },
        {
          "surface": "2 · record-detail field display (planned_end 2026-11-16, reported as 11月15日)",
          "host_chain": "app-shell RecordDetailView → synthesized record page. The details body comes from record:details → plugin-detail DetailView → DetailSection (`displayValue` → getCellRenderer). The highlight strip comes from record:highlights → plugin-detail HeaderHighlight (getCellRenderer)",
          "renderer": "packages/fields/src/index.tsx · DateCellRenderer in both placements. The DetailView summary chip (objectui#10183 site 2) is NOT drawn on this page: zero `[data-summary-chip]` nodes in both zones, because record:details defaults showHeader to false",
          "west_text": "planned_end 2026-11-16 → 'Nov 16', in both the body row and the highlight strip; relative: planned_start 2026-09-16 on clock 2026-09-16T15:05Z (the comment's PDT morning) → 'Today'",
          "utc_text": "'Nov 16'; relative 'Today'",
          "shifted": false,
          "owner": "none — correct on main"
        },
        {
          "surface": "3 · related-list grid cell on a record page (period_month 2026-09-01, reported as 8月31日)",
          "host_chain": "app-shell RecordDetailView related tab → record:related_list → plugin-detail RelatedList (`makeCell` → getCellRenderer) → data-table `col.cell`",
          "renderer": "packages/fields/src/index.tsx · DateCellRenderer, when the adapter serves the child schema (the console's standard path)",
          "west_text": "2026-09-01 / 2026-10-01 / 2026-11-01 → 'Sep 1' / 'Oct 1' / 'Nov 1'",
          "utc_text": "'Sep 1' / 'Oct 1' / 'Nov 1'",
          "shifted": false,
          "owner": "none — correct on main"
        },
        {
          "surface": "3-degraded · the same related list when the child schema is unavailable (the FK declares relatedListColumns, and the adapter either has no getObjectSchema or its schema read rejects; both measured)",
          "renderer": "packages/components/src/renderers/complex/data-table.tsx · `formatCellValue`. The column has no `cell`, so data-table does Date.parse then `new Date(ts)` and passes that Date to formatDate. The caller stack recorded in-probe names data-table's `formatCellValue` frames",
          "west_text": "'Aug 31' / 'Sep 30' / 'Oct 31'; datetime control 'Jul 31, 2026, 08:00 PM'",
          "utc_text": "'Sep 1' / 'Oct 1' / 'Nov 1'; datetime control 'Aug 1, 2026, 03:00 AM'",
          "shifted": true,
          "owner": "objectui#10183 (site 1, data-table.tsx) — a further place that site shows up. Nothing new for this card"
        }
      ],
      "zone_moved_control": "Same probe, same run, for both zones: a datetime value 2026-08-01T03:00:00.000Z rendered '7/31/2026 8:00 pm' in America/Los_Angeles and '8/1/2026 3:00 am' in UTC, which are different calendar days. It rendered through DateTimeCellRenderer on surfaces 1-3 and through data-table formatDateTime on 3-degraded. Also read inside the fork: Intl resolvedOptions().timeZone = 'America/Los_Angeles' / 'UTC', getTimezoneOffset for 2026-08-01 = 420 / 0, and a rig case asserts the resolved zone equals the requested one (green in both).",
      "pm_assumptions": {
        "1_list_cell_via_data_table_preparse": "FALSIFIED for the console list. For a declared date field, ObjectGrid always attaches DateCellRenderer as the column cell. That held in all four list cases, including when the adapter's schema read rejects, where the type still comes from the `objects` metadata. data-table's pre-parse was reached only by RelatedList with no child schema (3-degraded).",
        "2_zone_trap": "CONFIRMED and EXTENDED. vitest.config.mts sets `process.env.TZ = 'UTC'` in its module scope, and resolving the project configs re-runs that line. So a wrapper config that sets TZ in its own module scope is overwritten: I measured it, and the fork read 'UTC'. What worked: a globalSetup that re-sets TZ in the main process after config resolution, plus `--pool=forks`, so each fork starts with TZ in its environment. The datetime control above proves the zone moved.",
        "3_surface2_path": "CONFIRMED that it is the fields date cell (DateCellRenderer). It is reached through DetailSection for the details body and HeaderHighlight for the highlight strip. It is not the summary chip, which does not render on the console record page."
      },
      "tests": "Throwaway probes only, now deleted: two RTL files under packages/app-shell/src/views plus a wrapper vitest config and globalSetup at the worktree root. Each zone ran as a single locked command from the worktree root: `OS_PROBE_TZ=ZONE pnpm exec vitest run --config vitest.probe10110.config.mts --pool=forks --maxWorkers=2 --project dom PROBE_FILES`, through `bash /home/user/objectstack/scripts/pm/os-verify-lock.sh -c ...`. Final run, America/Los_Angeles: 'Test Files 2 passed (2) / Tests 11 passed (11)', `os-verify-lock: VERDICT command-exit 0`. Final run, UTC: the same counts, `VERDICT command-exit 0`. Readings were written as JSON lines to scratchpad files, because the dom setup does not surface console output. Attribution: a vi.mock of @object-ui/core wrapped formatDate, formatRelativeDate, formatDateTime and formatDateTimeCompactParts. Each wrapper recorded its argument and caller stack, then returned the actual function's output unchanged. The recorded outputs match the rendered DOM text. Frozen clocks came from vi.useFakeTimers({ toFake: ['Date'] }): the card's 2026-09-03T10:35Z for the list and the HotCRM comment's 2026-09-16T15:05Z for the record page. Display locale was 'en', the fallback, not the card's zh-CN; the shift is a calendar-day property, so the face language does not change the verdict. The first zone attempt, which set TZ in the wrapper config's module scope, read 'UTC' inside the fork and is discarded as NOT MEASURED (rig failure). It is not reported as a result. No ablation: nothing was repaired.",
      "gates": "None run and none owed: no repair shipped, and the branch has no commits (its remote head equals origin/main 62597c5). NOT MEASURED: check:control-bytes and check:new-line-citations. The dispatch makes them conditional on a repair.",
      "files_changed": [],
      "git_status_porcelain_lines": 0,
      "cleanup": "Probes, the wrapper config, the globalSetup and a local fetch ref (refs/probe10110/main) were all deleted. `git status --porcelain` read 0 lines. The worktree was removed without --force, after node_modules. The remote branch claude/issue-10110-date-only-residual-measure was pushed empty as the claim marker and points at 62597c5. Agents cannot delete remote refs, so it is left for the seat.",
      "mcp_calls": "0 — no MCP GitHub tool was called, read or write.",
      "api_writes": "1 REST write plus 1 git push. The git push was `git push -u origin claude/issue-10110-date-only-residual-measure` (empty branch; not a REST write). The REST write is this report: POST /repos/objectstack-ai/objectui/issues/10110/comments, through scripts/pm/post-stamped.mjs. No PR, no label write, no assignee write. Reads: GET issue 10110 and 10183 plus their comment lists, and GET /rate_limit.",
      "open_questions": [],
      "out_of_scope_findings": [
        "class: a · DateCellRenderer (packages/fields/src/index.tsx) colours a due-semantic date-only value dated TODAY as overdue for viewers west of UTC. `isOverdueInstant(date, dueLike)` is fed `new Date(safe)`, which is UTC midnight, and compares it with LOCAL start-of-day. The text is correct: the shared formatDate prints 'Today'. Evidence from the list probe: due_date 2026-09-03 on clock 2026-09-03T10:35Z. America/Los_Angeles: 'Today' with class 'tabular-nums text-red-600'. UTC: 'Today' with class 'tabular-nums', not red. The result was the same in list cases L1-L4. The same renderer draws DetailSection, HeaderHighlight and RelatedList date cells, but I did not probe those placements for this. This is outside objectui#10183's three sites and is the same mechanism: the value is re-parsed outside the shared calendar-day parse. I did not repair it here. A repair through the shared @object-ui/core path needs the module-private `toDisplayDate` (a new export, which the dispatch says to stop and report). The other route is a local re-parse, which brings back the two conventions that objectui#10182 removed. Suggested disposition: sub-issue of objectui#10183 (fourth site, same single repair). The predecessor's report called this instant tooltip-only and 'cosmetic'; the overdue half is not cosmetic. Dedupe words: DateCellRenderer overdue today · isOverdueInstant date-only UTC midnight · dueLike text-red-600 west of UTC · due date red on its own day",
        "carrier: objectui#10183 (site 1) · noted, not filed — RelatedList cells fall back to data-table `formatCellValue` whenever the child object's schema is unavailable (an adapter without getObjectSchema, or a rejected schema read, with relatedListColumns declared), and west of UTC they then shift. Measured: 2026-09-01/10-01/11-01 → 'Aug 31'/'Sep 30'/'Oct 31' (LA) vs 'Sep 1'/'Oct 1'/'Nov 1' (UTC). The objectui#10183 repair to data-table covers this case too; its pin can include it.",
        "carrier: whoever writes the objectui#10183 zone pin · noted, not filed — a test-rig trap. Because resolving the project configs re-runs the root vitest.config.mts line `process.env.TZ = 'UTC'`, even a wrapper config that sets TZ in its own module scope ends up at UTC inside the workers (measured: the fork read 'UTC'). A globalSetup re-pin plus --pool=forks, or the child-process pin already used in date-display.dateOnlyZone-10110.test.ts, does move the zone."
      ]
    }

    Generated by Claude Code

  12. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    ✅ Closed completed — the card's own reproduction, re-run on merged main, no longer shifts on any surface it reports

    domain:ui seat #1, session_01BA3nKVUwKQJf8DBxrSVtNC. Review of measurement report 5808905560 (no PR, as dispatched). ⛔ Not closed on the strength of the objectui#10182 merge alone. This is the re-run the release 5754617994 asked for.

    surface this card reports renderer that draws it on origin/main 62597c5 America/Los_Angeles UTC
    list cell (「周期开始 → 7月31日」) DateCellRenderer via ObjectGrid's column cell Aug 1 Aug 1
    relative branch (「4天前」) same 3 days ago 3 days ago
    record-detail field (「11月15日」) DateCellRenderer via DetailSection / HeaderHighlight Nov 16 Nov 16
    related-list grid (「8月31日」) DateCellRenderer via RelatedList Sep 1 Sep 1

    The zone really moved: a datetime control rendered different calendar days in the two zones in the same run. This seat re-read the deciding line on main: DateCellRenderer passes the raw string to the repaired formatDate.

    ⇒ premise_still_valid: false for what remains here: this card's surfaces are fixed by PR objectui#10182 (516583b54). The predecessor's lead — that the list cell goes through data-table's pre-parse — is falsified for the console list.

    What is left, and where it lives (⛔ not here)

    • A related list whose child schema is unavailable falls back to data-table's formatCellValue and still shifts. That is objectui#10183 site 1, and is recorded there (5808988744).
    • DateCellRenderer colours a date-only due date of TODAY as overdue west of UTC (the text is right). Same mechanism, fourth site, recorded on objectui#10183 (5808988744).
    • ⚠️ The dev's empty branch claude/issue-10110-date-only-residual-measure stays on origin (this container cannot delete refs). It is ⛔ not a live claim.

    Closing in one act: pm:dispatched stripped, assignee cleared, completed.


    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

No one assigned

    Labels

    bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions