You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
console: the localization fetch drops the timezone the server sends for the signed-in user, so no renderer can show dates in the user's or tenant's zone #11693
Ruled: 6030273339 · letter B · 2026-10-07T03:28Z
Blocked-by: #11758
Unlock-action: re-check PR #11729
Path: ② the capabilities an end user meets in the app — localization | 缺项 (no item asserts a date renders in the user's configured zone) | P2
Filed by the triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U. It is the follow-up named in the ruling on objectui#11675 (B: formats follow the locale; timezone stays a live setting). ⛔ Not a claim, ⛔ not a dispatch.
Triage: lands in apps/console/src/LocalizationFetchProvider.tsx (about :108) and packages/i18n/src/LocalizationContext.tsx (LocalizationValue) ⇒ domain:ui; rationale: the server answers locale, timezone and currency from /api/v1/auth/me/localization (objectstack plugin-hono-servercurrent-user-endpoints.ts, #15387), and the console keeps only two of the three.
Read on objectui main
setValue({ currency: json.currency ?? undefined, locale: json.locale ?? undefined }): the timezone in the same answer is dropped.
Done when
Measure first: whether any renderer already takes a zone from elsewhere (the session, or a per-field zone). Then the console carries the served timezone in its localization context.
The central date formatters render in that zone when it is set, and the browser's zone stays the fallback when it is not.
⛔ No per-component special cases. The zone enters through the central formatters only.
Pins: with the user's timezone set to a zone unlike the browser's, a datetime renders in the set zone; with none set, it renders in the browser's.
Claim: PM loop round 1
Session: session_01CGZy1BGCjdN5cXqL9cnvB8
Account: os-support-ai
Branch: claude/issue-11693-served-timezone
Worktree: objectui-issue-11693
Domain: domain:ui
Seat: domain:ui#3
File surface: apps/console/src/LocalizationFetchProvider.tsx (the setValue that keeps currency and locale only, about :108); packages/i18n/src/LocalizationContext.tsx (LocalizationValue); the central date formatters in packages/i18n/src/utils/formatting.ts and packages/i18n/src/utils/spec-formatters.ts, and the hook or context reader that hands them the localization value, only as much as a zone parameter needs; the tests beside them; and .changeset/11693-*.md (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: opus (dispatch-gates --tier --repo objectstack-ai/objectui: no path-derived mandate; default tier)
Clause-②: yes
Thread-read: none
Ruling-ref: 6010771237 (objectui#11675 ruling B, the maintainer's 「同意」 recorded by the triage seat), retrieved this round.
Serial constraints cleared: none blocking. Open PRs read: objectui#11719, #11069 and the release PR objectui#11600; none touches these files. Same axis (area:i18n): seat 2's in-flight objectui#11689 declares packages/fields/src/widgets/*, HomePage.tsx and the locale packs, disjoint from this surface. Read 2026-10-06T14:29Z.
Why Clause-②: yes: LocalizationValue is exported from @object-ui/i18n, and carrying timezone on it (and a zone input on the exported formatters) widens a published type: a minor changeset.
os-dev-report
{
"issue": 11693,
"status": "needs_decision",
"branch": "claude/issue-11693-served-timezone",
"pr": "#11729",
"session": "session_01CGZy1BGCjdN5cXqL9cnvB8 (subagent: the parent's id)",
"premise_still_valid": true,
"summary": "Implemented and pushed as draft PR #11729 (Part of #11693). The console keeps the timezone that GET /api/v1/auth/me/localization answers. LocalizationValue carries it, and LocalizationProvider hands it to a new display zone in @object-ui/core date-display (setDisplayTimeZone / getDisplayTimeZone / subscribeDisplayTimeZone). Every date and datetime face reads that zone for instants. A date-only value keeps its day: toDisplayDate registers the calendar-day Dates it builds, so the exemption survives hand-offs such as GridField's date cell. With no zone, every face is unchanged. Measurement moved the landing point from the dispatch's i18n formatters: every console datetime call site renders through core's date-display, threading only { locale }, while i18n formatting.ts and formatDateSpec (which already takes a timeZone) have no caller outside @object-ui/i18n. Zone 2 hypothesis 3 is falsified as a route. The core premise holds (the fetch did drop timezone), but two sub-premises do not. First, the served zone is the WORKSPACE default (localization.timezone; sys_user has no zone, and per-user overrides are out of scope for v1). Second, the wire never says 'none': the cascade floors timezone to UTC. Status needs_decision because the merge is gated on that floor. Merged alone, this PR moves every unconfigured workspace's datetimes from the viewer's zone to UTC. The PR is complete and identical under options A, B and D (D adds one line). It opens with 'Part of #11693' so that merging does not close the card before the ruling; under B, the seat closes the card after the merge.",
"tests": "All cited at HEAD 229db2b. Final union: pnpm exec vitest run packages/core/ packages/i18n/ apps/console/src/LocalizationFetchProvider.test.tsx apps/console/src/__tests__/localizationBootDedup.test.tsx gave 'Test Files 281 passed (281) / Tests 5257 passed | 40 skipped' (VERDICT command-exit 0). This includes date-display.dateOnlyZone-10110, whose real TZ=America/Los_Angeles and TZ=Asia/Shanghai children measure the rewritten relative-day arithmetic unchanged. New pins: core date-display.displayZone-11693 (16), i18n LocalizationContext.timezone-11693 (5), console LocalizationFetchProvider (2 new; 14 in the file). Los_Angeles set: 2026-09-02T03:00Z renders 'Sep 1, 2026, 08:00 PM'. None set: 'Sep 2, 2026, 03:00 AM'. A date-only 2026-09-01 renders 'Sep 1' on every face under the west zone. Control: the same instant as a plain Date renders 'Aug 31'. Consumers: 168 files (every mount of LocalizationProvider or LocalizationFetchProvider, plus the date and datetime suites of fields, components, plugin-grid, plugin-gantt, plugin-dashboard and others) gave '82 passed | 2 skipped' and '83 passed | 1 skipped', 0 failed. type-check: @object-ui/core and @object-ui/i18n exit 0. The console type-check was narrowed: the two touched console files and the two README snippets compile with the console tsconfig against built core, i18n and auth, exit 0. Reverse check: a misspelt key gives TS2353. Ablations via ablation-replace.mjs, each restored with blob == HEAD and git diff HEAD empty: (1) console drops timezone: 1 failed / 13 passed. (2) toDisplayDate stops registering: 4 failed / 12 passed, reading 'Aug 31' / 'Yesterday'. (3) toInclusiveEndDay stops registering: 1 failed / 15 passed. (4) default datetime face takes no zone: 4 failed / 17 passed. Its first attempt was refused by the tool and was a no-op, because the replacement was a substring of the anchor; it was rerun with a marker. (5) provider does not hand the zone to core: 4 failed / 15 passed. (6) context keyed on the prop zone (the ordering hazard): 3 failed / 2 passed, showing 'America/Los_Angeles|Sep 2, 2026, 03:00 AM'. (7) i18n hands setDisplayTimeZone a number: TS2345, so the type-check reads core's rebuilt d.ts. Rebuilds: the core closure (types, core), then i18n, auth and its closure, before any type judgement.",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "3 relayed REST writes, each sent as one POST /repos/objectstack-ai/objectstack/dispatches (fleet-write; 3 dispatches): pr_create = POST /repos/objectstack-ai/objectui/pulls (draft #11729, read-back identical 12636 bytes); label-write assign = POST /repos//issues/11729/assignees (os-support-ai, read-back matches); os-dev-report comment = POST /repos//issues/11693/comments (via post-stamped.mjs). git push is not counted (4 pushes of one branch). 0 label adds.",
"open_questions": [
{
"question": "The served timezone floors to UTC for a workspace that configured none (objectstack resolveCurrentUserLocalization: 'Always a string — the cascade's floor is UTC', pinned by a test; the settings row is 'Default timezone', default 'UTC', described as governing 'rendered datetimes'). The card wants 'with none set, it renders in the browser's', and the wire can never say none. Merged alone, PR #11729 moves every unconfigured workspace's datetimes from the viewer's zone to UTC. Which contract holds?",
"options": [
"A — the server says none: the endpoint answers timezone null when only the default answered (the settings service reports source 'default'). Server TODAY() and buckets keep the UTC floor, and unconfigured workspaces keep the viewer's zone. Cost: an objectstack change that reverses the endpoint's pinned 'never null' for timezone, plus its tests. Client: #11729 unchanged.",
"B — keep the floor: the configured-or-default zone governs rendered datetimes, as the settings row already declares. Display, TODAY() and analytics buckets share one zone. Cost: unconfigured workspaces render in UTC until an admin sets the zone. Client: #11729 unchanged; it completes the card.",
"C — the client reads 'UTC' as unset: refused. A workspace that really chose UTC renders in the viewer's zone, and it is consumer-side tolerance (objectui AGENTS.md #0.1, os-dev rule 5).",
"D — a separate wire signal (for example timezoneSource): additive. Cost: one more field on the wire, an objectstack change, and one line in the console fetch provider."
],
"recommendation": "B. Real business need: the only measured declaration (the settings row's description) says this zone governs rendered datetimes. Who configures it in practice is not measurable from here, and B's cost lands exactly on unconfigured workspaces, which is A's argument. Long-run fit: B leaves one reference zone for display, TODAY() and buckets, the Salesforce default-time-zone shape the locale ruling invoked. A keeps a client-only second zone that server-side features cannot follow. AI error-proofing: B has one declared meaning and no 'null means the browser' rule for an author or a server-side renderer to miss. Startup focus: B needs no further work; A and D need a server change and a wire or contract change. If UTC display on unconfigured workspaces is unacceptable, the lever is the default (provisioning sets localization.timezone, or the manifest's default), not a client fallback; that is a separate decision. Choose A if the unconfigured workspace's experience outweighs one shared zone."
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed — FAMILY: datetime renderers outside the shared core faces do not take the display zone. In a configured workspace they show browser-zone times beside zone-rendered cells (record meta footer, comments, history and activity timelines, audit log, calendar views, gantt chart frame beside its central-face tooltip). Would be class b: Seam: spec:localization.timezone (service-settings manifest row, declared to govern rendered datetimes) → renderer: RecordMetaFooter / AuditLogPage / CommentThread / GanttView frame / CalendarView. reach: NOT measured at a public door this run (no browser stack), so it is not fileable as is; one family closing card if measured. Instrument: git grep -n -E '\.toLocaleString\(|\.toLocaleTimeString\(|new Intl\.DateTimeFormat\(|\.toLocaleDateString\(' over non-test packages//src and apps/console/src (113 lines in about 55 files on 7300fca; over-counts, because toLocaleString also matches numbers). Dedupe words: display zone, toLocaleString, workspace timezone, datetime bypass, setDisplayTimeZone.",
"carrier: 承接者:无 · noted, not filed — core FormulaFunctions TODAY() still answers the UTC day. Its docblock names 'a later card' for 'once the timezone reaches the client'; the zone now does (getDisplayTimeZone). Dedupe words: TODAY reference timezone, FormulaFunctions UTC day, client formula zone.",
"carrier: 承接者:无 · noted, not filed — pre-existing, order-dependent: scripts/tests/date-only-zone-pins-10183.test.ts reds on FormulaFunctions.dateOnlyZone-10866 ('expected 2026-09-01 11:00 to be 2026-09-01 03:00'). The unit project runs isolate:false, so a pin file that leaves TZ east of UTC leaks into the next file in a shared fork when vitest's results cache orders it first. Control legs at 7300fca: fresh cache 27/27 green; this branch's cache copied in, same failure. CI starts cache-less, so it is likely invisible there. Not caused by this diff. Dedupe words: TZ leak forks, date-only-zone-pins-10183, isolate false process.env.TZ.",
"carrier: 承接者:无 · noted, not filed (zero pull) — @object-ui/i18n formatDate / formatDateTime / formatRelativeTime / formatDateSpec are a second published date-formatter family with no renderer caller in this repo, and they do not read the display zone (the i18n README now says so). Dedupe words: i18n formatting duplicate, formatDateSpec callers.",
"carrier: 承接者:无 · noted, not filed — the vitest.config.mts header's 'the one zone-aware surface that exists takes its zone from METADATA' is now stale prose (comment only). Dedupe words: vitest timezone pin header."
],
"gates": [
{
"command": "pnpm exec vitest run packages/core/ packages/i18n/ apps/console/src/LocalizationFetchProvider.test.tsx apps/console/src/tests/localizationBootDedup.test.tsx (lock)",
"exit": 0,
"verdict": "Test Files 281 passed (281); Tests 5257 passed | 40 skipped; VERDICT command-exit 0"
},
{
"command": "pnpm exec vitest run (168 consumer files, half A) (lock)",
"exit": 0,
"verdict": "Test Files 82 passed | 2 skipped (84); Tests 996 passed | 120 skipped"
},
{
"command": "pnpm exec vitest run (168 consumer files, half B) (lock)",
"exit": 0,
"verdict": "Test Files 83 passed | 1 skipped (84); Tests 1105 passed | 75 skipped"
},
{
"command": "pnpm exec vitest run scripts/tests/date-only-zone-pins-10183.test.ts",
"exit": 1,
"verdict": "2 failed | 25 passed — pre-existing order-dependent TZ leak; control at 7300fca fails identically with the same cache (see out_of_scope_findings)"
},
{
"command": "pnpm --filter @object-ui/core type-check",
"exit": 0,
"verdict": "0 TS errors (script echoed: @object-ui/core type-check)"
},
{
"command": "pnpm --filter @object-ui/i18n type-check",
"exit": 0,
"verdict": "0 TS errors (script echoed: @object-ui/i18n type-check)"
},
{
"command": "console narrowed tsc (console tsconfig; 2 console files + 2 README snippets; built core/i18n/auth)",
"exit": 0,
"verdict": "exit 0; reverse check TS2353"
},
{
"command": "pnpm exec eslint (6 touched source/test files)",
"exit": 0,
"verdict": "0 errors, 2 warnings, both on lines that predate the branch"
},
{
"command": "pnpm check:control-bytes",
"exit": 0,
"verdict": "check-control-bytes: OK (scanned 7670 tracked text file(s); skipped 85 binary)"
},
{
"command": "pnpm check:test-path-roots",
"exit": 0,
"verdict": "check-test-path-roots: OK"
},
{
"command": "pnpm check:changeset-claims",
"exit": 0,
"verdict": "No pending changeset names a file this change touches. Corpus: 1 changeset(s)"
},
{
"command": "pnpm check:pending-changeset-literals",
"exit": 0,
"verdict": "No test source names a pending changeset."
},
{
"command": "pnpm check:new-line-citations",
"exit": 0,
"verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0"
},
{
"command": "pnpm check:doc-fences",
"exit": 0,
"verdict": "every TypeScript block in 227 document(s) is fenced"
},
{
"command": "pnpm check:doc-example-ids",
"exit": 0,
"verdict": "415 real reference(s) all resolve in the catalog registry."
},
{
"command": "pnpm check:doc-example-readers",
"exit": 0,
"verdict": "OK 80 documented symbol(s) ... no @example hand-spells one."
},
{
"command": "pnpm check:doc-types",
"exit": 0,
"verdict": "Every documented component type is registered."
},
{
"command": "pnpm check:phantom-deps",
"exit": 0,
"verdict": "Every in-scope import is declared by the package that publishes it."
},
{
"command": "pnpm check:self-import",
"exit": 0,
"verdict": "No package names itself inside its own src/."
},
{
"command": "node scripts/check-changeset-presence.mjs",
"exit": 0,
"verdict": "6 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)"
},
{
"command": "node scripts/check-governed-queue-guard.mjs --test (10 changed paths)",
"exit": 0,
"verdict": "NOT GOVERNED — 10 path(s) checked against 5 governed surface(s); none matched."
},
{
"command": "pnpm check:readme-exports",
"exit": 1,
"verdict": "NOT MEASURED — 531 self-imports could not be judged (32 packages unbuilt); the core and i18n READMEs filtered from --json: 0 findings (control: app-shell 22)"
},
{
"command": "pnpm check:doc-snippets",
"exit": 2,
"verdict": "NOT MEASURED — THE GATE COULD NOT RUN (unbuilt packages); the new README snippets were compiled ad hoc instead"
},
{
"command": "pnpm check:doc-examples",
"exit": 2,
"verdict": "NOT MEASURED — THE GATE COULD NOT RUN (unbuilt packages)"
},
{
"command": "pnpm check:i18n-keys / i18n-drift / i18n-dead-keys / i18n-designer-parity",
"exit": null,
"verdict": "not run — no locale pack changed"
}
],
"deviations": [
"Landing point outside the claim's declared file surface: packages/core/src/utils/date-display.ts (the measured producer of every rendered datetime), plus docs (core and i18n README, content/docs/fields/datetime.mdx) owed under objectui AGENTS.md #2. Authorized by the dispatch's producer-side clause; the i18n formatting.ts and spec-formatters.ts files were not edited.",
"Mechanism differs from Zone 3's suggested route: an ambient display zone in core, read by every face, instead of handing a timeZone to formatDateSpec, which has no renderer caller. Threading an option per call site was not done: about 20 call sites, and packages/fields/src/widgets/ is held by objectui#11689.",
"PR opens with 'Part of #11693', not a closing keyword, because the merge is decision-gated (open_questions). Under B, the seat closes the card after the merge.",
"Declared narrowing: the console type-check and the readme-exports, doc-snippets and doc-examples gates need every package built (CI builds them); replaced by a narrowed tsc plus a README-filtered readme-exports reading, each with a control.",
"The verify lock timed out twice (exit 99, holder issue-11696 suites.sh / suites2.sh); each time I retried with the same OS_VERIFY_LOCK_SLOT, and both later runs completed.",
"A temporary detached worktree at 7300fca (objectui-11693-basectl) was created for the zone-pins control leg and removed afterwards.",
"The relay needs an actions file: the PR body and actions JSON were written under the issue-specific scratch subdirectory (issue-11693/pr/, issue-prefixed names) and read back before sending. The probe push log was briefly written to /tmp/claude-0 before being moved there.",
"Commit trailers follow the model-free pair (Claude-Session + Co-authored-by: Claude) per objectui AGENTS.md, not the harness's model-named trailer."
],
"files_changed": [
".changeset/11693-served-timezone.md",
"apps/console/src/LocalizationFetchProvider.tsx",
"apps/console/src/LocalizationFetchProvider.test.tsx",
"content/docs/fields/datetime.mdx",
"packages/core/README.md",
"packages/core/src/utils/date-display.ts",
"packages/core/src/utils/tests/date-display.displayZone-11693.test.ts",
"packages/i18n/README.md",
"packages/i18n/src/LocalizationContext.tsx",
"packages/i18n/src/tests/LocalizationContext.timezone-11693.test.tsx"
],
"line_budget": "not applicable (no skills/** surface)"
}
objectstack packages/services/service-settings/src/manifests/localization.manifest.ts:47-48:label: 'Default timezone',default: 'UTC',描述 "IANA zone used to resolve today()/daysFromNow, analytics date buckets, and rendered datetimes."
objectstack packages/plugins/plugin-hono-server/src/current-user-endpoints.ts:612:"The reference time zone. Always a string — the cascade's floor is UTC."
ADR-0053(Accepted)§7:"datetime rendering uses the reference timezone at the presentation boundary";Phase 2 设计表 Render-tz 一行:"show this UTC instant in the viewer's zone",来源 "the viewer";Rollback 段:"With no org reference timezone configured, the resolver returns 'UTC' and all compute/render paths are byte-for-byte today's behavior"。
ADR-0053 附录 D-G1(objectui#10903 维护者裁决 A):客户端 TODAY() 取参考时区的那一天,"Not taken: the viewer's local day"。
冲突点:设置清单说"默认 UTC 管渲染";ADR 的回退保证说"未配置时渲染与今天逐字节相同"(控制台今天按浏览器时区显示)。两份文本对"未配置工作区显示什么"给出相反答案,所以呈裁。
Ruling: batch #283 item 3 · letter B (B2, gated by #11758 in the prompt-and-confirm shape) · maintainer 「总监决裁」 2026-10-07T03:26Z
Director seat, summon #35, session_01VYToj6PQehTEKNrjGM9akg (GitHub os-zhuang; written as objectstack-fleet[bot] via the relay). Batch #283 was presented in chat with this card's options A / B (B1 immediate, B2 after a gate) / D from the domain:ui seat's request 6020424601. The maintainer asked, instead of a letter, whether an administrator's sign-in should set the workspace's default timezone from the browser's zone when none is configured; the seat answered in the decision form (silent write / prompt-and-confirm / nothing) and recommended prompt-and-confirm; the maintainer answered 「总监决裁」, delegating the letter to the seat. The seat takes its own recommendation and records it as a seat ruling on the maintainer's delegation. Freshness gate: the body and all three comments (newest 6020424601) were re-read before this record; nothing newer exists. Thread-read: 6020424601.
The ruling
B. The server's contract stands: /api/v1/auth/me/localization answers timezone as a string with the UTC floor (objectstack current-user-endpoints.ts:612; the manifest default localization.manifest.ts:47). PR #11729 lands as built: the console carries the server's timezone into every datetime rendering, date-only values do not shift, and a missing timezone leaves today's behavior. ⛔ Not taken: A (timezone: null for a defaulted workspace, which reverses the pinned "always a string" and leaves the viewer's local day beside TODAY()'s and the report buckets' UTC day, the split ADR-0053 D-G1 already refused), D (a timezoneSource field: the same split with one more wire field) and C (the console reading UTC as "unset": a workspace that chose UTC mis-rendered).
B2: the merge waits on #11758. Before #11729 lands, the console asks an administrator ONCE, when localization.timezone is still the manifest default, whether to set the workspace timezone to the browser's zone, pre-filled; confirm writes through the Settings page's own write path (same permission, same audit), decline writes nothing. ⛔ Not taken: the silent write on sign-in. The first administrator's browser would decide the organization's clock with nobody told, and a traveling administrator, a VPN or the platform owner signing in to a customer workspace for support would set it wrong, for TODAY(), report buckets and rendered datetimes alike.
Readings the seat took for the gate's shape, each re-checkable on #11758:
objectstack-ai/cloud provisioning writes no localization.timezone, so every cloud workspace is unconfigured today: B1 would switch every cloud user's display to UTC until an administrator acts. That is why the gate comes first.
The settings service already tells "default" from "chosen" (ResolvedSettingValue.source and cascadeChain; the settings GET returns them, objectstack settings-routes.ts:155) and the console's SettingsField.tsx:72-89 already reads that cascade, so the gate needs no objectstack change and no new key.
The console reads no browser zone anywhere today; the prompt's pre-fill is the one new reader.
Recorded gap, not ruled:sys_organization.timezone (objectstack, the #14238 ruling A) exists beside localization.timezone, and the localization cascade reads only the settings key: two homes for one fact. The triage seat may file it when it next touches the cascade; no card is filed from this ruling.
Unlocked: pm:blocked → pm:dispatched. The B2 gate closed; PR objectui#11729 is re-checked
domain:ui seat 3 · session_01CGZy1BGCjdN5cXqL9cnvB8 · 2026-10-07T05:33Z. This is the Unlock-action: re-check PR #11729 this card's body names. The claim 6018498275 and the assignee stand.
The blocker closed.Blocked-by: #11758 names objectui#11758, the one-time administrator prompt that the director's ruling 6030273339 (B2) made the gate. It closed at 2026-10-07T05:29Z, when PR objectui#11761 merged as 3bf8894. The landed note is 6031649859.
PR objectui#11729 re-checked on objectui main3bf8894:
head 229db2b, draft, 10 files, +628/−33, first line Part of #11693, Clause-②: yes;
CI on that head: 43 runs, 40 success, 3 skipped, 0 failure;
its merge-base is 7300fca, 30 commits behind main. None of those commits touches any of the PR's 10 files, and git merge-tree of main with the head is clean;
its body already names the gate and this sequence.
Next, in order:
the at-tier contract review that Clause-②: yes owes;
the seat's ACCEPT;
ready + auto-merge through the queue.
The PR is Part of, so the seat closes this card after the merge, with the landing note.
PR shape: draft against main. First line Part of #11693, no closing keyword, so the seat closes this card after the merge. Clause-②: yes at line start. The body names the B2 gate, which closed with objectui#11758 (3bf8894). The merge-base is 7300fca, 30 commits behind main. None of those commits touches the PR's 10 files, and git merge-tree with main3bf8894 is clean. Mergeable state clean.
Scope: 10 files, +628/−33. No governed path.
LocalizationFetchProvider.tsx and its test;
packages/core/src/utils/date-display.ts and a new pin;
packages/i18n/src/LocalizationContext.tsx and a new pin;
the core and i18n READMEs;
content/docs/fields/datetime.mdx ("Time Zone");
the changeset.
The landing point is core's date-display.ts, not the i18n formatters the dispatch named. The dev measured that the i18n formatters have no renderer caller. That is within the card's "the central date formatters", the claim's stop-and-explain clause was honoured, and the contract review judged it right (①.10).
Ruling honoured: director's ruling 6030273339, letter B with gate B2. The zone is whatever the endpoint answers, the UTC floor included, and the gate (a one-time administrator prompt) landed first. Nothing in this PR changes for the ruling.
Diff read (the seat's own):
The fetch keeps timezone: json.timezone ?? undefined beside currency and locale.
LocalizationProvider hands the zone to core in a layout effect whose cleanup clears it. It reads the applied zone back through useSyncExternalStore, so useLocalization().timezone is the zone the faces actually render in, and an unknown IANA name reads undefined.
Core gains setDisplayTimeZone / getDisplayTimeZone / subscribeDisplayTimeZone. An unknown name clears the zone with one console.warn instead of every face throwing RangeError.
Each instant face spreads inZone(zone); with no zone the options bag is unchanged.
Date-only values stay exempt through a WeakSet registered on toDisplayDate's date-only branch and in toInclusiveEndDay.
No per-renderer special case.
Changeset sentences checked against the head:
the zone the server answers;
date-only keeps its day;
no zone means the viewer's zone, as before;
a server answering UTC for an unconfigured workspace moves it to UTC;
the three core symbols and LocalizationValue.timezone;
renderers with their own Intl options don't take the zone yet.
minor on @object-ui/core, @object-ui/i18n and @object-ui/console, matching Clause-②: yes.
Reverse verification (dev report 6020328604):
the 16 core pins straddle the suite's UTC (America/Los_Angeles, Asia/Shanghai);
the console's "zone arrives after first render" case;
the ablation table, where row 6 (the context keyed on the prop instead of the applied zone) proves the read-back keying is load-bearing.
Gates: CI on 229db2b (head_sha equals the PR head): 43 runs, 40 success, 3 skipped (dependabot and the two coverage jobs), 0 failure. Type Check, README Export Check, Doc Snippet Type Check, Doc Example Id Check and Build Docs are green, covering the dev's narrowed local gates.
Contract review:6031806317, served at CONTRACT_REVIEW_TIER, same head, PASS.
All 12 ① judgments are right.
② semver matches.
③ is answered item by item, including a read of the 30-commit main delta: none changes what the pins assert.
Open questions:open_questions[0] (the UTC floor) was ruled by 6030273339.
Findings (the dev's five, plus the review's flags):
datetime renderers outside the shared core faces (record meta footer, comments, history and activity timelines, audit log, calendar views, gantt chart frame) do not take the zone. The card's ⛔ leaves them and the changeset names the limit. No public-door measurement was made → Acceptance notes (one family card once measured)
core FormulaFunctionsTODAY() still answers the UTC day. ADR-0053 D-G1 wants the reference zone's day, and the zone now reaches the client (getDisplayTimeZone). No measured wrong result yet → Acceptance notes (dedupe words: TODAY reference timezone, FormulaFunctions UTC day)
first paint: the fetch provider renders before the answer, so a configured workspace's datetimes paint in the viewer's zone and then move to the served zone on each load. The locale has a seed for this gap and the zone has none. This was pinned deliberately, as before → Acceptance notes
after an administrator confirms the objectui#11758 prompt, the running session keeps the zone it was served until the next load. That is within the cost ruling B2 states → Acceptance notes
scripts/__tests__/date-only-zone-pins-10183.test.ts reds when a cached run orders a TZ-leaving pin file first (isolate: false). CI starts cache-less and stays green. Pre-existing → Acceptance notes
the @object-ui/i18n formatter family has no renderer caller and does not read the zone, which the i18n README now states; the vitest.config.mts header prose is stale → Acceptance notes
Landing: ready + auto-merge through the queue, now. After the merge the seat closes this card (Part of) with the landing note.
Landed — domain:ui seat 3, session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-07T06:04Z. PR objectui#11729 merged through the queue as d50f724 at 2026-10-07T06:03Z. This card closes with this note: the PR was Part of, and nothing of the card's Done-when remains.
The merge:
One parent, 92f4e2b, an ancestor of origin/main.
The landed tree equals git merge-tree of that parent and the accepted head 229db2b (tree 5506d63b on both).
10 files, +628/−33.
Content check, 92f4e2b → d50f724:
export function setDisplayTimeZone in core's date-display.ts: 0 → 1.
timezone: json.timezone in LocalizationFetchProvider.tsx: 0 → 1.
setDisplayTimeZone(timezone) in LocalizationContext.tsx: 0 → 1.
Control: the B2 gate's <WorkspaceTimezonePrompt /> in AppContent.tsx is 1 on both sides, so the gate stands in front of the change it guards.
Ruling closed out: director's ruling 6030273339, letter B with gate B2. The gate landed first as objectui#11758 (3bf8894); this PR followed. Its contract review is 6031806317 (PASS) and the ACCEPT is 6031829629.
Closures: objectui#11693 is closed here as completed, and pm:dispatched is removed in the same relay call. No other issue closed in the window. The follow-up candidates are recorded in the ACCEPT's Acceptance notes and none is filed, because none has a measured reach yet:
Activity
objectstack-fleet commentedon Oct 6, 2026
Claim: PM loop round 1
Session:
session_01CGZy1BGCjdN5cXqL9cnvB8Account:
os-support-aiBranch:
claude/issue-11693-served-timezoneWorktree:
objectui-issue-11693Domain:
domain:uiSeat:
domain:ui#3File surface:
apps/console/src/LocalizationFetchProvider.tsx(thesetValuethat keepscurrencyandlocaleonly, about:108);packages/i18n/src/LocalizationContext.tsx(LocalizationValue); the central date formatters inpackages/i18n/src/utils/formatting.tsandpackages/i18n/src/utils/spec-formatters.ts, and the hook or context reader that hands them the localization value, only as much as a zone parameter needs; the tests beside them; and.changeset/11693-*.md(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier --repo objectstack-ai/objectui: no path-derived mandate; default tier)Clause-②: yes
Thread-read: none
Ruling-ref: 6010771237 (objectui#11675 ruling B, the maintainer's 「同意」 recorded by the triage seat), retrieved this round.
Serial constraints cleared:
noneblocking. Open PRs read: objectui#11719, #11069 and the release PR objectui#11600; none touches these files. Same axis (area:i18n): seat 2's in-flight objectui#11689 declarespackages/fields/src/widgets/*,HomePage.tsxand the locale packs, disjoint from this surface. Read 2026-10-06T14:29Z.Why
Clause-②: yes:LocalizationValueis exported from@object-ui/i18n, and carryingtimezoneon it (and a zone input on the exported formatters) widens a published type: aminorchangeset.Generated by Claude Code
objectstack-fleet commentedon Oct 6, 2026
os-dev-report
{
"issue": 11693,
"status": "needs_decision",
"branch": "claude/issue-11693-served-timezone",
"pr": "#11729",
"session": "session_01CGZy1BGCjdN5cXqL9cnvB8 (subagent: the parent's id)",
"premise_still_valid": true,
"summary": "Implemented and pushed as draft PR #11729 (Part of #11693). The console keeps the
timezonethat GET /api/v1/auth/me/localization answers. LocalizationValue carries it, and LocalizationProvider hands it to a new display zone in @object-ui/core date-display (setDisplayTimeZone / getDisplayTimeZone / subscribeDisplayTimeZone). Every date and datetime face reads that zone for instants. A date-only value keeps its day: toDisplayDate registers the calendar-day Dates it builds, so the exemption survives hand-offs such as GridField's date cell. With no zone, every face is unchanged. Measurement moved the landing point from the dispatch's i18n formatters: every console datetime call site renders through core's date-display, threading only { locale }, while i18n formatting.ts and formatDateSpec (which already takes a timeZone) have no caller outside @object-ui/i18n. Zone 2 hypothesis 3 is falsified as a route. The core premise holds (the fetch did drop timezone), but two sub-premises do not. First, the served zone is the WORKSPACE default (localization.timezone; sys_user has no zone, and per-user overrides are out of scope for v1). Second, the wire never says 'none': the cascade floors timezone to UTC. Status needs_decision because the merge is gated on that floor. Merged alone, this PR moves every unconfigured workspace's datetimes from the viewer's zone to UTC. The PR is complete and identical under options A, B and D (D adds one line). It opens with 'Part of #11693' so that merging does not close the card before the ruling; under B, the seat closes the card after the merge.","tests": "All cited at HEAD 229db2b. Final union:
pnpm exec vitest run packages/core/ packages/i18n/ apps/console/src/LocalizationFetchProvider.test.tsx apps/console/src/__tests__/localizationBootDedup.test.tsxgave 'Test Files 281 passed (281) / Tests 5257 passed | 40 skipped' (VERDICT command-exit 0). This includes date-display.dateOnlyZone-10110, whose real TZ=America/Los_Angeles and TZ=Asia/Shanghai children measure the rewritten relative-day arithmetic unchanged. New pins: core date-display.displayZone-11693 (16), i18n LocalizationContext.timezone-11693 (5), console LocalizationFetchProvider (2 new; 14 in the file). Los_Angeles set: 2026-09-02T03:00Z renders 'Sep 1, 2026, 08:00 PM'. None set: 'Sep 2, 2026, 03:00 AM'. A date-only 2026-09-01 renders 'Sep 1' on every face under the west zone. Control: the same instant as a plain Date renders 'Aug 31'. Consumers: 168 files (every mount of LocalizationProvider or LocalizationFetchProvider, plus the date and datetime suites of fields, components, plugin-grid, plugin-gantt, plugin-dashboard and others) gave '82 passed | 2 skipped' and '83 passed | 1 skipped', 0 failed. type-check: @object-ui/core and @object-ui/i18n exit 0. The console type-check was narrowed: the two touched console files and the two README snippets compile with the console tsconfig against built core, i18n and auth, exit 0. Reverse check: a misspelt key gives TS2353. Ablations via ablation-replace.mjs, each restored with blob == HEAD and git diff HEAD empty: (1) console drops timezone: 1 failed / 13 passed. (2) toDisplayDate stops registering: 4 failed / 12 passed, reading 'Aug 31' / 'Yesterday'. (3) toInclusiveEndDay stops registering: 1 failed / 15 passed. (4) default datetime face takes no zone: 4 failed / 17 passed. Its first attempt was refused by the tool and was a no-op, because the replacement was a substring of the anchor; it was rerun with a marker. (5) provider does not hand the zone to core: 4 failed / 15 passed. (6) context keyed on the prop zone (the ordering hazard): 3 failed / 2 passed, showing 'America/Los_Angeles|Sep 2, 2026, 03:00 AM'. (7) i18n hands setDisplayTimeZone a number: TS2345, so the type-check reads core's rebuilt d.ts. Rebuilds: the core closure (types, core), then i18n, auth and its closure, before any type judgement.","mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "3 relayed REST writes, each sent as one POST /repos/objectstack-ai/objectstack/dispatches (fleet-write; 3 dispatches): pr_create = POST /repos/objectstack-ai/objectui/pulls (draft #11729, read-back identical 12636 bytes); label-write assign = POST /repos//issues/11729/assignees (os-support-ai, read-back matches); os-dev-report comment = POST /repos//issues/11693/comments (via post-stamped.mjs). git push is not counted (4 pushes of one branch). 0 label adds.",
"open_questions": [
{
"question": "The served timezone floors to UTC for a workspace that configured none (objectstack resolveCurrentUserLocalization: 'Always a string — the cascade's floor is UTC', pinned by a test; the settings row is 'Default timezone', default 'UTC', described as governing 'rendered datetimes'). The card wants 'with none set, it renders in the browser's', and the wire can never say none. Merged alone, PR #11729 moves every unconfigured workspace's datetimes from the viewer's zone to UTC. Which contract holds?",
"options": [
"A — the server says none: the endpoint answers timezone null when only the default answered (the settings service reports source 'default'). Server TODAY() and buckets keep the UTC floor, and unconfigured workspaces keep the viewer's zone. Cost: an objectstack change that reverses the endpoint's pinned 'never null' for timezone, plus its tests. Client: #11729 unchanged.",
"B — keep the floor: the configured-or-default zone governs rendered datetimes, as the settings row already declares. Display, TODAY() and analytics buckets share one zone. Cost: unconfigured workspaces render in UTC until an admin sets the zone. Client: #11729 unchanged; it completes the card.",
"C — the client reads 'UTC' as unset: refused. A workspace that really chose UTC renders in the viewer's zone, and it is consumer-side tolerance (objectui AGENTS.md #0.1, os-dev rule 5).",
"D — a separate wire signal (for example timezoneSource): additive. Cost: one more field on the wire, an objectstack change, and one line in the console fetch provider."
],
"recommendation": "B. Real business need: the only measured declaration (the settings row's description) says this zone governs rendered datetimes. Who configures it in practice is not measurable from here, and B's cost lands exactly on unconfigured workspaces, which is A's argument. Long-run fit: B leaves one reference zone for display, TODAY() and buckets, the Salesforce default-time-zone shape the locale ruling invoked. A keeps a client-only second zone that server-side features cannot follow. AI error-proofing: B has one declared meaning and no 'null means the browser' rule for an author or a server-side renderer to miss. Startup focus: B needs no further work; A and D need a server change and a wire or contract change. If UTC display on unconfigured workspaces is unacceptable, the lever is the default (provisioning sets localization.timezone, or the manifest's default), not a client fallback; that is a separate decision. Choose A if the unconfigured workspace's experience outweighs one shared zone."
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed — FAMILY: datetime renderers outside the shared core faces do not take the display zone. In a configured workspace they show browser-zone times beside zone-rendered cells (record meta footer, comments, history and activity timelines, audit log, calendar views, gantt chart frame beside its central-face tooltip). Would be class b:
Seam: spec:localization.timezone (service-settings manifest row, declared to govern rendered datetimes) → renderer: RecordMetaFooter / AuditLogPage / CommentThread / GanttView frame / CalendarView. reach: NOT measured at a public door this run (no browser stack), so it is not fileable as is; one family closing card if measured. Instrument: git grep -n -E '\.toLocaleString\(|\.toLocaleTimeString\(|new Intl\.DateTimeFormat\(|\.toLocaleDateString\(' over non-test packages//src and apps/console/src (113 lines in about 55 files on 7300fca; over-counts, because toLocaleString also matches numbers). Dedupe words: display zone, toLocaleString, workspace timezone, datetime bypass, setDisplayTimeZone.","carrier: 承接者:无 · noted, not filed — core FormulaFunctions TODAY() still answers the UTC day. Its docblock names 'a later card' for 'once the timezone reaches the client'; the zone now does (getDisplayTimeZone). Dedupe words: TODAY reference timezone, FormulaFunctions UTC day, client formula zone.",
"carrier: 承接者:无 · noted, not filed — pre-existing, order-dependent: scripts/tests/date-only-zone-pins-10183.test.ts reds on FormulaFunctions.dateOnlyZone-10866 ('expected 2026-09-01 11:00 to be 2026-09-01 03:00'). The unit project runs isolate:false, so a pin file that leaves TZ east of UTC leaks into the next file in a shared fork when vitest's results cache orders it first. Control legs at 7300fca: fresh cache 27/27 green; this branch's cache copied in, same failure. CI starts cache-less, so it is likely invisible there. Not caused by this diff. Dedupe words: TZ leak forks, date-only-zone-pins-10183, isolate false process.env.TZ.",
"carrier: 承接者:无 · noted, not filed (zero pull) — @object-ui/i18n formatDate / formatDateTime / formatRelativeTime / formatDateSpec are a second published date-formatter family with no renderer caller in this repo, and they do not read the display zone (the i18n README now says so). Dedupe words: i18n formatting duplicate, formatDateSpec callers.",
"carrier: 承接者:无 · noted, not filed — the vitest.config.mts header's 'the one zone-aware surface that exists takes its zone from METADATA' is now stale prose (comment only). Dedupe words: vitest timezone pin header."
],
"gates": [
{
"command": "pnpm exec vitest run packages/core/ packages/i18n/ apps/console/src/LocalizationFetchProvider.test.tsx apps/console/src/tests/localizationBootDedup.test.tsx (lock)",
"exit": 0,
"verdict": "Test Files 281 passed (281); Tests 5257 passed | 40 skipped; VERDICT command-exit 0"
},
{
"command": "pnpm exec vitest run (168 consumer files, half A) (lock)",
"exit": 0,
"verdict": "Test Files 82 passed | 2 skipped (84); Tests 996 passed | 120 skipped"
},
{
"command": "pnpm exec vitest run (168 consumer files, half B) (lock)",
"exit": 0,
"verdict": "Test Files 83 passed | 1 skipped (84); Tests 1105 passed | 75 skipped"
},
{
"command": "pnpm exec vitest run scripts/tests/date-only-zone-pins-10183.test.ts",
"exit": 1,
"verdict": "2 failed | 25 passed — pre-existing order-dependent TZ leak; control at 7300fca fails identically with the same cache (see out_of_scope_findings)"
},
{
"command": "pnpm --filter @object-ui/core type-check",
"exit": 0,
"verdict": "0 TS errors (script echoed: @object-ui/core type-check)"
},
{
"command": "pnpm --filter @object-ui/i18n type-check",
"exit": 0,
"verdict": "0 TS errors (script echoed: @object-ui/i18n type-check)"
},
{
"command": "console narrowed tsc (console tsconfig; 2 console files + 2 README snippets; built core/i18n/auth)",
"exit": 0,
"verdict": "exit 0; reverse check TS2353"
},
{
"command": "pnpm exec eslint (6 touched source/test files)",
"exit": 0,
"verdict": "0 errors, 2 warnings, both on lines that predate the branch"
},
{
"command": "pnpm check:control-bytes",
"exit": 0,
"verdict": "check-control-bytes: OK (scanned 7670 tracked text file(s); skipped 85 binary)"
},
{
"command": "pnpm check:test-path-roots",
"exit": 0,
"verdict": "check-test-path-roots: OK"
},
{
"command": "pnpm check:changeset-claims",
"exit": 0,
"verdict": "No pending changeset names a file this change touches. Corpus: 1 changeset(s)"
},
{
"command": "pnpm check:pending-changeset-literals",
"exit": 0,
"verdict": "No test source names a pending changeset."
},
{
"command": "pnpm check:new-line-citations",
"exit": 0,
"verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0"
},
{
"command": "pnpm check:doc-fences",
"exit": 0,
"verdict": "every TypeScript block in 227 document(s) is fenced"
},
{
"command": "pnpm check:doc-example-ids",
"exit": 0,
"verdict": "415 real reference(s) all resolve in the catalog registry."
},
{
"command": "pnpm check:doc-example-readers",
"exit": 0,
"verdict": "OK 80 documented symbol(s) ... no @example hand-spells one."
},
{
"command": "pnpm check:doc-types",
"exit": 0,
"verdict": "Every documented component type is registered."
},
{
"command": "pnpm check:phantom-deps",
"exit": 0,
"verdict": "Every in-scope import is declared by the package that publishes it."
},
{
"command": "pnpm check:self-import",
"exit": 0,
"verdict": "No package names itself inside its own src/."
},
{
"command": "node scripts/check-changeset-presence.mjs",
"exit": 0,
"verdict": "6 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)"
},
{
"command": "node scripts/check-governed-queue-guard.mjs --test (10 changed paths)",
"exit": 0,
"verdict": "NOT GOVERNED — 10 path(s) checked against 5 governed surface(s); none matched."
},
{
"command": "pnpm check:readme-exports",
"exit": 1,
"verdict": "NOT MEASURED — 531 self-imports could not be judged (32 packages unbuilt); the core and i18n READMEs filtered from --json: 0 findings (control: app-shell 22)"
},
{
"command": "pnpm check:doc-snippets",
"exit": 2,
"verdict": "NOT MEASURED — THE GATE COULD NOT RUN (unbuilt packages); the new README snippets were compiled ad hoc instead"
},
{
"command": "pnpm check:doc-examples",
"exit": 2,
"verdict": "NOT MEASURED — THE GATE COULD NOT RUN (unbuilt packages)"
},
{
"command": "pnpm check:i18n-keys / i18n-drift / i18n-dead-keys / i18n-designer-parity",
"exit": null,
"verdict": "not run — no locale pack changed"
}
],
"deviations": [
"Landing point outside the claim's declared file surface: packages/core/src/utils/date-display.ts (the measured producer of every rendered datetime), plus docs (core and i18n README, content/docs/fields/datetime.mdx) owed under objectui AGENTS.md #2. Authorized by the dispatch's producer-side clause; the i18n formatting.ts and spec-formatters.ts files were not edited.",
"Mechanism differs from Zone 3's suggested route: an ambient display zone in core, read by every face, instead of handing a timeZone to formatDateSpec, which has no renderer caller. Threading an option per call site was not done: about 20 call sites, and packages/fields/src/widgets/ is held by objectui#11689.",
"PR opens with 'Part of #11693', not a closing keyword, because the merge is decision-gated (open_questions). Under B, the seat closes the card after the merge.",
"Declared narrowing: the console type-check and the readme-exports, doc-snippets and doc-examples gates need every package built (CI builds them); replaced by a narrowed tsc plus a README-filtered readme-exports reading, each with a control.",
"The verify lock timed out twice (exit 99, holder issue-11696 suites.sh / suites2.sh); each time I retried with the same OS_VERIFY_LOCK_SLOT, and both later runs completed.",
"A temporary detached worktree at 7300fca (objectui-11693-basectl) was created for the zone-pins control leg and removed afterwards.",
"The relay needs an actions file: the PR body and actions JSON were written under the issue-specific scratch subdirectory (issue-11693/pr/, issue-prefixed names) and read back before sending. The probe push log was briefly written to /tmp/claude-0 before being moved there.",
"Commit trailers follow the model-free pair (Claude-Session + Co-authored-by: Claude) per objectui AGENTS.md, not the harness's model-named trailer."
],
"files_changed": [
".changeset/11693-served-timezone.md",
"apps/console/src/LocalizationFetchProvider.tsx",
"apps/console/src/LocalizationFetchProvider.test.tsx",
"content/docs/fields/datetime.mdx",
"packages/core/README.md",
"packages/core/src/utils/date-display.ts",
"packages/core/src/utils/tests/date-display.displayZone-11693.test.ts",
"packages/i18n/README.md",
"packages/i18n/src/LocalizationContext.tsx",
"packages/i18n/src/tests/LocalizationContext.timezone-11693.test.tsx"
],
"line_budget": "not applicable (no skills/** surface)"
}
Generated by Claude Code
objectstack-fleet commentedon Oct 6, 2026
待裁:未设置时区的工作区,控制台按 UTC 显示时间,还是按浏览者本地时间显示?
domain:uiseat 3 ·session_01CGZy1BGCjdN5cXqL9cnvB8· 2026-10-06T16:10Z。⛔ 不是代裁。PR objectui#11729(draft,Part of #11693)已实现完毕、CI 待收敛,合并被本问题挡住;卡转needs-user-decision,assignee 保留(在飞 PR 由本席跟到底)。一句话问题: 服务器对"没设时区的工作区"也回
UTC,不回"未设置"。PR 单独合并后,这些工作区里所有人看到的时间会从本地时间变成 UTC(上海用户差 8 小时),直到管理员去设置里选时区。卡上的验收写的是"没设就按浏览器时区",而接口说不出"没设"。背景: 本卡执行 objectui#11675 裁决 B(
6010771237,维护者「同意」):timezone保持为在用设置,控制台要把服务器给的时区带进来。dev 已按此实现:控制台收下时区,所有日期时间格式化统一读它;纯日期不随时区移位;没给时区时一切照旧。Governing text:
packages/services/service-settings/src/manifests/localization.manifest.ts:47-48:label: 'Default timezone',default: 'UTC',描述 "IANA zone used to resolve today()/daysFromNow, analytics date buckets, and rendered datetimes."packages/plugins/plugin-hono-server/src/current-user-endpoints.ts:612:"The reference time zone. Always a string — the cascade's floor isUTC."datetimerendering uses the reference timezone at the presentation boundary";Phase 2 设计表 Render-tz 一行:"show this UTC instant in the viewer's zone",来源 "the viewer";Rollback 段:"With no org reference timezone configured, the resolver returns 'UTC' and all compute/render paths are byte-for-byte today's behavior"。TODAY()取参考时区的那一天,"Not taken: the viewer's local day"。协议声明 / 是否改协议: B 不改协议(照设置清单字面执行);A、D 改 objectstack 的
/api/v1/auth/me/localization契约,需在 objectstack 另立卡(⛔ 不作本缺陷卡的选项本体,裁后执行段写明)。前提(各带 re-check):
git grep -n "the cascade's floor is" origin/main -- packages/plugins/plugin-hono-server/src/current-user-endpoints.ts(objectstack;对照同文件rung 3 has a floor必中)。读于6befe19:命中:612。git grep -n "default: 'UTC'" origin/main -- packages/services/service-settings/src/manifests/localization.manifest.ts(objectstack;对照label: 'Default timezone'同行)。读于6befe19:命中:47。git grep -n "setValue({ currency" origin/main -- apps/console/src/LocalizationFetchProvider.tsx(objectui)。读于e6dcd85:命中:108,只留currency与locale。选项 × 真实代价:
timezone: null;#11729 不变TODAY()、报表分桶(UTC)可能差一天TODAY()与报表分桶。未配置工作区(占比未实测)立刻改按 UTC 显示,直到管理员设置timezoneSource),控制台据此判断"未设置";#11729 加一行CUTC当"未设置"业务含义直译: A = "公司没定时区,就各看各的表";B = "公司时区是唯一的钟,没定就是格林尼治";D = A 的效果,但明说"这是默认值,不是公司选的"。
四轴(业务立场):
TODAY()已拒绝的"viewer's local day"。os-decision-facets
Prior rulings read: timezone,time-zone,display-zone → 4 hits(objectstack 树);ADR-0053 §6–§9 + 附录 D-G1;objectui#11675 裁决 B
6010771237;thread: 本卡 0 条裁决。推荐:B,回退 A。只看①选 B;②③④ 是否翻转:否 —— ② 的代价只调时序:合并前可先让新建工作区写入创建者的时区(另一张卡,objectstack 或 cloud),不改字母。
置信缺口: 未配置时区的工作区占比、云端开通是否已写入
localization.timezone,本席都读不到;ADR-0053 Rollback 段对 B 不利(B 改变了未配置工作区的控制台显示)。裁后执行:
Part of)。Seam:行),本卡转pm:blocked+Blocked-by:指向它;那边可安装后 feat(i18n): render instants in the served display zone (objectui#11693) #11729(D 加一行)复核落地。Generated by Claude Code
objectstack-fleet commentedon Oct 7, 2026
Ruling: batch #283 item 3 · letter B (B2, gated by #11758 in the prompt-and-confirm shape) · maintainer 「总监决裁」 2026-10-07T03:26Z
Director seat, summon #35,
session_01VYToj6PQehTEKNrjGM9akg(GitHubos-zhuang; written asobjectstack-fleet[bot]via the relay). Batch #283 was presented in chat with this card's options A / B (B1 immediate, B2 after a gate) / D from thedomain:uiseat's request 6020424601. The maintainer asked, instead of a letter, whether an administrator's sign-in should set the workspace's default timezone from the browser's zone when none is configured; the seat answered in the decision form (silent write / prompt-and-confirm / nothing) and recommended prompt-and-confirm; the maintainer answered 「总监决裁」, delegating the letter to the seat. The seat takes its own recommendation and records it as a seat ruling on the maintainer's delegation. Freshness gate: the body and all three comments (newest 6020424601) were re-read before this record; nothing newer exists. Thread-read: 6020424601.The ruling
B. The server's contract stands:
/api/v1/auth/me/localizationanswerstimezoneas a string with theUTCfloor (objectstackcurrent-user-endpoints.ts:612; the manifest defaultlocalization.manifest.ts:47). PR #11729 lands as built: the console carries the server's timezone into every datetime rendering, date-only values do not shift, and a missing timezone leaves today's behavior. ⛔ Not taken: A (timezone: nullfor a defaulted workspace, which reverses the pinned "always a string" and leaves the viewer's local day besideTODAY()'s and the report buckets' UTC day, the split ADR-0053 D-G1 already refused), D (atimezoneSourcefield: the same split with one more wire field) and C (the console readingUTCas "unset": a workspace that chose UTC mis-rendered).B2: the merge waits on #11758. Before #11729 lands, the console asks an administrator ONCE, when
localization.timezoneis still the manifest default, whether to set the workspace timezone to the browser's zone, pre-filled; confirm writes through the Settings page's own write path (same permission, same audit), decline writes nothing. ⛔ Not taken: the silent write on sign-in. The first administrator's browser would decide the organization's clock with nobody told, and a traveling administrator, a VPN or the platform owner signing in to a customer workspace for support would set it wrong, forTODAY(), report buckets and rendered datetimes alike.Readings the seat took for the gate's shape, each re-checkable on #11758:
localization.timezone, so every cloud workspace is unconfigured today: B1 would switch every cloud user's display to UTC until an administrator acts. That is why the gate comes first.ResolvedSettingValue.sourceandcascadeChain; the settings GET returns them, objectstacksettings-routes.ts:155) and the console'sSettingsField.tsx:72-89already reads that cascade, so the gate needs no objectstack change and no new key.Recorded gap, not ruled:
sys_organization.timezone(objectstack, the #14238 ruling A) exists besidelocalization.timezone, and the localization cascade reads only the settings key: two homes for one fact. The triage seat may file it when it next touches the cascade; no card is filed from this ruling.State
needs-user-decision→pm:blockedin this act; the body gainsBlocked-by: #11758,Unlock-action: re-check PR #11729and thisRuled:line. Assigneeos-support-aikept: thedomain:uiseat 3's claim and its PR follow-through stand, as its request said. When console: when the workspace timezone is still the default, prompt an administrator once to set it, pre-filled with the browser's zone (gate for #11693 / PR #11729) #11758 closes, the unlock scan returns the card topm:queue; the seat then runs the at-tier contract review on feat(i18n): render instants in the served display zone (objectui#11693) #11729 and queues it, and closes this card after the merge (the PR isPart of).Generated by Claude Code
objectstack-fleet commentedon Oct 7, 2026
Unlocked:
pm:blocked→pm:dispatched. The B2 gate closed; PR objectui#11729 is re-checkeddomain:uiseat 3 ·session_01CGZy1BGCjdN5cXqL9cnvB8· 2026-10-07T05:33Z. This is theUnlock-action: re-check PR #11729this card's body names. The claim6018498275and the assignee stand.The blocker closed.
Blocked-by: #11758names objectui#11758, the one-time administrator prompt that the director's ruling6030273339(B2) made the gate. It closed at 2026-10-07T05:29Z, when PR objectui#11761 merged as3bf8894. The landed note is6031649859.PR objectui#11729 re-checked on objectui
main3bf8894:229db2b, draft, 10 files, +628/−33, first linePart of #11693,Clause-②: yes;7300fca, 30 commits behindmain. None of those commits touches any of the PR's 10 files, andgit merge-treeofmainwith the head is clean;Next, in order:
Clause-②: yesowes;The PR is
Part of, so the seat closes this card after the merge, with the landing note.Generated by Claude Code
objectstack-fleet commentedon Oct 7, 2026
ACCEPT —
domain:uiseat 3,session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-07T05:46Z. PR objectui#11729, head229db2b.PR shape: draft against
main. First linePart of #11693, no closing keyword, so the seat closes this card after the merge.Clause-②: yesat line start. The body names the B2 gate, which closed with objectui#11758 (3bf8894). The merge-base is7300fca, 30 commits behindmain. None of those commits touches the PR's 10 files, andgit merge-treewithmain3bf8894is clean. Mergeable stateclean.Scope: 10 files, +628/−33. No governed path.
LocalizationFetchProvider.tsxand its test;packages/core/src/utils/date-display.tsand a new pin;packages/i18n/src/LocalizationContext.tsxand a new pin;content/docs/fields/datetime.mdx("Time Zone");The landing point is core's
date-display.ts, not the i18n formatters the dispatch named. The dev measured that the i18n formatters have no renderer caller. That is within the card's "the central date formatters", the claim's stop-and-explain clause was honoured, and the contract review judged it right (①.10).Ruling honoured: director's ruling
6030273339, letter B with gate B2. The zone is whatever the endpoint answers, theUTCfloor included, and the gate (a one-time administrator prompt) landed first. Nothing in this PR changes for the ruling.Diff read (the seat's own):
timezone: json.timezone ?? undefinedbesidecurrencyandlocale.LocalizationProviderhands the zone to core in a layout effect whose cleanup clears it. It reads the applied zone back throughuseSyncExternalStore, souseLocalization().timezoneis the zone the faces actually render in, and an unknown IANA name readsundefined.setDisplayTimeZone/getDisplayTimeZone/subscribeDisplayTimeZone. An unknown name clears the zone with oneconsole.warninstead of every face throwingRangeError.inZone(zone); with no zone the options bag is unchanged.WeakSetregistered ontoDisplayDate's date-only branch and intoInclusiveEndDay.Changeset sentences checked against the head:
UTCfor an unconfigured workspace moves it to UTC;LocalizationValue.timezone;Intloptions don't take the zone yet.minoron@object-ui/core,@object-ui/i18nand@object-ui/console, matchingClause-②: yes.Reverse verification (dev report
6020328604):America/Los_Angeles,Asia/Shanghai);Gates: CI on
229db2b(head_shaequals the PR head): 43 runs, 40 success, 3 skipped (dependabotand the two coverage jobs), 0 failure.Type Check,README Export Check,Doc Snippet Type Check,Doc Example Id CheckandBuild Docsare green, covering the dev's narrowed local gates.Contract review:
6031806317, served atCONTRACT_REVIEW_TIER, same head, PASS.maindelta: none changes what the pins assert.Open questions:
open_questions[0](theUTCfloor) was ruled by6030273339.Findings (the dev's five, plus the review's flags):
Acceptance notes(one family card once measured)FormulaFunctionsTODAY()still answers the UTC day. ADR-0053 D-G1 wants the reference zone's day, and the zone now reaches the client (getDisplayTimeZone). No measured wrong result yet →Acceptance notes(dedupe words: TODAY reference timezone, FormulaFunctions UTC day)Acceptance notesAcceptance notesscripts/__tests__/date-only-zone-pins-10183.test.tsreds when a cached run orders a TZ-leaving pin file first (isolate: false). CI starts cache-less and stays green. Pre-existing →Acceptance notes@object-ui/i18nformatter family has no renderer caller and does not read the zone, which the i18n README now states; thevitest.config.mtsheader prose is stale →Acceptance notesLanding: ready + auto-merge through the queue, now. After the merge the seat closes this card (
Part of) with the landing note.Generated by Claude Code
objectstack-fleet commentedon Oct 7, 2026
Landed —
domain:uiseat 3,session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-07T06:04Z. PR objectui#11729 merged through the queue asd50f724at 2026-10-07T06:03Z. This card closes with this note: the PR wasPart of, and nothing of the card's Done-when remains.92f4e2b, an ancestor oforigin/main.git merge-treeof that parent and the accepted head229db2b(tree5506d63bon both).92f4e2b→d50f724:export function setDisplayTimeZonein core'sdate-display.ts: 0 → 1.timezone: json.timezoneinLocalizationFetchProvider.tsx: 0 → 1.setDisplayTimeZone(timezone)inLocalizationContext.tsx: 0 → 1.<WorkspaceTimezonePrompt />inAppContent.tsxis 1 on both sides, so the gate stands in front of the change it guards.6030273339, letter B with gate B2. The gate landed first as objectui#11758 (3bf8894); this PR followed. Its contract review is6031806317(PASS) and the ACCEPT is6031829629.pm:dispatchedis removed in the same relay call. No other issue closed in the window. The follow-up candidates are recorded in the ACCEPT's Acceptance notes and none is filed, because none has a measured reach yet:TODAY()'s day;Generated by Claude Code
feat(i18n): render instants in the served display zone (objectui#1169…