0de152fa

ical: refuse an out-of-range rule part value

The last of the parser findings. Nine numeric rule parts accepted out-of-range values and handed back a `RecurrenceRule` that fails its own `Validity`:

| rule part | accepted, invalid |
| --- | --- |
| `INTERVAL` | `0` |
| `BYSECOND` | `61` |
| `BYMINUTE` | `60` |
| `BYHOUR` | `24` |
| `BYMONTHDAY` | `32`, `0` |
| `BYYEARDAY` | `367`, `0` |
| `BYWEEKNO` | `54`, `0` |
| `BYSETPOS` | `367`, `0` |
| `BYDAY` | `54MO` |

That breaks the invariant the rest of the codebase leans on: a value that came out of a parser is supposed to be valid.

## Why nothing noticed

`recurrenceRulePartSpec` already has a `parses only valid things` property, which looks like it should have caught this. It renders an already-valid value and feeds *that* back, so the parser only ever sees text some valid value produced — out-of-range text never arrives. Hence the new `recurrenceRulePartRefusesSpec`, which hands it text directly.

## The range is not restated

Each of these types already declares its range in its `Validity` instance, so `requireInRange` asks that:

```haskell
requireInRange name val part =
  if isValid part
    then pure part
    else unfixableError $ RecurrenceRulePartOutOfRange name val
```

Tighten a `Validity` later and the parser follows automatically. That is also **why this had to come last**: the check inherits whatever `Validity` says, so a wrong bound becomes a wrong refusal. Before #33 and #35 this would have started refusing `COUNT=100` and `BYDAY=6MO`, both ordinary values.

## Two decisions worth reviewing

**Refused, not repaired.** The spec says which values are valid but not what to do with one that is not. Dropping the value would empty the rule part — and for a part that *limits* rather than *expands*, an empty part means no restriction at all, so a forbidden value would widen the recurrence set. That is the same shape as the BYDAY widening fixed in #32. `BYMONTH` already refused out-of-range values, so refusing matches what the module did anyway.

I had earlier leaned toward a fixable error that ignores the part, and retract that: the RFC's "MUST be ignored" sentence is about `BYSECOND`/`BYMINUTE`/`BYHOUR` appearing with a DATE-valued `DTSTART`, not about out-of-range values, so it does not license ignoring them.

**Per rule part, not per rule.** `Validity RecurrenceRule` also rules on *combinations* — a numeric `BYDAY` at a frequency that forbids one, `BYWEEKNO` outside `YEARLY`, `BYSETPOS` without a companion. Those are fixable errors raised while recurring, and checking the whole rule here would refuse them outright and undo #32. So the check is on the part only.

The check lives in the parsers rather than in `parseRecurrenceRule`, so `recurrenceRulePartP` cannot hand back an invalid value either. It is the primitive; the invariant belongs there.

## `BYDAY=54MO` is new

Before #35 it was refused for the wrong reason — the ordinal was read one digit at a time, so `54MO` failed on `"4MO"` rather than for being out of range. #35 taught it to read two digits, which dropped `54MO` into this same hole. This closes it.

## Checks

`ical-gen` 1088 → 1109 passing, `ical-recurrence-gen` 301, both 0 failing. `nix flake check` passes.