---
title: Pin the draft-window slot behavior on a nonexistent DST hour
type: task
status: planned
description: >-
  The published-daily-slate rewrite removed the offset-latching walker entirely, closing this task's
  first three items. The residual: [0, 24) is now a real production band, so a slot hour that does
  not exist on a spring-forward date needs a pinned behavior instead of dayjs.tz's incidental
  forward resolution.
base_uri: user:task/league/fix-draft-window-dst-offset-latching.md
created_at: '2026-08-13T02:40:18.837Z'
entity_id: 344a7fe3-feff-4816-9c00-20519b0b1a5f
owner_identity_uri: █████████████████████████
priority: Low
public_read: true
relations:
  - relates [[█████████████████████████-█████-███████-██-█████████-█████-████████]]
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-08-16T22:38:47.408Z'
visibility_analyzed_at: '2026-11-05T23:49:22.039Z'
---

# Pin the draft-window slot behavior on a nonexistent DST hour

## Background

**Everything in this section is HISTORY as of 2026-08-16.** The walker it describes no longer exists:
the published-daily-slate rewrite replaced `get_draft_window` outright, so the offset-latching defect
is gone by construction rather than repaired. It is kept because it is the evidence for why the
residual below is worth pinning — the same timezone library is still what resolves every slot.

`libs-shared/get-draft-window.mjs` builds each window by stepping a timezone-aware dayjs object one
hour at a time through `advance_to_open_hour` / `advance_one_step`. The dayjs timezone plugin latches
the UTC offset at the moment the object is BUILT and `.add(1, 'hour')` never re-resolves it, so every
window computed across a DST transition reports a wall-clock hour that is an hour off the true
America/New_York wall clock.

This is the same defect the repo `CLAUDE.md` documents for `.add(n, 'day')` followed by `.hour(h)` in
the restricted-free-agency window code, in a different function that was never swept.

Measured 2026-08-13 against the real `getDraftWindow`, anchor Fri Oct 30 11:00 ET, hourly cadence with
interval 4:

| pick | formatted | `.hour()` | true New York wall clock |
| --- | --- | --- | --- |
| 8 | Sun 11-01 11:00 -04:00 | 11 | Sun 11/01 **10:00** |
| 9 | Sun 11-01 15:00 -04:00 | 15 | Sun 11/01 **14:00** |
| 11 | Mon 11-02 11:00 -05:00 | 11 | Mon 11/02 11:00 |

Pick 8's window is truly 10:00 ET — **an hour before the daily band opens**, which is a window the
daily-window gate should have made impossible. Spring-forward runs the other way (a window reported at
19:00 is truly 20:00). Under `'day'` cadence it is worse: the error persists for every subsequent pick
rather than correcting at the next day boundary.

Two mitigating facts, neither of which closes it. The open-hour COUNT for the live band `[11, 23)` is
correct across both transitions, because the duplicated and skipped hours are 01:00/02:00 and fall
outside the band. But the invalid-config fallback in `resolve_daily_window` is `[0, 24)`, which does
straddle the transition — a misconfigured league lands exactly on the unsafe band.

Found while reviewing the league pause primitive's draft-clock design. Not caused by that work and not
fixed by it.

## Assumptions

- **The defect is in the walker, not the counter.** Verified by executing `getDraftWindow` across the
  November 2026 transition and comparing `.hour()` against the true wall clock. Check: re-run that
  comparison.
- **Live league 1 config is `'hour'` cadence with band `[11, 23)`**, so the count is currently correct
  and only the reported window instants are wrong. Check: read `seasons` for the current year.
- **No 2026 rookie draft window crosses a DST transition**, since the draft runs in August. The defect
  is latent for this season and live for any draft or window feature that spans early November.
  Check: compare `draft_start` plus the projected final window against the transition date.

## Plan

The first three items are CLOSED by
[[█████████████████████████-█████-███████-██-█████████-█████-████████]], which replaced the walker
outright rather than repairing it. That change shipped in `07352afbd` (merged `f52e754d3`) and is
deployed to both hosts.

- [x] Reproduce the table above as a failing spec in `test/common.get-draft-window.spec.mjs`,
      asserting true wall-clock hours across both the November and March transitions. **Closed by the
      slate rewrite**, whose spec covers wall-clock hours across both transitions.
- [x] Rebuild the hour walk so the offset is re-resolved after each step. **Closed differently than
      planned: there is no hour walk left.** The slate calculator derives each slot directly from its
      `(date, hour)` pair as a fresh timezone-aware instant, so no dayjs object is ever advanced
      across a transition and there is no latched offset to re-resolve. That is a stronger fix than
      the re-resolving walk this item specified — the defect is removed by construction rather than
      corrected per step.
- [x] Check the `'day'` cadence path separately, where the error compounds across picks. **Closed by
      removal.** The calculator no longer reads `seasons.draft_type` and takes hours unconditionally;
      the four `draft_type = 'day'` rows migrated to `draft_pick_interval = 24` on their existing
      `[0, 24)` band. There is no separate day-cadence path left to compound an error.
- [ ] Pin the nonexistent-hour behavior explicitly rather than inheriting it from `dayjs.tz` — see
      the Residual section below for why this is the only item left. Decide whether such a slot
      resolves forward (to 03:00), backward (to 01:00), or is skipped for that day, and state the
      decision in a code comment beside the slot construction. — verify: a spec constructing a
      `[0, 24)` band with an interval that steps onto 02:00 on a spring-forward date, asserting the
      chosen instant; proven red against a build with the opposite resolution.

## Residual: a nonexistent spring-forward slot hour

**Open, and the only thing left in this task.** The fourth item — whether to keep the `[0, 24)`
invalid-config fallback — is superseded by a bigger fact: **`[0, 24)` is now a REAL production band,
not just an invalid-config fallback.** The four historical `draft_type = 'day'` seasons carry it, and
nothing prevents a future election from choosing it.

A full-day band places slots at hours that do not EXIST on a spring-forward date. In
America/New_York, 02:00 does not occur on the second Sunday in March. Today `dayjs.tz` resolves such
an hour forward to 03:00, and **nothing asserts that behavior** — it is incidental library behavior
that a dayjs upgrade could change silently.

**There is no live exposure, which is why this is a residual and not a defect.** No elected band
currently puts a slot at 02:00: the live league 1 band is `[11, 24)` with an interval of 3, giving
slots at 11, 14, 17, 20 and 23. The four `[0, 24)` seasons run at a 24-hour interval, so their single
daily slot is midnight. A slot lands on 02:00 only under a band including it AND an interval that
steps onto it.

## Notes

The two Assumptions above about live config are stale and kept only as the record of what was
measured on 2026-08-13. League 1 is no longer `'hour'` cadence on `[11, 23)`: it runs the slate at
`draft_pick_interval = 3` on `[11, 24)`, and `seasons.draft_type` is no longer read by the
calculator at all.

The third assumption still holds and is what bounds the residual: the 2026 rookie draft runs in
August, so no window this season comes near a DST transition.

Sequencing: nothing is time-boxed. The residual is latent by construction — it needs a band and an
interval that place a slot on a nonexistent hour, and no elected configuration does.
