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.