6f7822c2

ical-recurrence: ignore a generated leap second whatever DTSTART is

`FREQ=MINUTELY;BYSECOND=60;COUNT=3` produced a different recurrence set depending only on `DTSTART`'s value type:

| DTSTART | occurrences |
| --- | --- |
| floating | `12:00:00`, `12:00:60`, `12:01:60` — all three kept |
| zoned `+01:00` | `2020-01-01 12:00:00`, **`2020-01-02 00:59:60`** — a different day |

## Why

RFC 5545 §3.3.10 makes the input legal:

> The BYSECOND rule part specifies a COMMA-separated list of seconds
> within a minute.  Valid values are 0 to 60.

and `bySecondExpand` puts the 60 straight into a `TimeOfDay` without normalising, so a rule really can generate a local time no wall clock ever shows. The same section says what to do with one:

> Recurrence rules may generate recurrence instances with an invalid
> date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM
> on a day where the local time is moved forward by an hour at 1:00
> AM).  Such recurrence instances MUST be ignored and MUST NOT be
> counted as part of the recurrence set.

`localTimeExists` is the predicate that implements that, but it answered `const True` for anything that is not a zoned DATE-TIME, because the resolve/unresolve round trip it uses needs a time zone. So a floating or UTC `DTSTART` never had its instances checked at all.

The zoned column is odder than it looks. Every leap second is dropped *except* the one landing on the UTC leap-second slot — in a `+01:00` zone that is `00:59:60` the next day, the exact analogue of `23:59:60` at a zero offset. That one survives the round trip, so `localTimeExists` calls it existent.

## The fix

A leap second needs no time zone to rule out, so it is now ruled out in every branch:

```haskell
isLeapSecond :: Time.LocalTime -> Bool
isLeapSecond = (>= 60) . Time.todSec . Time.localTimeOfDay
```

Honouring second 60 was never coherent. Whether such a local time survives resolving and unresolving depends on the time of day *and* the offset, so the recurrence set came to depend on facts about neither the rule nor the calendar.

## The tradeoff, worth a look

**`BYSECOND=60` now generates no instance at all, ever.** A spec-legal rule part becomes inert.

I think that is the honest outcome: real leap seconds are announced by IERS for particular dates, and nothing here knows which, so any instance we did generate would be fiction. The alternative is a fixable error at parse time, so the rule part is refused loudly rather than ignored silently — that is a larger change in `ical` and I did not take it. Easy to add later if you would rather it were loud.

## Two commits

1. **Red.** Both cases fail, and the floating one prints `12:00:60` and `12:01:60` surviving. The tests assert **local** starts rather than resolved instants, because resolving is exactly what mangles a leap second and would hide what is under test.
2. **Green.**

## One incidental cleanup

`ICal.Recurrence.TimeZoneSpec` had its own copy of this predicate as a let-binding, added in #31. It now uses the exported one, so the concept has a single definition.

That does not make that property tautological: `isLeapSecond` is a syntactic check on the local time, not something derived from the round trip, and the three concrete cases in that file still pin what actually happens to a leap second independently.

## Checks

`ical-recurrence-gen` 299 → 301 passing, 0 failing, 1 pending (the deliberate one). `ical-gen` unchanged at 1077. `nix flake check` passes.

Independent of #33 — disjoint files, so no rebase needed either way.

Suite timing

Time to Start Worker time Duration Time to finish Idle
Config 11s 2s 2s 13s 11s
Eval 25s 10s 10s 35s 11s
Build 35s 2m04s 31m59s 32m34s 29m54s
Suite 11s 2m17s 32m23s 32m34s 30m17s

Timeline

0s30s30m40s30m50s31m31m10s31m20s31m30s31m40s31m50s32m32m10s32m20s32m30s