oficina / calendar Public

booking: spec revision 9 — two more engine bugs, and the spec rounds close

Fortnightly BYDAY skips the first selected weekday of an eligible week,
and ParseRule accepts a monthly BYDAY the expander then reads as the
master's day of month. Both are calendar bugs every view already had.
Eight rounds, 58 findings, 58 accepted; the next review is of the code.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 49c36268048f345272d4d5f0e8afca978fcafc4e parent 5f79e4e
1 file changed, +46 −7
  • docs/superpowers/specs/2026-09-07-booking-design.md +46 −7
diff --git a/docs/superpowers/specs/2026-09-07-booking-design.md b/docs/superpowers/specs/2026-09-07-booking-design.md
index 034670a..a83ec9e 100644
--- a/docs/superpowers/specs/2026-09-07-booking-design.md
+++ b/docs/superpowers/specs/2026-09-07-booking-design.md
@@ -4,11 +4,12 @@
Fable 5.1. Approved in outline by Paul in the session that wrote it
(three questions, answered: confirm by email link; everything the host
can see counts as busy; pages are configurable per person). **Revision
-8.** Reviewed seven times by GPT-6 (`gpt-6-astra` on the one run that
-exposed its identifier) through `codex-cli 0.153.0`; all seven returned
-REWRITE, each round narrower than the last — rounds 6 and 7 found three
-each, and round 7's were a fourth pre-existing calendar bug and two
-transaction boundaries. §12 records every finding
+9.** Reviewed eight times by GPT-6 (`gpt-6-astra` on the one run that
+exposed its identifier) through `codex-cli 0.153.0`; all eight returned
+REWRITE, each round narrower than the last. Rounds 7 and 8 found
+nothing in the booking design itself: every finding was a pre-existing
+bug in the recurrence engine the design depends on, and the spec owns
+them (§4.1). The spec rounds end here; the next review is of the code. §12 records every finding
and disposition, including the two scoped rather than solved.
Asked for: a Calendly / Cal.com style "book time with me" page to share
@@ -324,6 +325,27 @@ time must be what the calendar shows.
resolved time. Regression: the daily 01:30 series shows 01:30 on 30
March and on a September day, and a September booking page does not
offer 01:30–02:00 over it.
+- **Fortnightly `BYDAY` skips a weekday (r8 f1).** `advance` for a
+ weekly rule with an interval searches only the next seven days and
+ then jumps a whole interval, so `FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE`
+ from Wednesday 2 September generates 16 September and never 14
+ September. Fix: weekly advancement enumerates every selected weekday
+ within each ELIGIBLE civil week, anchored to the master's week
+ (weeks counted from the master's week start, eligible when the
+ difference is a multiple of the interval), in order. Regression: 14
+ and 16 September both show and neither is bookable.
+- **A parsed rule the expander cannot execute (r8 f2).** `ParseRule`
+ accepts `FREQ=MONTHLY;BYDAY=MO` (every Monday of the month) and the
+ expander ignores `ByDay` for monthly rules, falling back to the
+ master's day of month. Fix: `ParseRule` validates COMBINATIONS —
+ `ByDay` (non-ordinal) only with `FREQ=WEEKLY`; `Ordinal`/`MonthDay`
+ only with `FREQ=MONTHLY`; nothing but `INTERVAL`/`COUNT`/`UNTIL` with
+ `DAILY` and `YEARLY` — and returns `ErrForeignRule` otherwise. The
+ rule is still stored verbatim (ruling 4), the calendar shows its
+ first occurrence as it does for any foreign rule, and the strict
+ reader fails closed on it. Regression: the import above → the day
+ page shows one occurrence, `EventsInRangeStrict` errs, and the public
+ page 503s.
- **Expansion that did not finish, or could not start (r4 B5, r6 f1;
B23).** `Expand` stops at 100,000 steps and returns an ordinary
slice; and `EventsInRange` renders a master whose rule `ParseRule`
@@ -726,7 +748,10 @@ NULL`.
inverted window return errors, not lists.
- **Range query**: the moved-override-of-finished-series case; the
long-final-occurrence case; a daily 01:30 Dublin series created 28
- March shows 01:30 on 30 March and on a September day (gap drift); the final occurrence across the fold
+ March shows 01:30 on 30 March and on a September day (gap drift); a
+ fortnightly MO,WE series from Wednesday 2 September shows 14 AND 16
+ September; `FREQ=MONTHLY;BYDAY=MO` is foreign, shows once, and fails
+ the strict reader; the final occurrence across the fold
with `UNTIL` the night before (the 25-hour slack); the 47-hour
two-day weekly event keeps its second day on later occurrences; a
1700 daily rule → `ErrIncompleteExpansion` from the strict reader
@@ -962,4 +987,18 @@ Rejected: none.
Rejected: none.
-**Round 8** — to be recorded below.
+**Round 8: REWRITE.** 2 blockers, both recurrence-engine bugs;
+round 7's three HOLD.
+
+| Ref | Finding | Disposition |
+|---|---|---|
+| f1 | Fortnightly `BYDAY` skips the first selected weekday of an eligible week | **Accepted, a fifth calendar bug.** Enumerate selected weekdays per eligible week (§4.1). |
+| f2 | `ParseRule` accepts combinations the expander misreads | **Accepted, a sixth.** Combination validation → `ErrForeignRule`; strict fails closed (§4.1). |
+
+Rejected: none.
+
+**Spec review closed at revision 9.** Eight rounds, 58 findings, 58
+accepted (two scoped). The design has been stable since revision 5;
+rounds 6–8 moved transaction boundaries and found six bugs in the
+recurrence engine that every calendar view already had. Further review
+is of the branch's code, recorded in the calendar spec's review log.