An occurrence with no stated end runs until the next one opens. That is the
boundary a bare rotation was missing — docs/SOURCES.md declines to publish
arustats' Abyss openings precisely because nothing bounded them — and here
it is entailed by the interval the reader typed rather than invented for
them. The store still holds endsAt: null; only this projection resolves it.
nextOccurrences returns what has not finished rather than what is running,
so a rule between cycles answers "opens Saturday" instead of vanishing for
its whole off week.
The "ancient anchor" test's expected occurrences now include 31 August: with
a six-year-old anchor at 09:00 and a query window opening at midnight, that
day's occurrence (no stated end, so it runs until 1 September 09:00 opens
the next) genuinely overlaps the window's first nine hours — the same edge
rule the sibling "overlapping at either edge" test exists to prove. The
brief's original expected list omitted it.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Replace the local-vs-UTC test with a timestamp that actually straddles UTC
day boundary (00:30 local on 2 September = 22:30Z on the 1st). The original
test was vacuous because both readings agreed on the same calendar day.
Delete the rename-stability test which was a tautology — occurrenceId never
takes a title, so any pure function satisfied it. The real assertion belongs
in a later task where the whole rule is passed in and a rename can be
exercised.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
myevent:<token>#<YYYY-MM-DD>. The token says which recurring thing, the
local day says which time round, and marks, ignores, progress and daily
ticks all key off the whole string — so an occurrence carries its own
completion and its own streak rather than sharing the rule's.
'#' is outside [a-z0-9] and therefore outside CustomEventId, so an
occurrence cannot be written back into the store or survive an import.
That is the guardrail rather than a code path anybody has to remember, and
a test pins it in both directions.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The app can express a window closing and a day repeating; daily.ts counts
in days and stops there. This is the arithmetic under the rung between
them.
Local wall-clock rather than milliseconds, because a reader's own event is
local throughout — a weekly reset set for 09:00 stays at 09:00 across a DST
transition, where adding 7*DAY would move it an hour and drag every later
occurrence with it. Months clamp to the last valid day rather than letting
setMonth roll 31 February into 3 March, which is the same silent shift
readerInstant already refuses on the way in.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>