Skip to content

Commit 34ec30d

Browse files
docs(adr-0053): addendum D-G1 — a client-evaluated today() names the compute-tz reference day, UTC while none reaches the client (#20409)
Fixes #20394 Clause-②: no ## What this changes One file, `docs/adr/0053-date-and-datetime-semantics.md`, +13 / -1 lines, prose only: - **Addendum D-G1 (2026-09-28)**, nested under Phase 2 item 6 (`today()` / `daysFromNow()` / `daysAgo()` become timezone-aware), just before item 7. It records the maintainer's ruling A on objectui#10903: a formula evaluated in the client (objectui's expression evaluator, `@object-ui/core` `FormulaFunctions`, where the function is spelled `TODAY()`) resolves `today()` to the same reference day the server resolves for that user on the compute-tz axis (items 5–6). While no reference timezone reaches the client, it resolves to the UTC day. One "today" per execution context, so a preview and the saved answer agree. The two options not taken are also recorded: the viewer's local day, and the UTC day as the permanent rule. The addendum names threading the reference timezone to the client as a follow-up once Phase 2 exposes it. - **Consumers line**: `@object-ui/core` (`FormulaFunctions`, the client-evaluated `TODAY()`; addendum D-G1) now sits next to `@objectstack/formula`. The objectui half of ruling A (the `FormulaFunctions` `TODAY()` JSDoc) is in objectui PR 10915. That JSDoc cites this ADR only by "Phase 2 items 5 and 6", not by a decision id, so choosing the id D-G1 here breaks no citation. The wording follows the seat's ruling on the cross-repo note (comment 5865501291). It says "the reference day the server resolves for that user". It does not restate the resolver's precedence, and it does not claim that a per-user override takes effect today. See Acceptance notes. ## Gates — all run at `1b063d17` (`git rev-parse --short HEAD`), exit code captured before any pipe The list comes from `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`, which found 19 commands. The `--ran` reconciliation reported `19 derived famil(ies) accounted for — 19 run, 0 NOT-MEASURED (a DERIVED zero — all 19 recorded an exit code and none of them is 3)`. | command | exit | verdict line | |---|---|---| | `node scripts/check-adr-links.mjs` | 0 | | | `node scripts/check-adr-links.mjs --self-test` | 0 | | | `node scripts/check-adr-symbol-anchors.mjs` | 0 | | | `node scripts/check-adr-symbol-anchors.mjs --self-test` | 0 | | | `node scripts/check-ci-filter-parity.mjs` | 0 | | | `node scripts/check-closing-keyword-parity.mjs` | 0 | | | `node scripts/check-closing-keyword-parity.mjs --self-test` | 0 | | | `node scripts/check-comment-mask-corpus.mjs` | 0 | | | `pnpm --filter @objectstack/lint run check:doc-formula-expressions` | 0 | the first run exited 3 (`PREREQUISITE NOT MET`, formula and lint not built), so nothing was measured. After `turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` under `os-verify-lock` (`VERDICT command-exit 0`), the rerun exited 0 | | `pnpm check:adr-anchors` | 0 | `check-adr-anchors: OK (54 anchored file(s) … 1123 decision-letter citation(s) on the anchored surface name a decision the record makes)` | | `pnpm check:cross-package-test-inputs` | 0 | | | `pnpm check:doc-authoring` | 0 | | | `pnpm check:driver-memory-census` | 0 | | | `pnpm check:gitlink-declared` | 0 | | | `pnpm check:nul-bytes` | 0 | `check-nul-bytes: OK (scanned 9892 text file(s) … no raw ASCII control bytes)` | | `pnpm check:pm-governed-merges` | 0 | | | `pnpm check:pm-prior-rulings` | 0 | | | `pnpm check:refd-timer-probe` | 0 | | | `pnpm check:watch-hint-literal` | 0 | | | `node scripts/check-issue-citations.mjs --base df3ba16` (named by the dispatch; not in the derivation) | 0 | `no issue citations added … (0 file(s) read)`. **NOT MEASURED for this diff**: `docs/adr/**` is one of that gate's declared `DEFERRED_SURFACES` | A separate control-byte scan of the edited file (`grep -naP` over the C0 range and DEL) found no match (exit 1). Not run locally, and left to CI: the type-check lanes, the `Test Core` shards, and the families the derivation places outside its list (wide-population, artifact-roster, pending-changeset). The diff touches no package, so there is no dependency-closure build and no package test or typecheck to run. ## Line budget | file | before | after | |---|---|---| | `docs/adr/0053-date-and-datetime-semantics.md` | 1210 lines · 75844 bytes · 18961 tokens | 1222 lines · 76692 bytes · 19173 tokens | Tokens are counted as `ceil(bytes / 4)`, the unit `check-skills-token-ratchet` uses. There is no package-level total because the diff touches no package. ## Changeset None, and the `skip-changeset` criterion holds: `docs/adr/**` is on the fast-lane list. I also measured it. Across 69 non-private `package.json` files, zero `files[]` entries name `adr` or reach outside their own package. `Addendum D-G1` has zero hits in the built `packages/formula/dist` and `packages/lint/dist`. As a positive control, `ADR-0053` has 4 hits in `packages/formula/dist`. ## Acceptance notes - **Why the id is D-G1.** The dispatch suggested "Addendum D-A2". D-A2 is already taken: it is the 2026-06-18 addendum's `temporalFilterValue` decision, which the Status line marks as resolved on 2026-07-30. D-A4 would put the ruling inside the family of that analytics raw-SQL addendum. Each existing addendum has its own letter family (D-A through D-F), so the next free one, D-G, is used. The addendum sits under item 6 as a dated blockquote, the same shape as the `Resolved (2026-07-30)` note under D-A2. Its blank lines make the item 5–7 list loose, which affects rendering spacing only. - **The Status line is unchanged.** The card asks for the addendum line and the Consumers entry. Not every landed addendum is listed on the Status line (D-D and D-E are not). - **A detail in the card's premise.** The card says the server's `today()` "is UTC midnight today (`startOfDayUtc`)". On `df3ba164`, `packages/formula/src/stdlib.ts` `registerStdLib` already resolves `today()` through `calendarDayUtc(now(), timezone)`: the reference-timezone calendar day, with `'UTC'` as the default and fallback. `startOfDayUtc` is that fallback branch. The ruling and this addendum are unaffected, because both name "the reference day the server resolves". - **Drift, not changed here.** The ADR's Phase 2 text and the runtime disagree about a per-user timezone: - ADR text: Phase 2 item 5 ("optionally overridden by `sys-user-preference.timezone`"), the resolver-precedence table's "User override" row, Non-goals ("org-default + user-override only"), and the Consumers entry `sys-user-preference.timezone`. - Runtime: it resolves the organization level only. `packages/core/src/security/resolve-authz-context.ts` `resolveLocalizationContext` reads `localization.timezone`. `packages/services/service-settings/src/manifests/localization.manifest.ts` declares `scope: 'tenant'` and says "Per-user overrides are intentionally out of scope for v1". `packages/runtime/src/security/resolve-execution-context.test.ts` pins "ignores per-user sys_user_preference rows (organization-level only)". - The per-user tier was dropped by the released "Resolver 收编" change (`packages/runtime/CHANGELOG.md`). No writer of a timezone preference was found in this tree or in objectui `origin/main` `733fd5ac6`. - Whether Phase 2's per-user layer is still the intent is a question for the maintainer. D-G1 is worded so that it holds either way. - **Duplicate heading.** `### D-D2` appears twice with two different decisions: "Consequences for D-A3" in the #3777 addendum, and "The primitive lives in `spec`" in the addendum that follows it. This is a naming issue only, and it is not changed here. ## 维护者速读(草稿) **改了什么**:在 ADR-0053 Phase 2 第 6 项(`today()` 按参考时区取日)下面加了一条附加条 D-G1,记下维护者对 objectui#10903 的裁决 A。同时在 ADR 的 Consumers 行加了 objectui 的前端公式求值器(`@object-ui/core` `FormulaFunctions`)。只改文档,不动任何代码和运行时行为。 **为什么改**:同一个用户在浏览器里预览公式看到的「今天」,要和服务端保存时算出的「今天」是同一天。裁决 A 已经批准,objectui 那一半(JSDoc)在 objectui PR 10915。objectstack 这一半要写进管这件事的 ADR,否则之后读 ADR 的人或 AI 不知道前端也受这条规则约束。 **风险与代价(含回滚)**:纯文档,没有运行时风险,ADR 多了 12 行。回滚只需 revert 本 PR 的一个 commit。有一处已知的不一致没在本 PR 改:Phase 2 第 5 项写的是参考时区「可由用户偏好覆盖」,但运行时现在只读组织级设置(这是有意的,有测试钉住)。附加条的措辞刻意没有承诺按用户覆盖。这一层是否仍是 Phase 2 的意图,需要您定。 **席位意见**: **你要做的**:审阅 D-G1 的措辞,同意就在本 PR 上点 Approve。 --- _Generated by [Claude Code](https://claude.ai/code/session_01MjvgiFAmjHqsxy1XLiVYfH)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 65352b7 commit 34ec30d

1 file changed

Lines changed: 13 additions & 1 deletion

File tree

‎docs/adr/0053-date-and-datetime-semantics.md‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
**Status**: Accepted (2026-06-16) — Phase 1 + addendum D-A1 implemented (`sql-driver.ts` `toDateOnly` write/read/filter normalization; analytics `coerceTemporalFilterValue`), Phase 2 landing incrementally; D-A2 resolved 2026-07-30: `temporalFilterValue` + `temporalFilterColumnSql` are optional `IDataDriver` contract members with identity semantics, and analytics types its driver seam from the contract. **Partly superseded (2026-07-29, addendum D-B1..D-B4):** Phase 1's "`Field.datetime` stays stored as UTC epoch ms" is replaced by one canonical UTC instant per dialect — `YYYY-MM-DDTHH:MM:SS.sssZ` text on SQLite, `timestamptz` on Postgres, `DATETIME(3)` on MySQL — applied on write and to filter comparands alike (#3912, #3942). **Extended (2026-07-30, addendum D-C1..D-C3):** `Field.time` takes the same construction — canonical `HH:MM:SS[.fff]` wall-clock text, one function on write/filter/read, `TIME(3)` on MySQL, UTC `NOW()` defaults on every dialect (#3994). **Extended (2026-09-07, addendum D-F1..D-F3):** the READ side takes the same canon — every `@objectstack/driver-sql` record read door ~~but `findWithWindowFunctions` (#16609)~~ presents `Field.datetime` values and the builtin `created_at` / `updated_at` audit stamps as the canonical `YYYY-MM-DDTHH:MM:SS.sssZ` text on every dialect, folded at the driver's read boundary with the client parsers untouched; those doors never hand out a JS `Date` for those columns, save an Invalid `Date`, which has no canonical text and passes through unchanged (#13973, maintainer ruling B1 narrow, 2026-09-02). **Corrected 2026-09-08 (#16609 / PR #16716):** that exception is gone — `findWithWindowFunctions` now routes each row through the same `formatOutput` pass, so those two column classes present as the same canonical text there, with the window ALIAS columns carved out (a computed alias wins the key and its value stays raw). D-F1 rules those two classes and no more: the other presentations that pass applies at that door are #16609's contract, not this ADR's.
44
**Deciders**: ObjectStack Protocol Architects
55
**Builds on**: [ADR-0032](./0032-unified-expression-layer.md) (unified expression layer — CEL dialect, `today()`/`daysFromNow()`), [ADR-0014](./0014-record-form-field-type.md) (field types)
6-
**Consumers**: `@objectstack/spec` (`Field.date`/`Field.datetime`), `@objectstack/driver-sql` (`coerceFilterValue`, `formatInput`/`formatOutput`, `dateFields`/`datetimeFields`), `@objectstack/formula` (`stdlib` time functions, `cel-engine` hydration), `@objectstack/objectql` (`applyFormulaPlan`), schedule/cron executors, report/analytics date bucketing, `sys-user-preference.timezone`.
6+
**Consumers**: `@objectstack/spec` (`Field.date`/`Field.datetime`), `@objectstack/driver-sql` (`coerceFilterValue`, `formatInput`/`formatOutput`, `dateFields`/`datetimeFields`), `@objectstack/formula` (`stdlib` time functions, `cel-engine` hydration), `@object-ui/core` (`FormulaFunctions`, the client-evaluated `TODAY()`; addendum D-G1), `@objectstack/objectql` (`applyFormulaPlan`), schedule/cron executors, report/analytics date bucketing, `sys-user-preference.timezone`.
77
**Surfaced by**: the formula/flow guardrail series (#1928) and the templates time-relative bug family (#1874). Browser testing of `example-crm` and `templates` found that a `Field.date` compared for equality against a time function (`end_date == daysFromNow(60)`, `expires_on: { $in: [daysFromNow(30)] }`) **silently matches nothing** — and that the value is stored as a full timestamp, not a calendar day.
88

99
---
@@ -124,6 +124,18 @@ day), `$in` of dates works, and the day-window range pattern keeps working. The
124124
23:00 UTC-8 on the 15th, `today()` must be the 15th, not UTC's 16th.) For a
125125
genuine sub-day time offset, authors use `now() + duration("Nh")` — the
126126
documented escape hatch.
127+
128+
> **Addendum D-G1 (2026-09-28) — a client-evaluated `today()` names the
129+
> compute-tz reference day** (objectui#10903, maintainer ruling A). A formula
130+
> evaluated in the client — objectui's expression evaluator, `@object-ui/core`
131+
> `FormulaFunctions`, where the function is spelled `TODAY()` — resolves
132+
> `today()` to the same reference day the server resolves for that user on the
133+
> compute-tz axis (items 5–6), and to the UTC day while no reference timezone
134+
> reaches the client. One "today" per execution context: a preview and the
135+
> saved answer agree. Not taken: the viewer's local day, and the UTC day as
136+
> the permanent rule. Threading the reference timezone to the client is a
137+
> follow-up once Phase 2 exposes it.
138+
127139
7. **`datetime` rendering** uses the reference timezone at the presentation
128140
boundary (console, templates, reports). Storage stays UTC.
129141

0 commit comments

Comments
 (0)