Skip to content

fix(rest): stamp the export download filename in the business timezone - #8729

Merged
qq9340100 merged 1 commit into
mainfrom
claude/issue-8484-export-filename-business-timezone
Aug 14, 2026
Merged

fix(rest): stamp the export download filename in the business timezone#8729
qq9340100 merged 1 commit into
mainfrom
claude/issue-8484-export-filename-business-timezone

Conversation

@qq9340100

Copy link
Copy Markdown
Collaborator

Fixes#8484

exportContentDisposition built the -YYYYMMDD-HHMMSS half of the export download's suggested filename from process-local getters (now.getFullYear() / getHours() / …), which read the deployment host's TZ — a hosting fact, not the caller's business timezone. After #8373 moved the export's contents onto the business timezone, the filename was the last export surface still on the host clock, so the name and the rows inside it disagreed exactly when TZ was not the business zone: a container at TZ=UTC serving an Asia/Shanghai tenant downloaded orders-20260731-220000.csv whose first row read 2026-08-01 06:00:00 — off by a day, and across a month boundary by a month.

The fix

The route had already resolved the business timezone 42 lines above the call — const timezone = … off ExecutionContext.timezone (the platform-default to global to tenant cascade), introduced by #8373 for the cells. It is now passed to exportContentDisposition as well, so the name and the contents read one clock. No new resolution path, and nothing else in rest-server.ts touched.

The shared clock helper is split rather than given a default: zonedWallClock now returns null when no usable zone resolves, and each of the two callers supplies its own fallback at the call site. wallClock (the cell path) keeps its UTC fallback; the filename keeps process-local. Baking either into the shared helper would silently re-time the other surface.

⚠️ The no-timezone fallback stays PROCESS-LOCAL, not UTC

This inverts the intuitive answer and is the whole risk on this card. Each fallback preserves the historical output of its own surface, and the two have different histories: the cells were hardcoded to UTC before #8373; this filename has always used the process clock. "UTC is the safe default" would in fact re-time the filename of every deployment that sets a host TZ but resolves no business timezone — a silent change to a user-visible name, for zero correctness gain. An explicitly resolved 'UTC' is a resolved zone, not a missing one, and does produce a UTC stamp regardless of the host.

Tests

The five existing pins keep their exact stamps, and that is deliberate rather than an oversight: NOW is built from local calendar components, so the local getters read the same components back under any host zone — pinning 20260714-153045 is a host-zone-independent statement of the process-local fallback. What moved is the arity (timezone sits between ext and the injected clock), so a caller that forgets it gets a type error rather than a silently re-timed filename.

New cases cover the zone being read, the day/month boundary crossing (the issue's own scenario), agreement with the datetime cells, an unresolvable zone, and explicitly-resolved 'UTC'.

The fallback pins own a skewed process TZ for their duration. Without that they would be vacuously green: packages/rest is not in CI's skewed-zone job, so on GitHub's UTC runners process-local and UTC are the same stamp and flipping the fallback would pass. One case asserts the zone actually took, mirroring the same assert-the-axis-is-real discipline CI applies to its own skewed-zone job.

Reverse verification (fix committed first, then ablated, then restored byte-identically): flipping the fallback to UTC turns exactly the two process-local pins red — expected '20260801-020000' to be '20260801-100000' — while the explicit-zone, resolved-'UTC' and five naming pins stay green. Predicted direction before running; observed direction matched.

Verification

All at head d82240dd4, tree clean:

  • pnpm --filter @objectstack/rest test115 files, 1910 tests passed (includes rest.test.ts's route-level Content-Disposition assertions, which match \d{8}-\d{6} and are zone-agnostic)
  • pnpm --filter @objectstack/rest typecheck — clean
  • Build closure: pnpm --workspace-concurrency=2 --filter '@objectstack/rest^...' build, then the full workspace closure (70 tasks) for the ratchet
  • Named gates: check:cross-package-test-inputs (+ the .mjs form), check:query-options-erasure, check:type-check-coverage, check:type-check-debt (--re-measure, 33 entries, none above ceiling), check:nul-bytes
  • Added after re-deriving against the actual diff (scripts/pm/dispatch-gates.mjs): check:authz-resolver and check:route-envelope (pulled in by the rest-server.ts line), plus check:changeset-gate-self-tests, check:objectui-changeset, check-adr-0087-registration.mjs, check-changeset-no-major.mjs, check-empty-changeset.mjs (pulled in by the new changeset, which did not exist when the dispatch list was taken). All green.

route-envelope reports a pre-existing ticks-down-only ratchet on rest-server.ts; this change adds no response site and the gate passes. The @objectstack/lint TEST_DEBT surplus it prints is pre-existing, unrelated, and explicitly not an error — left alone as out of scope.

Scope held: the filename stamp only. The file's contents are already correct after #8373 and are untouched; the import half (#8485) is not addressed here and remains open.


Generated by Claude Code

#8484)
exportContentDisposition built the -YYYYMMDD-HHMMSS stamp from process-local
getters, which read the deployment host's TZ rather than the caller's business
timezone. After #8373 moved the export's contents onto the business timezone,
the filename was the last export surface still on the host clock, so the name
and the rows inside disagreed whenever TZ was not the business zone.
The route had already resolved ExecutionContext.timezone one frame up for the
cells; it is now threaded to the filename too, so both read one clock.
The no-timezone fallback stays PROCESS-LOCAL, deliberately not UTC: each
surface keeps its own historical output, and defaulting to UTC would silently
re-time the filename of every deployment with a host TZ set. zonedWallClock now
returns null when no usable zone resolves so each caller supplies its own
fallback; the fallback pins own a skewed process TZ so they cannot pass
vacuously on a UTC runner.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NaS1PAHJcPfAA2acnV53Tn
@vercel

vercelBot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 14, 2026 4:39pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/rest.

9 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/connect-mcp.mdx(via @objectstack/rest)
  • content/docs/api/error-handling-server.mdx(via @objectstack/rest)
  • content/docs/api/index.mdx(via @objectstack/rest)
  • content/docs/permissions/authentication.mdx(via @objectstack/rest)
  • content/docs/permissions/system-context.mdx(via packages/rest)
  • content/docs/plugins/index.mdx(via @objectstack/rest)
  • content/docs/plugins/packages.mdx(via @objectstack/rest)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/rest)
  • content/docs/protocol/kernel/i18n-standard.mdx(via packages/rest)

3 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/rest)
  • content/docs/releases/v12.mdx(via @objectstack/rest)
  • content/docs/releases/v17.mdx(via @objectstack/rest)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 14, 2026
@qq9340100
qq9340100 marked this pull request as ready for review August 14, 2026 17:00
@qq9340100
qq9340100 added this pull request to the merge queueAug 14, 2026
Merged via the queue into main with commit 3ab2488Aug 14, 2026
27 checks passed
@qq9340100
qq9340100 deleted the claude/issue-8484-export-filename-business-timezone branch August 14, 2026 17:19
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Export download filename is stamped in the process-local timezone, not the business timezone

2 participants

@qq9340100@claude