Skip to content

【缺陷】数据导出(CSV/XLSX)日期时间列硬编码按 UTC 渲染,与界面时区不一致(@objectstack/rest export-format.ts formatDate) #8373

Description

@baozhoutao

版本与环境

  • @objectstack/* 17.0.0-rc.6(rest / console / core / spec)
  • 复现环境:全新空库 SQLite dev 实例,OS_LOCALIZATION_TIMEZONE=Asia/Shanghai,容器 TZ=Asia/Shanghai
  • 触发入口:Console 列表工具栏原生「导出」按钮(CSV 与 XLSX 均复现)

现象

界面按业务时区(东八区)正确显示日期时间,但导出文件中同一字段按 UTC 输出,相差 8 小时;跨日记录在导出文件中会退到前一天(月初记录退到上个月,月度对账直接对不上)。

字段(datetime) 界面显示(东八区) 导出文件值 差值
扫码时间 2026/8/1 上午6:00 2026-07-31 22:00:00 −8h,跨月退到 7 月
下工时间 2026/8/13 下午5:00 2026-08-13 09:00:00 −8h
created_at(系统字段) 当地 16:59:35 2026-08-13 08:59:35 −8h

CSV 与 XLSX 输出完全相同的 UTC 字符串(XLSX 中为文本单元格,非带时区的日期型)。已在 3 个不同对象上复现;formatCellValue() 按字段类型分派,与对象无关,波及全部含 date/datetime 列的导出。

复现步骤

  1. 全新实例,OS_LOCALIZATION_TIMEZONE=Asia/Shanghai;
  2. 任一对象造一条 datetime 值为当地 2026-08-01 06:00(落库 2026-07-31T22:00:00.000Z)的记录;
  3. 列表页确认界面显示 2026/8/1 上午6:00;
  4. 点工具栏「导出」下载 CSV 或 XLSX;
  5. 文件中该列为 2026-07-31 22:00:00(期望:2026-08-01 06:00:00)。

根因(已定位到函数)

调用链:Console exportDownload() → GET /api/v1/data/{object}/export?format=…&fields=…(请求无时区参数)→ @objectstack/rest src/rest-server.ts:6930 导出路由 → CSV rowsToCsv()(:1651-1663)/ XLSX(:7208)→ formatRowCells(row, cols, metaMap)(签名无时区入参)→ src/export-format.ts:174-181:

/** `YYYY-MM-DD` (date) or `YYYY-MM-DD HH:mm:ss` (datetime), in UTC. */
function formatDate(value: unknown, withTime: boolean): unknown {
  const d = toDate(value);
  if (!d) return value;
  const ymd = `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;
  if (!withTime) return ymd;
  return `${ymd} ${pad2(d.getUTCHours())}:${pad2(d.getUTCMinutes())}:${pad2(d.getUTCSeconds())}`;
}

getUTC* 写死,由同文件 formatCellValue()(:251-252)对 type: 'date' / type: 'datetime' 无条件调用。getUTC* 与进程 TZ 无关,部署侧无从规避;导出链路亦无时区配置项、无 export 钩子、视图 exportOptions 无格式化项。

三条佐证,倾向这是疏漏而非有意设计:

  1. 时区在这条路径上拿得到、只是没接:导出路由第一步 resolveExecCtx(src/rest-server.ts:6935)取到的 ExecutionContext 已带 timezone(resolveLocalizationContext() 级联 平台默认→全局→租户),格式化层未使用;
  2. 同平台别处认时区:自动编号日期令牌按 ADR-0053「business timezone」渲染,fallback 才是 UTC(@objectstack/spec RenderAutonumberInput.timezone);导出格式化与该语义不一致;
  3. 导出自身两套时钟:同文件 exportContentDisposition()(src/export-format.ts:58-82)生成文件名时间戳用 getFullYear()/getHours() 本地时区取值——文件名东八区、文件内容 UTC,同一次导出内部自相矛盾。

建议修法

把 ExecutionContext.timezone 透传进 formatRowCells / formatCellValue,formatDate 用 Intl.DateTimeFormat(…, { timeZone }) 按业务时区取日历分量;timezone 缺省时维持现状(UTC),保证向后兼容。

来源

下游实施项目验收中发现并完成定位:steedos-labs/os-project-titanwind-ehr#1269(含完整取证:3 对象实测截图、导出原件、逐介入点排查记录)。修复前下游导出文件无法用于月度对账,盼排期。

Activity

  1. added theissue type on Aug 13, 2026
  2. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    Triage: lands in packages/rest (src/export-format.ts formatDate / formatCellValue, plumbed through formatRowCells in src/rest-server.ts) ⇒ domain:cli, pm:queue, type Bug, target:v17. Release-board rationale (binary test, class ①): a published-surface defect a downstream deployment hits today — 17.0.0-rc.6, Console export of every date/datetime column renders UTC while the UI renders the business timezone, cross-day rows fall into the wrong month and monthly reconciliation fails (full forensics in steedos-labs/os-project-titanwind-ehr#1269). Root cause is already located to the function; the fix direction in the body (thread ExecutionContext.timezone — already resolved at the export route — into the formatter, Intl.DateTimeFormat with timeZone, UTC fallback when absent) is consistent with the ADR-0053 business-timezone semantics that autonumber date tokens already follow, and with the file-name-vs-content self-contradiction noted in exportContentDisposition. Not a contract accept/reject change — output formatting pulled back to declared semantics. Dedup: no open twin found (export timezone / formatDate UTC searches). Size/model suggestion: M, opus; consumer-package tests should cover CSV and XLSX both, plus the timezone-absent fallback.


    Generated by Claude Code

  3. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    Queued first in this lane — dispatches the moment #7912 lands. (domain:cli seat #6024, session session_01P7vaLs7bhBPi9m3JyzkhDj.) Stays pm:queue, unassigned; ⛔ not a claim.

    Maintainer directive, 2026-08-13 (live PM chat): target:v17 and Bug take priority. Under it this card is the top of this lane's queue — it is the only dispatchable v17 card here (the other, #7644, is pm:on-hold awaiting a 17.0.0 release), it is type Bug, and it has a measured downstream deployment blocked on it rather than an internal sweep finding.

    Why it is not out already — same-file hard serial, not a grading doubt:

    card region of packages/rest/src/rest-server.ts
    #7912 (in flight) filterAppForUser, ~:2976-3056
    #8373 (this card) the export route ~:6930, rowsToCsv ~:1651-1663, XLSX ~:7208

    Regions are ~4000 lines apart, but the rule is the rule: priority is ordering, not exemption — even priority:p0 does not waive same-file serialization. I put the exemption to the maintainer explicitly (region-level declaration + merge main before PR + re-merge after the sibling lands + merge-queue arbitration) and the ruling was do not exempt; wait for #7912 to land. Recording that here so the next reader does not re-litigate it.

    ⇒ rest-server.ts queue is now #7912 (in flight) → #8373 → #8039. This card displaces #8039, which is comment-only and was previously next.

    For whoever takes it — leads to verify, not inherit. The card is unusually well-diagnosed (root cause located to formatDate at export-format.ts:174-181, with the call chain traced), and that is exactly the kind of precision that decays: re-measure the line anchors on origin/main at dispatch time and anchor on symbols. Two things on the card worth carrying:

    • The timezone is already resolved on this path and simply not threaded — resolveExecCtx at the export route hands back an ExecutionContext carrying timezone (platform default → global → tenant cascade). The fix is plumbing, not a new resolution mechanism.
    • ⭐ The strongest evidence that this is an oversight rather than a design choice: the same file already keeps two clocks. exportContentDisposition() (:58-82) builds the download's filename timestamp with local-time getters while the file's contents use getUTC*. One export, contradicting itself.

    Coverage must include CSV and XLSX (both reproduce), plus the timezone-absent fallback staying UTC for backward compatibility. Size/model per triage: M, mode:subagent, model: opus.


    Generated by Claude Code

  4. self-assigned this
    on Aug 13, 2026
  5. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    Claim: PM loop round 4 (domain:cli seat #6024)
    Session: session_01P7vaLs7bhBPi9m3JyzkhDj
    Branch: claude/issue-8373-export-business-timezone
    Worktree: objectstack-issue-8373
    Domain: domain:cli
    File surface: packages/rest/src/export-format.ts (formatDate / formatCellValue / formatRowCells) + the export route's call sites in packages/rest/src/rest-server.ts (the export handler and the CSV/XLSX writers only) + tests in packages/rest. (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus
    Serial constraints cleared: #7912 MERGED (PR #8426) — rest-server.ts is free, verified on origin/main. #8039 is queued behind this card in that file's chain and is not in flight. No dev in flight in this lane.

    Maintainer priority. target:v17 and Bug take precedence in this lane (maintainer directive, 2026-08-13), and this is the only dispatchable v17 card here — the other, #7644, is pm:on-hold awaiting a 17.0.0 release, which is a human-only action. It also has a measured downstream deployment blocked on it rather than an internal sweep finding: monthly reconciliation is failing on 17.0.0-rc.6 because exported cross-day rows land in the wrong month.

    Premise re-verified on the merged ref, post-#7912

    rest-server.ts changed 20 minutes ago, so these were re-measured rather than carried from the card:

    • packages/rest/src/export-format.ts:175 — function formatDate(value, withTime)
    • :178 / :180 — getUTCFullYear() … getUTCHours(), hardcoded
    • :237 formatCellValue, :258 formatRowCells — neither takes a timezone parameter
    • ⭐ :66-67 — exportContentDisposition() builds the filename timestamp with getFullYear() / getHours(), i.e. local time

    That last one is the sharpest evidence on the card and it survives: one export, two clocks. The filename says the business timezone and the contents say UTC. That is very hard to read as a deliberate design and easy to read as a formatter that was never handed the timezone the rest of the request already knows.

    The fix direction (the card's, and it is sound — but verify the seam yourself)

    Thread ExecutionContext.timezone into formatRowCells / formatCellValue, and have formatDate take the calendar components in the business timezone via Intl.DateTimeFormat(…, { timeZone }). timezone absent ⇒ keep today's UTC behaviour, so nothing changes for a deployment that never set one.

    ⚠️ The timezone is already resolved on this path and simply not threaded. The card states resolveExecCtx at the export route hands back an ExecutionContext carrying timezone (platform default → global → tenant cascade). Verify that yourself on the merged tree — it is the one claim the whole fix rests on, the line numbers around it moved today, and if it turns out the export route does not have the timezone in hand, stop and report: the shape of the card changes completely.

    Consistency anchor worth checking rather than assuming: autonumber date tokens already render in the ADR-0053 business timezone with UTC as the fallback (RenderAutonumberInput.timezone in @objectstack/spec). If that is still true, this card is bringing the export formatter into line with an existing platform semantic rather than inventing one — which is a much easier change to justify and to review.

    Coverage — non-negotiable

    • CSV and XLSX, both reproduce and both must be pinned. The card notes XLSX writes a text cell, not a timezone-aware date cell, so a fix that only corrects CSV would leave the reported symptom half-standing.
    • The timezone-absent fallback stays UTC — pinned, because that is the backward-compatibility promise.
    • ⭐ Pin the cross-day case specifically. The reported harm is not "8 hours off", it is a row moving to the previous month — 2026-08-01 06:00 +08 exported as 2026-07-31 22:00. A test using a mid-day timestamp would pass while the actual customer symptom survived. Use a timestamp that crosses both a day and a month boundary.
    • Ablate: revert the threading and show the cross-day case goes red.

    Non-negotiables

    • Run every build and test as an ordinary foreground Bash call and block on it (raise timeout up to 600000 ms). ⛔ Do not background anything, ⛔ do not poll for a notification — none will arrive.
    • ⚠️ packages/rest hides its test layer from tsc. If you add a test file, use explicit .js extensions on relative imports — the package resolves NodeNext, the extensionless spelling used by the older test files is a TS2835, and check:type-check-debt has zero headroom. A PR went red on exactly this today. ⛔ Never raise the ledger; fix the type.
    • ⛔ Do not touch the docblock above DATA_RECORD_READ_PARAMS in rest-server.ts — that region is card [rest] GET /data/:object/:id folds no query aliases — the CANONICAL fields spelling is dropped while the alias select works #8039, queued behind you.
    • ⛔ Do not edit content/docs/releases/**.
    • Changeset required (user-visible behaviour change on a published surface).
    • Draft PR, target main, first line Fixes #8373.
    • Return the structured JSON report and post it as an issue comment here with first line <!-- os-dev-report -->.

    Generated by Claude Code

  6. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor
    {
      "issue": 8373,
      "status": "done",
      "branch": "claude/issue-8373-export-business-timezone",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/8487",
      "premise_still_valid": true,
      "summary": "The card's load-bearing claim held on the merged tree: the export route's resolveExecCtx returns an ExecutionContext whose timezone is set by assembleExecutionContext (packages/core/src/security/assemble-execution-context.ts) from resolveLocalizationContext's platform-default -> global -> tenant cascade, and the formatter simply never took it. Threaded it into formatRowCells / formatRowForJson / formatCellValue and gave formatDate its calendar components through Intl.DateTimeFormat with that zone (formatters memoized per zone; a 50k-row export formats one cell per datetime column per row). All three writers of the one route got it — CSV, XLSX and JSON: JSON shares formatCellValue via formatRowForJson, so leaving it out would have recreated inside the route the 'one export, two clocks' contradiction the card cites as evidence; that is one line beyond the dispatch's 'CSV/XLSX writers only' wording and is flagged here and in the PR. TWO DELIBERATE NARROWINGS the PM should read: (1) timezone absent, or a zone the platform does not know, keeps today's UTC rendering byte for byte — the compatibility promise, pinned both ways; (2) `date` columns are NOT re-projected. ADR-0053 makes date a timezone-naive calendar day and datetime an instant rendered in a reference timezone, and driver-sql's toDateOnly is the single source of truth the filter/write/read paths all agree with — projecting a date-only value through a zone would move 2026-08-01 to 2026-07-31 for every deployment west of UTC, inventing the off-by-one-day defect ADR-0053 exists to remove. That narrows the card's 'date/datetime' phrasing to datetime only; the customer's own evidence table is datetime-only (扫码时间 / 下工时间 / created_at). exportContentDisposition's local-time filename stamp was left alone and filed as #8484. The issue body fetched complete — not truncated.",
      "tests": "New packages/rest/src/export-business-timezone.test.ts, 18 tests: unit formatter cases plus the REAL export route over a real ObjectQL engine + real sqlite :memory: driver (mirrors export-integration.test.ts; only resolveExecCtx is stubbed, standing in for the localization cascade). Every datetime fixture straddles a MONTH boundary (2026-07-31T22:00Z <-> 2026-08-01 06:00 +08), never a mid-day instant. Green: `npx vitest run src/export-business-timezone.test.ts` -> 'Test Files 1 passed (1) / Tests 18 passed (18)'. Whole package: `pnpm --filter @objectstack/rest test` -> 'Test Files 113 passed (113) / Tests 1861 passed (1861)'; `pnpm --filter @objectstack/rest typecheck` clean. ABLATION (direction predicted first: business-tz assertions red, UTC-fallback and date-naive pins green because they assert pre-fix behaviour) — commit the fix first, then wallClock(d, undefined): 'Tests 9 failed | 9 passed (18)', with \"expected '2026-07-31 22:00:00' to be '2026-08-01 06:00:00'\" and the same shift at route level for CSV and XLSX both; restored via `git checkout <branch> -- <path>` (never stash), re-verified green. GATES on a full built closure (`turbo run build --filter=./packages/* --filter=./packages/*/*`, 70/70): check:type-check-debt OK — 33 entries re-measured, none above recorded, 'surplus: none'; the FIRST run was red at +1 on this package's frozen test layer (TS2554 from registry.registerObject's missing packageId in the new test) and was fixed at the source, ledger untouched. Also green: check:type-check-coverage, check:nul-bytes, check:route-envelope, check:authz-resolver, check:query-options-erasure, check:cross-package-test-inputs, check:filter-alias-parity, check:meta-type-normalized, check:changeset-gate-self-tests, check:objectui-changeset, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, and `eslint --no-inline-config` over the three changed files. Gate list re-derived against the actual changed paths with scripts/pm/dispatch-gates.mjs — it surfaced the changeset family and check:route-envelope / check:authz-resolver / check:filter-alias-parity / check:meta-type-normalized / check:cross-package-test-inputs / check:query-options-erasure beyond the prompt's named check:type-check-debt, and all were run. No consumer sweep was needed: the changed signatures are package-internal (packages/rest/src/index.ts exports only buildFieldMetaMap and the ExportFieldMeta type from this module), and every new parameter is optional.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #8484: exportContentDisposition still stamps the download FILENAME in the process-local timezone (getFullYear/getHours) — the other half of the card's 'one export, two clocks' evidence; left out because it changes a different user-visible surface and was not in the fix direction",
        "filed as #8485: bulk import's parseDateCell reads a naive datetime cell in the PROCESS-local timezone (measured: `new Date('2026-08-01 06:00:00')` is 2026-07-31T22:00Z under TZ=Asia/Shanghai and 2026-08-01T06:00Z under TZ=UTC), so the export/edit/re-import round trip depends on the host TZ rather than the tenant's — pre-existing, and this PR changes which deployments happen to be lucky"
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  7. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    ACCEPT — PR #8487, reviewed by the domain:cli seat (#6024, session session_01P7vaLs7bhBPi9m3JyzkhDj). ⏳ Flip held: CI started 15:35 and has not converged. Card stays pm:dispatched.

    Verified against GitHub. 4 files: changeset (+39), new export-business-timezone.test.ts (+296), export-format.ts (+128/−11), rest-server.ts (+25/−4). The rest-server.ts hunks are rowsToCsv (:1700) and the export handler (:8019, :8140-8160) — ⛔ nowhere near DATA_RECORD_READ_PARAMS (:1515), so #8039's region is untouched and that hand-off stays clean. No release-owned docs.

    The load-bearing premise held. resolveExecCtx really does return an ExecutionContext whose timezone comes from the platform-default → global → tenant cascade; the formatter simply never asked. That was the one claim I told the dev to verify before anything else, because the card's whole shape depended on it.

    ⭐ The most important judgement here is a NARROWING, and it is right

    The card says the defect covers "全部含 date/datetime 列的导出". The dev shipped datetime only, and deliberately left date alone. I verified the justification independently rather than taking it:

    docs/adr/0053-date-and-datetime-semantics.md — its title is "date is a timezone-naive calendar day; datetime is an instant rendered in a reference timezone", :37 defines date as a timezone-naive YYYY-MM-DD, :27 says it is never converted to an instant, and driver-sql's toDateOnly (sql-driver.ts:8185) is the source of truth the filter/write/read paths share.

    ⇒ Projecting a date through a zone would move 2026-08-01 to 2026-07-31 for every deployment west of UTC — inventing the exact off-by-one-day defect ADR-0053 exists to remove. Following the card's wording literally would have shipped a new bug alongside the fix. The customer's own evidence table is datetime-only (扫码时间 / 下工时间 / created_at), so nothing reported is left unfixed.

    The tests pin the dangerous direction explicitly: America/New_York and Pacific/Honolulu must not pull 2026-08-01 back a day.

    The fixtures do the thing I asked for, and the header says why

    Every datetime fixture straddles a month boundary (2026-07-31T22:00Z ↔ 2026-08-01 06:00 +08), never a comfortable mid-day instant — because "a test written at 12:00 would have passed both before and after the fix while the reported symptom survived untouched." That is the difference between testing the offset and testing the harm.

    Also worth recording:

    • hourCycle: 'h23', not hour12: false — so midnight reads 00, never 24. Pinned by its own case. That is a real ICU trap and it was anticipated rather than discovered.
    • DST comes from the tz database, via formatToParts, not hand-rolled offset arithmetic — with a test asserting the same zone on both sides of the US DST boundary (−05:00 then −04:00). The same primitive @objectstack/core's calendarPartsInTz and the autonumber date tokens already use, so this aligns with an existing platform semantic rather than minting one.
    • An unknown zone degrades to UTC rather than failing the export — memoized as null so a bad config costs one construction attempt, not one per cell. The throughput reasoning (a 50k-row export formats one cell per datetime column per row) is stated rather than assumed.
    • CSV and XLSX asserted to agree cell for cell, over the real route, real ObjectQL, real sqlite :memory:, with only resolveExecCtx stubbed.

    One scope note, flagged by the dev rather than hidden

    JSON was included, which is one line beyond my dispatch's "CSV/XLSX writers only" wording. It is correct and I would have written the brief that way had I looked: JSON shares formatCellValue through formatRowForJson, so leaving it out would have recreated the very "one export, two clocks" contradiction the card cites as its evidence — this time inside a single route. Flagged in the report and in the PR. That is the standard.

    The ratchet lesson landed

    check:type-check-debt went red on the first run at +1 (TS2554, registry.registerObject missing packageId in the new test) and was fixed at the source — switching to the engine.registerObject facade — with the ledger untouched. The test file even carries the reasoning in a comment: "this package's test layer sits at its check:type-check-debt ceiling." The brief's warning did its job one card after the failure that produced it.

    ⚠️ #8485 deserves attention beyond a normal finding

    Both out-of-scope findings are verified filed, and the second is sharper than its label suggests:

    ⚠️ The honest part the dev stated itself: this PR changes which deployments happen to be lucky. The reporter's box (OS_LOCALIZATION_TIMEZONE=Asia/Shanghai, container TZ=Asia/Shanghai) gets a round trip that now works. A box running TZ=UTC with a non-UTC business timezone will newly export +08 wall-clock and re-import it as UTC. Pre-existing defect, genuinely out of this card's scope — but it is now reachable by a different population, and it is the same customer workflow. Surfaced to the maintainer in the round report; recommending it be graded alongside this card rather than in the ordinary findings queue.

    Landing

    On convergence — ESLint and TypeScript Type Check both conclusion: success — mark ready, then enable_pr_auto_merge. #8039 is queued behind this on rest-server.ts.


    Generated by Claude Code

  8. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    已落地并结单 —— PR #8487 merged。domain:cli 座位 (#6024) 评分。

    落地按本车道的探针纪律复验:探针取 PR 新增的 packages/rest/src/export-business-timezone.test.ts(status added),在 refs/heads/main 上存在;同一次目录列举里同时看到 export-format.ts / rest-server.ts 作为同批正对照,证明列举本身有效。不是拿一个改前就为真的东西当探针。

    评级:ACCEPT

    这张卡的核心价值不在改动本身,在于 dev 拒绝了卡片的字面要求。

    卡片标题和正文写的是「日期时间列」,字面读法是 date 和 datetime 一起按业务时区渲染。dev 只改了 datetime,并把 date 分支显式保留为 UTC 日历日 —— 依据 ADR-0053:date 是时区无关的日历日,datetime 才是「渲染在参考时区里的瞬间」。

    这个收窄是对的,而且是避免了一个新缺陷而非省了工作量:把 date-only 值投影过时区,会把 2026-08-01 推成 2026-07-31 —— 对每一个 UTC 以西的部署 —— 正是 ADR-0053 当初设立就是为了消除的那个差一天缺陷。照卡片字面实现,交付的会是一个用新 off-by-one 换掉旧 off-by-one 的补丁。本座位独立复核过 ADR-0053(:27 「never converting it to an instant」、:37 tz-naive YYYY-MM-DD),确认收窄成立。

    收窄本身也被钉住了:Asia/Shanghai、America/New_York、Pacific/Honolulu 三个方向都断言 date 不动。

    其余做对的地方

    • 夹具跨的是月边界,不是日边界。 测试注释自陈得很清楚:写在 12:00 的测试改前改后都会绿,而症状原封不动。这正是「不可能失败的检查」的反面。
    • 反向验证方向先预测后测量,9 红 9 绿,且红的恰好是业务时区断言、绿的恰好是 UTC 回退与 date-naive 钉子 —— 因为后两类断言的就是改前行为。
    • 三个 writer 全覆盖。 派工只点名了 CSV/XLSX,dev 发现 JSON 经 formatRowForJson 共用同一个 formatter,一并修了并在 PR 里显式说明为何越出派工范围 —— 留下 JSON 会在同一个 route 内部重造这张卡引为证据的「一次导出,两个时钟」矛盾。声明后越界,不是静默越界。
    • hourCycle: 'h23' 而不是 hour12: false —— 午夜必须读作 00 而非 24,并有专门一条测试钉住。这是 Intl 上的经典坑。
    • 无时区 ⇒ UTC 逐字节不变,作为向后兼容承诺双向钉住;平台不认识的时区降级而非让导出失败。
    • check:type-check-debt 的 +1 修在源头(测试里 registry.registerObject 缺 packageId),没有抬账本。这是本车道反复强调的那条。

    顺带解除的串行

    PR 同时改了 packages/rest/src/rest-server.ts(+25/-4,route 里读 context.timezone 并往下传)。该文件的热点串行队列就此释放 —— #8264、#8039 解除阻塞,本座位下一轮排期。

    衍生卡

    dev 归档了两张范围外发现,均未夹带进本 PR:


    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

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions