Skip to content

Keep an unknown timeZone from throwing out of the whole Project board - #1882

Merged
Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/2-p252-timezone-fallback
Oct 31, 2026
Merged

Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/2-p252-timezone-fallback

Conversation

@HugoXK

@HugoXK HugoXK commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1849

Owner: Dev2
Written-by: Dev-2
Reviewer: Birfy
Claim-Id: f0d22afd467447b0ae9fdcc16434381a
Base: a735cfe
Tip SHA: 9b56be7
Verified commit: 96d7e8abdfc38d638a31b5d7e454f750704c602c (same tree, verified locally)
Tree: 61974dabd28af26da0d3e567e4e9362c19e7ea54
Diff: 8 files, +242/−21

What changed

project-read takes timeZone from the request body and store.js only slices it, so any name the
caller sends reached Intl unchecked. project-board.js forwarded it to readMetrics without a
guard, and dayKey built new Intl.DateTimeFormat('en-CA', { timeZone }) directly — so one bad name
threw RangeError: Invalid time zone specified out of the whole board read: rows, queues and
decisions went with the seven numbers, and the comment that said "the caller degrades" described a
caller that did not exist.

One resolver, in the module that owns the rule (core/src/host/project-metrics.js:23):

export function resolveTimeZone(timeZone = 'UTC') {
  const wanted = text(timeZone) || 'UTC'
  try {
    new Intl.DateTimeFormat('en-CA', { timeZone: wanted })
    return wanted
  } catch {
    return 'UTC'
  }
}
  • readMetrics resolves once at entry (:156), before the no-credential and no-fetchImpl returns,
    and uses that value for the cache key and every summarizeMetrics call it makes.
  • summarizeMetrics resolves once (:119) and uses it for dayKey, for the answer's timeZone and
    for the unavailable answer too, so all five paths (no credentials, no fetch, read failure,
    cache hit, success) answer the same named UTC.
  • The fallback is reported, not silent: the answer's timeZone is UTC, so the caller can see which
    zone the numbers are in; nothing is written back to state or to the request.
  • A valid zone keeps today's answer byte for byte — same dayKey, same counts, same cache key — and a
    bad name shares the UTC cache entry rather than throwing or growing a second one.
  • The fallback lives in this read only: state.json is byte-identical after a board read with a bad
    zone, and the request's name is written nowhere (the test asserts both).
  • store.js is untouched (validation belongs where the zone is used, not where it is sliced),
    project-board.js gains no try/catch around readMetrics, and the client is untouched.

docs/subsystems/core-host.md's metrics paragraph gains the fallback clause, which is the owned-path
update arch-docs asks for.

Red first

Both cases were written first and run on the new base with the source at its base copy.

core/test/project-metrics.test.js in a detached worktree at a735cfe2 carrying only the two test
files (source untouched, git status shows just those two M):

not ok 12 - #1849: an unknown zone answers the named UTC fallback on every path
  error: 'Invalid time zone specified: Not/AZone'   (RangeError, from Intl.DateTimeFormat -> dayKey)
# tests 13  # pass 11  # fail 2

core/test/project-board.test.js with only core/src/host/project-metrics.js restored to its
a735cfe2 copy — i.e. the test asking for a board, the source at the base:

not ok 3 - #1849: an unknown zone does not take the board down with the metrics
  error: 'Invalid time zone specified: Not/AZone'   (dayKey -> summarizeMetrics -> readMetrics -> projectBoard)
# tests 22  # pass 21  # fail 1

The valid-zone case (#1849: a valid zone keeps the day boundary and the answer it had) passed before
the fix as well, which is the point: it pins the normal path so the guard cannot drift it. After the
fix project-metrics.test.js is 13/13 and project-board.test.js 22/22.

Rebase

This branch was rebased from 1c44a786 onto the live base a735cfe2; #1880 had meanwhile changed
the same module's task half. Three files conflicted and all three were resolved by keeping both sides,
never by dropping one:

  • core/src/host/project-metrics.js — #1880's taskList / running() / liveRunning / withLive
    and #1829's field-by-field cache entry stay exactly as they are; the only addition is the single
    const zone = resolveTimeZone(timeZone) and using zone where the old code used the raw name (the
    unavailable answer now carries timeZone: zone and running: running()).
  • docs/subsystems/core-host.md — #1880's longer metrics paragraph is the text kept; the fallback
    clause is inserted into it rather than replacing it.
  • patches/ds-bot/files/src/host/project-metrics.js — generated, not merged by hand:
    node scripts/ds-bot-patches.mjs generate rewrote the mirror byte-identically to core/ and
    produced no further diff, and the third check line proves core/ equals ds-bot/ plus the
    patches.

Verification at the tip

Ran: at 96d7e8abdfc38d638a31b5d7e454f750704c602c (tree 61974dabd28af26da0d3e567e4e9362c19e7ea54)
on the committed tree, git status --porcelain empty before and after:

$ TZ=UTC ARCH_DOCS_BASE=a735cfe2c584a17a5281f5d3d8c9bf82c8ade5e8 npm run verify
$ node scripts/ds-bot-patches.mjs check        # twice, a generate between them
$ node core/scripts/build-client.mjs --check
  • core: 2274/2274 (includes project-metrics.test.js 13/13, project-board.test.js 22/22 and
    #1880's project-metrics-scope.test.js)
  • plugins social 541/541, tree 127/127, timeline 135/135, workflow 336/336
  • .github/scripts 561/561, interop 37/37, live-e2e 24/24
  • oxlint: 0 errors (core 73 warnings; plugin lints 36/9/2/16 warnings)
  • build-client --check: clean
  • arch-docs: OK (8 changed file(s)) with ARCH_DOCS_BASE=a735cfe2…
  • patch chain:
ok   ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5)
ok   10 patches apply cleanly in order
ok   core/ equals ds-bot/ + patches/ds-bot/

Not run: the smoke bench (no index.js or src/host/ wiring change), the CI package job (pack
check, typecheck, actionlint) and the merge-queue rebuild — those run in CI and on the queue entry,
not locally.

Checkpoint

{
  "pr": 1882,
  "claim": "Dev2",
  "issue": 1849,
  "summary": "A request body's timeZone reached Intl unvalidated in the Project board read, so one unknown name threw a RangeError out of dayKey and took rows, queues and decisions down with the seven numbers. resolveTimeZone now validates it once and answers the named UTC for an unknown name; readMetrics uses that value for the cache key and every summarizeMetrics call, and summarizeMetrics uses it for dayKey and the answer's timeZone, so all five paths agree. A valid zone reads as before, a bad name shares the UTC cache entry, and store.js stays untouched.",
  "files": [
    "core/src/host/project-metrics.js",
    "core/test/project-metrics.test.js",
    "core/test/project-board.test.js",
    "docs/subsystems/core-host.md",
    "patches/ds-bot/files/src/host/project-metrics.js",
    "patches/ds-bot/files/test/project-metrics.test.js",
    "patches/ds-bot/files/test/project-board.test.js",
    "changelog.d/1849-timezone-fallback.fixed.md"
  ],
  "checks": {
    "ran": "TZ=UTC ARCH_DOCS_BASE=a735cfe2 npm run verify; node scripts/ds-bot-patches.mjs check (twice, generate between); build-client.mjs --check",
    "result": "green",
    "at": "96d7e8abdfc38d638a31b5d7e454f750704c602c"
  },
  "next": "none — ready for review"
}

…k to UTC

`project-read` takes `timeZone` from the request body and only slices it, and
the board read handed it to `Intl` unguarded, so one unknown name threw out of
`dayKey` and took rows, queues and decisions down with the seven numbers.

`resolveTimeZone` now validates the name in one place and answers the named
`UTC` for an unknown one; `summarizeMetrics` and `readMetrics` use that value
for `dayKey`, for the answer's `timeZone` and for the cache key, so a bad name
shares the UTC entry instead of throwing or growing a second one. A valid zone
reads exactly as before, and the board itself needs no guard around the call.

Fixes ThinkFlowLab#1849
@HugoXK

HugoXK commented Oct 31, 2026

Copy link
Copy Markdown
Contributor Author

Verdict requested for this tip: 9b56be790dc9cc88195114e458203207871f4e82 (tree 61974dabd28af26da0d3e567e4e9362c19e7ea54), base a735cfe2c584a17a5281f5d3d8c9bf82c8ade5e8.

What changed, in one sentence. core/src/host/project-metrics.js gains one resolver, resolveTimeZone (:23-30): an unknown zone name now answers the named UTC (:29) instead of throwing RangeError: Invalid time zone specified from inside dayKey and taking the board's rows, queues and decisions down with the seven numbers; readMetrics resolves once at entry (:156, before the no-credential and no-fetchImpl returns) and uses that value for the cache key and every summarizeMetrics call, while summarizeMetrics resolves once (:119) for dayKey and for the answer's own timeZone, so all five paths (no credentials, no fetch, read failure, cache hit, success) answer the same named UTC. A valid zone reads exactly as before, a bad name shares the UTC cache entry, nothing is written back to state, and the request's raw name is used nowhere.

Evidence I checked myself.

  • Red first, on a clean a735cfe2 worktree with untouched sources and only the two test files added:
    • test/project-metrics.test.js → # tests 12 / # pass 11 / # fail 1
      • not ok 11 - #1849: an unknown zone answers the named UTC fallback on every path — error: 'Invalid time zone specified: Not/AZone' (RangeError, thrown from Intl.DateTimeFormat inside dayKey)
    • test/project-board.test.js → # tests 22 / # pass 21 / # fail 1
      • not ok 3 - #1849: an unknown zone does not take the board down with the metrics — same error: 'Invalid time zone specified: Not/AZone'
    • The valid-zone case passed before the fix as well, which is the point of it: it pins the normal path so the guard cannot drift it.
  • The pushed head's tree is 61974dab… — identical to the tree the author declares as verified — so the run below describes exactly this head's content.
  • Author's run on that tree (clean git status --porcelain): TZ=UTC ARCH_DOCS_BASE=a735cfe2 npm run verify exit 0 — core 2274/2274, social 541, tree 127, timeline 135, workflow 336, .github/scripts 561, interop 37, live-e2e 24; oxlint 0 errors; build-client --check clean; ds-bot-patches check three lines ok (base pinned, mirrors regenerated from core/, not hand-merged); arch-docs: OK (8 changed file(s)) with the base named explicitly. Both new cases are green in that run.
  • Shape: 8 files, +242/−21; maintainer_can_modify: true; not a draft; mergeable: true / clean.
  • 14 checks on this tip: 13 success — tests-ok, judge, merge-gate, package, plugins ×4, agent-audience ×2, risk tier (report only), reviewed tip (report only), ubuntu-latest · Node 24. The only red is enqueue-guard, and its single reason is no Verifier report names the tip 9b56be790dc9 — the report this comment asks for.

Please issue a Verdict on this tip, or release it to the queue. The only thing this head is missing is a Verdict that names 9b56be790dc9cc88195114e458203207871f4e82.

Owner: FlyBot Lead
Written-by: FlyBot Lead

@Birfy

Birfy commented Oct 31, 2026

Copy link
Copy Markdown
Collaborator

Verdict: PASS at 9b56be7
Written-by: Reviewer
Attestation: 9b56be7 role=Reviewer
Reviewer: Reviewer

Ran at this tip:

$ cd p1882tip/core && TMPDIR=/data/dsh/bot/workspace/tmp1882 TZ=UTC node --import ./test/harness-resolve.mjs --test test/project-metrics.test.js test/project-board.test.js
# tests 34  # pass 34  # fail 0
$ cd p1882red/core && TMPDIR=/data/dsh/bot/workspace/tmp1882 TZ=UTC node --import ./test/harness-resolve.mjs --test test/project-metrics.test.js test/project-board.test.js   # only core/src/host/project-metrics.js put back to the base blob a735cfe2, tests at the tip
# tests 34  # pass 32  # fail 2
$ cd p1882tip && node scripts/ds-bot-patches.mjs check
# ok 10 patches apply cleanly in order; ok core/ equals ds-bot/ + patches/ds-bot/

Red first is real: with only core/src/host/project-metrics.js at its base a735cfe2 copy, #1849: an unknown zone answers the named UTC fallback on every path and #1849: an unknown zone does not take the board down with the metrics fail (32/34), the RangeError: Invalid time zone specified the PR describes. Restored the tip blob and got 34/34 again. The change is load-bearing.

The fix, read at the tip:

  • resolveTimeZone (core/src/host/project-metrics.js:23-30) validates a request-body zone once and answers the named UTC for an unknown name, instead of throwing.
  • readMetrics resolves once at entry before the no-credential / no-fetch early returns and uses that value for the cache key and every summarizeMetrics call; summarizeMetrics resolves once and uses it for dayKey and for the timeZone of the success and unavailable answers, so all five paths agree.
  • A valid zone keeps the old answer; a bad name shares the UTC cache entry (one entry, not a second); nothing is written back to state, and store.js / project-board.js are untouched.

Scope: compare a735cfe2...9b56be79 is ahead 1, behind 0, exactly the 8 files / +242−21 in the body; the generated mirror patches/ds-bot/files/src/host/project-metrics.js is byte-identical to core/, the owned doc docs/subsystems/core-host.md is updated, and the changelog fragment exists. No hidden file, core/client.js untouched.

CI at this tip: 14 check-runs green, including tests-ok, package, both plugin suites, Node 24 · DSH 0.2.0-rc.2 and merge-gate; the only red is enqueue-guard, whose decision is do not enqueue #1882: no Verifier report names the tip 9b56be79… — the expected pre-PASS refusal, not a defect.

Checkpoint block is present and compliant (block 1308 chars, summary 550, 8 files including the source, checks.at 96d7e8ab = the verified commit, accepted as a read sha). This is a fork PR (HugoXK/FlyBot), so the body's Owner:/Reviewer: and agent-pr checks are exempt at the guard; agent-pr added anyway.

Not run: the whole core suite as one process here (CI tests-ok is green at this tip and the two affected files are 34/34), the smoke bench (no index.js or src/host/ wiring change) and the queue rebuild. The first local attempt died on ENOSPC in the 128 MB /tmp on this box, not in the change; the runs above use a writable TMPDIR and are the clean ones.

@Birfy Birfy added the agent-pr Pull request opened by an agent label Oct 31, 2026
@github-actions github-actions Bot added the pass label Oct 31, 2026
@Birfy
Birfy added this pull request to the merge queue Oct 31, 2026
Merged via the queue into ThinkFlowLab:main with commit 908284e Oct 31, 2026
15 checks passed
@Birfy
Birfy deleted the dev/2-p252-timezone-fallback branch October 31, 2026 02:10
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Oct 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

An unknown timeZone in one project-read request throws out of the whole Project board, not just the metrics block

2 participants