docs(schedule): state the UNTIL type exception, the density refusal and MaxDuration's real overrun (TASK-4100 3a)

Comment-only, per the lead's 3a final-round ruling:
- doc.go: ParseRule refuses what RFC 5545 forbids EXCEPT an UNTIL whose
  value type differs from DTSTART's, which is accepted as Until defines it
  (C-M1, option B), and the refusal list names the 10,000-candidate density
  cap (H-L3).
- caps.go: MaxDuration is checked between yields, so a call can overrun it by
  one period expansion or one MaxSteps no-match stretch, and MaxSteps has no
  ceiling (H-L1).

Claude-Session: https://claude.ai/code/session_01Pa22hHpfXoZbQkEFvPtNeS
This commit is contained in:
xarmian
2026-10-28 13:13:32 +00:00
parent 230fef4be8
commit 34ef709504
2 changed files with 11 additions and 10 deletions
+5 -5
View File
@@ -18,8 +18,9 @@ type Limits struct {
// candidates it produces (TASK-4100 H1, r2): the count restarts at every
// candidate, so it ends a no-match stretch and nothing else. A stall is a
// CapStall only when an answer could lie past the examined part (r3).
// MaxDuration is checked between yields, bounding a call to within one
// period expansion, limited by maxPeriodCandidates (TASK-4100 final round H1).
// MaxDuration is checked between yields, so a call can overrun it by one
// period expansion (limited by maxPeriodCandidates) or one no-match stretch of
// MaxSteps steps; MaxSteps has no ceiling (TASK-4100 final round H1, 3a H-L1).
// MaxScan additionally refuses a wall-clock query far from Start, except
// for COUNT rules and rules whose UNTIL is already spent (TASK-4100 r4).
MaxSteps int64
@@ -106,9 +107,8 @@ func (c *Compiled) checkScan(loc *time.Location, base, q time.Time, lim Limits)
// guard runs fn in the calling goroutine, so nothing can be abandoned or leak.
// A panic in the library becomes a MalformedError instead of crashing the caller.
// MaxDuration is checked between yields, bounding a call to within one period
// expansion, limited by maxPeriodCandidates (TASK-4100 final round H1).
// MaxSteps ends a no-match stretch inside the vendored generator (r2, r4).
// MaxDuration is checked between yields and MaxSteps ends a no-match stretch inside the
// vendored generator (r2, r4); Limits.MaxSteps states what the two bound together.
func guard(fn func() error) (err error) {
defer func() {
if recover() != nil {
+6 -5
View File
@@ -35,11 +35,12 @@
// EXDATE matched on the wall reading
// UNTIL compared on the wall; a Z UNTIL and elapsed rules: on the instant
//
// RULES. ParseRule is strict. It refuses what RFC 5545 forbids (COUNT with
// UNTIL, BYWEEKNO off YEARLY, an ordinal BYDAY off MONTHLY and YEARLY,
// BYSETPOS alone, BYMONTHDAY on WEEKLY, BYYEARDAY on DAILY, WEEKLY or MONTHLY),
// rules that can never fire (Feb 30), FREQ=SECONDLY (the tick is 30 s), and
// rules over 512 bytes, INTERVAL over 1,000,000 or years outside 1970..2200.
// RULES. ParseRule refuses what RFC 5545 forbids (COUNT with UNTIL, BYWEEKNO off YEARLY,
// an ordinal BYDAY off MONTHLY and YEARLY, BYSETPOS alone, BYMONTHDAY on WEEKLY, BYYEARDAY
// on DAILY, WEEKLY or MONTHLY), except that an UNTIL whose type differs from DTSTART's is
// accepted as Until defines it (untilbounds_test.go). It also refuses rules that never fire
// (Feb 30), FREQ=SECONDLY (the tick is 30 s), over 10,000 candidates per FREQ period
// (density.go), over 512 bytes, INTERVAL over 1,000,000 or years outside 1970..2200.
// A DTSTART the rule does not produce is not emitted (dateutil semantics, ruling
// Q1). Every refusal is a *MalformedError carrying a Code and the offending Part;
// no third-party error text ever reaches a caller.