== A. purity and the closed state set == PASS the public state set is exactly the four D1a states PASS no RRULE is not_recurring PASS a blank RRULE is not_recurring PASS RDATE without RRULE is a supported series PASS neither RRULE nor RDATE is not_recurring PASS an RRULE beside RDATEs is still the classifier's answer PASS is_degraded covers exactly the two degraded states PASS the classification is independent of the process default timezone PASS the TZID is never resolved: an unknown zone name still makes the DTSTART zoned PASS an empty TZID parameter is no TZID (the value floats) PASS the strict value parser refuses 30 February rather than rolling it forward PASS the strict value parser reads a UTC DATE-TIME exactly PASS a positive integer is unsigned, non-zero and decimal == B. cross-client classification matrix (tools/fixtures/recurrence-qualification-d1a.json) == PASS the matrix parses and names the closed state set PASS every matrix case classifies exactly as pinned (154 cases) PASS the matrix covers group supported_controls (20 cases) PASS the matrix covers group sub_daily (3 cases) PASS the matrix covers group byday (8 cases) PASS the matrix covers group by_parts (16 cases) PASS the matrix covers group dtstart_shape (8 cases) PASS the matrix covers group malformed (25 cases) PASS the matrix covers group impossible_until (11 cases) PASS the matrix covers group until_matrix (16 cases) PASS the matrix covers group not_recurring (2 cases) PASS the matrix covers group integer_bound (33 cases) PASS the matrix covers group duplicate_rrule (12 cases) PASS the matrix exercises every one of the four states PASS the allowlist admits exactly FREQ, INTERVAL, COUNT, UNTIL, BYDAY PASS the supported FREQs are exactly DAILY, WEEKLY, MONTHLY, YEARLY PASS the sub-daily FREQs are exactly SECONDLY, MINUTELY, HOURLY PASS #83: MONTHLY;BYDAY=2TU is not a plain monthly schedule PASS #83: WEEKLY;BYDAY=2MO is not every Monday PASS #83: BYMONTHDAY=1,15 is not the 1st PASS #83: COUNT=abc is malformed, not COUNT=0 PASS #83: INTERVAL=0 is malformed, not INTERVAL=1 == C. supported controls expand exactly as before == PASS control DAILY expands unchanged PASS control DAILY reports supported on every occurrence PASS control DAILY keeps its generated recurrenceId on every occurrence PASS control DAILY + INTERVAL expands unchanged PASS control DAILY + INTERVAL reports supported on every occurrence PASS control DAILY + INTERVAL keeps its generated recurrenceId on every occurrence PASS control DAILY + COUNT expands unchanged PASS control DAILY + COUNT reports supported on every occurrence PASS control DAILY + COUNT keeps its generated recurrenceId on every occurrence PASS control valid DAILY + UNTIL (UTC) expands unchanged PASS control valid DAILY + UNTIL (UTC) reports supported on every occurrence PASS control valid DAILY + UNTIL (UTC) keeps its generated recurrenceId on every occurrence PASS control valid DAILY + UNTIL (floating) expands unchanged PASS control valid DAILY + UNTIL (floating) reports supported on every occurrence PASS control valid DAILY + UNTIL (floating) keeps its generated recurrenceId on every occurrence PASS control valid DAILY + UNTIL (TZID + UTC UNTIL) expands unchanged PASS control valid DAILY + UNTIL (TZID + UTC UNTIL) reports supported on every occurrence PASS control valid DAILY + UNTIL (TZID + UTC UNTIL) keeps its generated recurrenceId on every occurrence PASS control valid all-day DAILY + UNTIL (DATE) expands unchanged PASS control valid all-day DAILY + UNTIL (DATE) reports supported on every occurrence PASS control valid all-day DAILY + UNTIL (DATE) keeps its generated recurrenceId on every occurrence PASS control WEEKLY expands unchanged PASS control WEEKLY reports supported on every occurrence PASS control WEEKLY keeps its generated recurrenceId on every occurrence PASS control WEEKLY + non-ordinal BYDAY expands unchanged PASS control WEEKLY + non-ordinal BYDAY reports supported on every occurrence PASS control WEEKLY + non-ordinal BYDAY keeps its generated recurrenceId on every occurrence PASS control WEEKLY + INTERVAL expands unchanged PASS control WEEKLY + INTERVAL reports supported on every occurrence PASS control WEEKLY + INTERVAL keeps its generated recurrenceId on every occurrence PASS control safe MONTHLY day 10 expands unchanged PASS control safe MONTHLY day 10 reports supported on every occurrence PASS control safe MONTHLY day 10 keeps its generated recurrenceId on every occurrence PASS control safe MONTHLY day 1 expands unchanged PASS control safe MONTHLY day 1 reports supported on every occurrence PASS control safe MONTHLY day 1 keeps its generated recurrenceId on every occurrence PASS control safe MONTHLY day 28 expands unchanged PASS control safe MONTHLY day 28 reports supported on every occurrence PASS control safe MONTHLY day 28 keeps its generated recurrenceId on every occurrence PASS control safe YEARLY non-Feb-29 expands unchanged PASS control safe YEARLY non-Feb-29 reports supported on every occurrence PASS control safe YEARLY non-Feb-29 keeps its generated recurrenceId on every occurrence PASS control safe YEARLY Feb 28 expands unchanged PASS control safe YEARLY Feb 28 reports supported on every occurrence PASS control safe YEARLY Feb 28 keeps its generated recurrenceId on every occurrence PASS control RDATE-only series expands unchanged PASS control RDATE-only series reports supported on every occurrence PASS a supported zoned control still mints its v3 identity (20260825T073000Z) == D. degraded shapes: DTSTART only, no derived schedule, no identity == PASS HOURLY: exactly one displayed occurrence, the source DTSTART (no 03:00 / 04:00 / 05:00) PASS HOURLY: recurring=true (rrule retained), degraded_unsupported, no recurrenceId PASS HOURLY: the source DTEND is kept PASS HOURLY: raw RRULE retained verbatim PASS HOURLY: no Event Link identity PASS HOURLY: a window after DTSTART gets NOTHING (no invented occurrence) PASS month-end MONTHLY;COUNT=4 from 31 Jan: 31 Jan only PASS month-end: degraded_unsupported, no recurrenceId PASS month-end: neither a clamped 28 Feb nor a skipped-to 31 Mar exists in any later window PASS month-end: no Event Link identity PASS FREQ=MONTHLY;BYDAY=2TU: DTSTART only, degraded_unsupported, no identity PASS FREQ=MONTHLY;BYDAY=2TU: a window after DTSTART gets nothing PASS FREQ=MONTHLY;BYDAY=2TU: no Event Link identity PASS FREQ=WEEKLY;BYDAY=2MO: DTSTART only, degraded_unsupported, no identity PASS FREQ=WEEKLY;BYDAY=2MO: a window after DTSTART gets nothing PASS FREQ=WEEKLY;BYDAY=2MO: no Event Link identity PASS FREQ=MONTHLY;BYDAY=TU: DTSTART only, degraded_unsupported, no identity PASS FREQ=MONTHLY;BYDAY=TU: a window after DTSTART gets nothing PASS FREQ=MONTHLY;BYDAY=TU: no Event Link identity PASS FREQ=YEARLY;BYDAY=TU: DTSTART only, degraded_unsupported, no identity PASS FREQ=YEARLY;BYDAY=TU: a window after DTSTART gets nothing PASS FREQ=YEARLY;BYDAY=TU: no Event Link identity PASS FREQ=MONTHLY;BYSETPOS=-1: DTSTART only, degraded_unsupported, no identity PASS FREQ=MONTHLY;BYSETPOS=-1: a window after DTSTART gets nothing PASS FREQ=MONTHLY;BYSETPOS=-1: no Event Link identity PASS FREQ=YEARLY;BYMONTH=3,9: DTSTART only, degraded_unsupported, no identity PASS FREQ=YEARLY;BYMONTH=3,9: a window after DTSTART gets nothing PASS FREQ=YEARLY;BYMONTH=3,9: no Event Link identity PASS FREQ=MONTHLY;BYMONTHDAY=1,15: DTSTART only, degraded_unsupported, no identity PASS FREQ=MONTHLY;BYMONTHDAY=1,15: a window after DTSTART gets nothing PASS FREQ=MONTHLY;BYMONTHDAY=1,15: no Event Link identity PASS FREQ=WEEKLY;WKST=SU: DTSTART only, degraded_unsupported, no identity PASS FREQ=WEEKLY;WKST=SU: a window after DTSTART gets nothing PASS FREQ=WEEKLY;WKST=SU: no Event Link identity PASS FREQ=DAILY;BYHOUR=9: DTSTART only, degraded_unsupported, no identity PASS FREQ=DAILY;BYHOUR=9: a window after DTSTART gets nothing PASS FREQ=DAILY;BYHOUR=9: no Event Link identity PASS FREQ=DAILY;X-CALEE-FOO=1: DTSTART only, degraded_unsupported, no identity PASS FREQ=DAILY;X-CALEE-FOO=1: a window after DTSTART gets nothing PASS FREQ=DAILY;X-CALEE-FOO=1: no Event Link identity PASS FREQ=MINUTELY;COUNT=10: DTSTART only, degraded_unsupported, no identity PASS FREQ=MINUTELY;COUNT=10: a window after DTSTART gets nothing PASS FREQ=MINUTELY;COUNT=10: no Event Link identity PASS FREQ=SECONDLY: DTSTART only, degraded_unsupported, no identity PASS FREQ=SECONDLY: a window after DTSTART gets nothing PASS FREQ=SECONDLY: no Event Link identity PASS FREQ=DAILY;INTERVAL=0: DTSTART only, degraded_invalid, no identity PASS FREQ=DAILY;INTERVAL=0: a window after DTSTART gets nothing PASS FREQ=DAILY;INTERVAL=0: no Event Link identity PASS FREQ=DAILY;INTERVAL=abc: DTSTART only, degraded_invalid, no identity PASS FREQ=DAILY;INTERVAL=abc: a window after DTSTART gets nothing PASS FREQ=DAILY;INTERVAL=abc: no Event Link identity PASS FREQ=DAILY;COUNT=abc: DTSTART only, degraded_invalid, no identity PASS FREQ=DAILY;COUNT=abc: a window after DTSTART gets nothing PASS FREQ=DAILY;COUNT=abc: no Event Link identity PASS FREQ=DAILY;COUNT=0: DTSTART only, degraded_invalid, no identity PASS FREQ=DAILY;COUNT=0: a window after DTSTART gets nothing PASS FREQ=DAILY;COUNT=0: no Event Link identity PASS FREQ=DAILY;COUNT=3;UNTIL=20260410T090000Z: DTSTART only, degraded_invalid, no identity PASS FREQ=DAILY;COUNT=3;UNTIL=20260410T090000Z: a window after DTSTART gets nothing PASS FREQ=DAILY;COUNT=3;UNTIL=20260410T090000Z: no Event Link identity PASS FREQ=DAILY;UNTIL=20260410T090000: DTSTART only, degraded_invalid, no identity PASS FREQ=DAILY;UNTIL=20260410T090000: a window after DTSTART gets nothing PASS FREQ=DAILY;UNTIL=20260410T090000: no Event Link identity PASS FREQ=DAILY;UNTIL=20260230T090000Z: DTSTART only, degraded_invalid, no identity PASS FREQ=DAILY;UNTIL=20260230T090000Z: a window after DTSTART gets nothing PASS FREQ=DAILY;UNTIL=20260230T090000Z: no Event Link identity PASS FREQ=DAILY;UNTIL=20260101T250000Z: DTSTART only, degraded_invalid, no identity PASS FREQ=DAILY;UNTIL=20260101T250000Z: a window after DTSTART gets nothing PASS FREQ=DAILY;UNTIL=20260101T250000Z: no Event Link identity PASS FREQ=DAILY;FREQ=WEEKLY: DTSTART only, degraded_invalid, no identity PASS FREQ=DAILY;FREQ=WEEKLY: a window after DTSTART gets nothing PASS FREQ=DAILY;FREQ=WEEKLY: no Event Link identity PASS FREQ=DAILY;COUNT: DTSTART only, degraded_invalid, no identity PASS FREQ=DAILY;COUNT: a window after DTSTART gets nothing PASS FREQ=DAILY;COUNT: no Event Link identity PASS FREQ=WEEKLY;BYDAY=XX: DTSTART only, degraded_invalid, no identity PASS FREQ=WEEKLY;BYDAY=XX: a window after DTSTART gets nothing PASS FREQ=WEEKLY;BYDAY=XX: no Event Link identity PASS FREQ=FORTNIGHTLY: DTSTART only, degraded_invalid, no identity PASS FREQ=FORTNIGHTLY: a window after DTSTART gets nothing PASS FREQ=FORTNIGHTLY: no Event Link identity PASS INTERVAL=2: DTSTART only, degraded_invalid, no identity PASS INTERVAL=2: a window after DTSTART gets nothing PASS INTERVAL=2: no Event Link identity PASS calee_calendar_parse() reports recurrenceResolution on every component PASS an all-day MONTHLY from the 31st is degraded: the DTSTART date only, no identity PASS a degraded series consumes no work units (nothing is stepped) == E. explicit source evidence: RDATE, EXDATE, RECURRENCE-ID == PASS HOURLY + RDATE emits 10 Mar (DTSTART) and 15 Mar (RDATE) and no inferred hourly instant PASS ...the DTSTART carries no recurrenceId and no link identity PASS ...the RDATE stays refused for linking exactly as the identity contract already refuses RDATEs PASS ...both report degraded_unsupported PASS HOURLY + RDATE: a window holding only the RDATE gets only the RDATE PASS EXDATE naming the DTSTART of a degraded series excludes it PASS EXDATE naming an alleged 10:00 instance generates nothing to exclude; the DTSTART stays PASS ...and never suppresses the unrelated event at 10:00 PASS EXDATE naming an explicit RDATE excludes that RDATE and keeps the DTSTART PASS a detached cancellation naming the degraded DTSTART suppresses it PASS a detached replacement naming the degraded DTSTART replaces it (no stale twin at 09:00) PASS ...the replacement keeps its own explicit RECURRENCE-ID identity PASS ...and that explicit identity is still linkable under the existing identity rules PASS derived detached orphan: the master DTSTART survives and the override is shown as itself PASS derived detached orphan: no generated 10:00 occurrence appears for the master PASS adversarial neighbour: the unrelated 10:00 event is never suppressed by the orphan PASS a cancellation naming an alleged 10:00 instance cancels nothing: the DTSTART stays PASS ...and the neighbour at 10:00 is untouched (wrong suppression is impossible) PASS a cancellation naming a clamped 28 Feb cancels nothing; 31 Jan stays PASS ...and the unrelated 28 Feb event is untouched == E2. RDATE-only: DTSTART + explicit RDATEs, nothing more, nothing lost == PASS RDATE-only emits exactly 10 Mar and 12 Mar PASS RDATE-only: both occurrences report supported PASS RDATE-only: the parsed component reports supported PASS RDATE-only: the RDATE occurrence keeps the source duration PASS RDATE-only: nothing is invented between DTSTART and the RDATE PASS RDATE-only: the batch path yields the same two instants PASS RDATE-only: the single-series path yields the same two instants PASS a component with neither RRULE nor RDATE is still an ordinary one-off (not_recurring) PASS window holding only the RDATE gets only the RDATE PASS window holding only the DTSTART gets only the DTSTART PASS window between DTSTART and RDATE gets nothing PASS window after every explicit instant gets nothing PASS multiple RDATE values and lines all survive within the window PASS EXDATE removes the RDATE, keeps DTSTART PASS EXDATE removes the DTSTART, keeps the RDATE PASS EXDATE naming neither instant removes nothing PASS an RDATE restating the DTSTART yields one logical occurrence, not a twin PASS ...and the surviving 10 Mar is the DTSTART occurrence, not an RDATE clone PASS repeated identical RDATE values yield one visible occurrence PASS an RDATE restating a generated supported occurrence adds no twin either PASS all-day RDATE-only preserves both literal dates PASS all-day RDATE-only: both are all-day and supported PASS all-day RDATE-only: the RDATE keeps the one-day span PASS all-day EXDATE removes the all-day RDATE PASS zoned RDATE-only across Adelaide DST: 09:00 local both times (23:30Z then 22:30Z) PASS zoned RDATE-only: local wall clock is 09:00 on both instants PASS zoned RDATE-only: both supported PASS a detached override naming the explicit RDATE replaces it (no stale twin at 12 Mar) PASS a detached cancellation naming the DTSTART of an RDATE-only set suppresses it, keeps the RDATE PASS a cancellation naming an instant that is neither DTSTART nor RDATE cancels nothing PASS ...and never the unrelated neighbour at that instant PASS RDATE-only DTSTART keeps the one-off identity rule: ok with no recurrenceId (calendar + UID) PASS RDATE-only explicit RDATE occurrence stays refused (existing RDATE policy) PASS RDATE beside a supported RRULE stays refused too PASS the detached override of an RDATE keeps its explicit RECURRENCE-ID identity PASS a degraded DTSTART is still refused after the RDATE-only correction == E3. RDATE property context: own VALUE type, own TZID, never DTSTART's == PASS rdateLines keeps one entry per physical RDATE line PASS rdateLines pairs each line's values with that line's own params PASS rdateLines is built by the same property-line helper EXDATE uses (plus per-line parameter counts) PASS the flat rdate list is still present for callers that only need presence PASS cross-zone: DTSTART 09:00 Perth = 01:00Z, RDATE 09:00 Kolkata = 03:30Z PASS cross-zone: the RDATE instant is exactly 2026-03-12T03:30:00Z, never 01:00Z (Perth's clock) PASS cross-zone: the RDATE displays as 09:00 on ITS OWN Kolkata source clock PASS cross-zone: (control) on Perth's clock it is 11:30, which is what a display zone legitimately shows PASS cross-zone: the series is supported (every form reproduced exactly) PASS cross-zone: the RDATE keeps the series duration on its own clock PASS cross-zone: the display zone does not change the instant PASS mixed-zone lines: Perth 09:00 = 01:00Z, Kolkata 09:00 = 03:30Z, explicit 01:00Z stays 01:00Z PASS mixed-zone lines: the last-parsed line (UTC) did not decide the others PASS mixed-zone lines: the first line (Perth) did not decide the Kolkata line PASS mixed-zone lines: supported PASS same-line values: both read on Kolkata (03:30Z, 03:30Z), neither on Perth PASS same-line values: preserved as one line with two values PASS a trailing Z means UTC: 09:00Z, not Perth 09:00 (01:00Z) PASS floating DTSTART and floating RDATE are both read on the calendar context (Kolkata): 03:30Z each PASS floating: supported PASS a floating RDATE under a ZONED DTSTART is not bound to DTSTART's Perth clock: it floats on the calendar context (03:30Z, not 01:00Z) PASS floating RDATE: identity stays fail-closed (RDATE occurrences mint nothing) PASS floating RDATE-only DTSTART: the existing one-off rule (calendar + UID, no recurrenceId) is unchanged; no instant identity is minted from a floating value PASS all-day RDATE under Pacific/Kiritimati: literally 10 March and 12 March PASS a TZID on a VALUE=DATE RDATE is malformed: degraded_invalid, the RDATE is not emitted, the DTSTART date is preserved PASS ...and an unresolvable TZID on a DATE is the same malformation, not a resolvability question PASS a DATE value on a timed series is a form CalEmbed cannot place: degraded_unsupported, no invented instant PASS a DATE-TIME value on an all-day series likewise degrades without inventing a day PASS unsupported RDATE TZID: the series is degraded_unsupported, not supported PASS unsupported RDATE TZID: only the DTSTART is emitted; the RDATE is NOT read on Perth's clock (no 01:00Z on 12 Mar) PASS unsupported RDATE TZID: no occurrence on 12 Mar at any instant (no neighbouring guess) PASS unsupported RDATE TZID: the DTSTART carries no recurrence identity; its one-off calendar + UID rule is unchanged (its own instant is exact) and no RDATE identity exists to mint PASS unsupported RDATE TZID: the classifier names the reason PASS a Windows TZID on an RDATE resolves through the pinned map exactly as DTSTART's would PASS a quoted TZID is unquoted before resolution PASS PERIOD (explicit end): degraded_unsupported PASS PERIOD (explicit end): no fabricated 12 Mar occurrence, DTSTART only PASS PERIOD (duration): degraded_unsupported PASS PERIOD (duration): no fabricated 12 Mar occurrence, DTSTART only PASS PERIOD (slash without VALUE=PERIOD): degraded_unsupported PASS PERIOD (slash without VALUE=PERIOD): no fabricated 12 Mar occurrence, DTSTART only PASS an unknown VALUE type fails closed as degraded_invalid PASS malformed RDATE (30 February): degraded_invalid PASS malformed RDATE (30 February): not normalised into a real occurrence; DTSTART only PASS malformed RDATE (hour 25): degraded_invalid PASS malformed RDATE (hour 25): not normalised into a real occurrence; DTSTART only PASS malformed RDATE (free text): degraded_invalid PASS malformed RDATE (free text): not normalised into a real occurrence; DTSTART only PASS malformed RDATE (month 13): degraded_invalid PASS malformed RDATE (month 13): not normalised into a real occurrence; DTSTART only PASS invalid wins over unsupported when both RDATE forms are present PASS an invalid RDATE beside a supported RRULE degrades the whole series (invalid wins) PASS a PERIOD RDATE beside a supported RRULE makes the series degraded_unsupported and the rule is not stepped PASS the RDATE classifier is pure: no TZID resolver means no TZID is resolved PASS the RDATE classifier does not depend on the process default timezone PASS RDATE-only UTC set still emits exactly 10 and 12 March PASS EXDATE against a cross-zone RDATE (named in UTC) still removes it PASS EXDATE against a cross-zone RDATE (named on its own Kolkata clock) still removes it PASS an RDATE restating DTSTART on a different clock is still one logical occurrence PASS a hand-built event with only a flat rdate list reads each value on its own self-describing form PASS cross-zone RDATE occurrence: still refused a portable identity (no_canonical_recurrence_identity) PASS cross-zone RDATE-only DTSTART: still the one-off rule (ok, no recurrenceId) PASS cross-zone RDATE refused under contract v1 PASS cross-zone RDATE refused under contract v2 PASS cross-zone RDATE refused under contract v3 PASS the identity module still names no RDATE resolution of its own (refusal is by the isRdate tag) == E4. RDATE source-form validity: TZID on DATE / UTC, bare DATE under DATE-TIME, duplicate params == PASS control: legal UTC RDATE (no TZID) is supported and emits 12 Mar 09:00Z PASS control: legal zoned RDATE (local DATE-TIME + TZID) is supported and resolves to 03:30Z PASS control: floating RDATE on an established floating context is supported (03:30Z on the Kolkata calendar clock) PASS control: legal DATE RDATE (VALUE=DATE declared, no TZID) on an all-day series is supported with literal dates PASS control: explicit VALUE=DATE-TIME with a DATE-TIME value is supported PASS TZID (resolvable) on a VALUE=DATE RDATE: degraded_invalid, reason rdate_tzid_forbidden_on_date PASS TZID (resolvable) on a VALUE=DATE RDATE: no RDATE occurrence, the DTSTART date only PASS TZID (unresolvable) on a VALUE=DATE RDATE: degraded_invalid, reason rdate_tzid_forbidden_on_date PASS TZID (unresolvable) on a VALUE=DATE RDATE: no RDATE occurrence, the DTSTART date only PASS TZID (resolvable (extreme offset)) on a VALUE=DATE RDATE: degraded_invalid, reason rdate_tzid_forbidden_on_date PASS TZID (resolvable (extreme offset)) on a VALUE=DATE RDATE: no RDATE occurrence, the DTSTART date only PASS TZID on a VALUE=DATE RDATE under a TIMED master is still the malformation (invalid), not merely a shape mismatch PASS RDATE;TZID=Asia/Kolkata:20260312T090000Z: degraded_invalid, reason rdate_tzid_forbidden_on_utc PASS RDATE;TZID=Asia/Kolkata:20260312T090000Z: no occurrence at 09:00Z, 03:30Z, 01:00Z or any 12 Mar instant; DTSTART only PASS RDATE;VALUE=DATE-TIME;TZID=Asia/Kolkata:20260312T090000Z: degraded_invalid, reason rdate_tzid_forbidden_on_utc PASS RDATE;VALUE=DATE-TIME;TZID=Asia/Kolkata:20260312T090000Z: no occurrence at 09:00Z, 03:30Z, 01:00Z or any 12 Mar instant; DTSTART only PASS RDATE;TZID=Nowhere/Unknown:20260312T090000Z: degraded_invalid, reason rdate_tzid_forbidden_on_utc PASS RDATE;TZID=Nowhere/Unknown:20260312T090000Z: no occurrence at 09:00Z, 03:30Z, 01:00Z or any 12 Mar instant; DTSTART only PASS a UTC value beside a TZID rejects the whole PROPERTY line, its legal sibling value included (fail closed) PASS RDATE:20260312 on an ALL-DAY master: degraded_invalid, reason rdate_value_not_datetime (VALUE=DATE is never inferred from shape or master) PASS RDATE:20260312 on an all-day master: no RDATE occurrence, DTSTART only PASS RDATE:20260312 on a TIMED master: degraded_invalid too, DTSTART only PASS RDATE;VALUE=DATE-TIME:20260312 on an ALL-DAY master: degraded_invalid, reason rdate_value_not_datetime (VALUE=DATE is never inferred from shape or master) PASS RDATE;VALUE=DATE-TIME:20260312 on an all-day master: no RDATE occurrence, DTSTART only PASS RDATE;VALUE=DATE-TIME:20260312 on a TIMED master: degraded_invalid too, DTSTART only PASS the inverse: VALUE=DATE with a DATE-TIME value is still degraded_invalid (rdate_value_not_date) PASS RDATE;VALUE=DATE;VALUE=DATE:20260312: degraded_invalid, reason rdate_duplicate_value_param PASS RDATE;VALUE=DATE;VALUE=DATE:20260312: no RDATE occurrence from any of the stated values; DTSTART only PASS RDATE;VALUE=DATE-TIME;VALUE=DATE:20260312: degraded_invalid, reason rdate_duplicate_value_param PASS RDATE;VALUE=DATE-TIME;VALUE=DATE:20260312: no RDATE occurrence from any of the stated values; DTSTART only PASS RDATE;VALUE=DATE;VALUE=DATE-TIME:20260312T090000Z: degraded_invalid, reason rdate_duplicate_value_param PASS RDATE;VALUE=DATE;VALUE=DATE-TIME:20260312T090000Z: no RDATE occurrence from any of the stated values; DTSTART only PASS RDATE;TZID=Asia/Kolkata;TZID=Australia/Perth:20260312T090000: degraded_invalid, reason rdate_duplicate_tzid_param PASS RDATE;TZID=Asia/Kolkata;TZID=Australia/Perth:20260312T090000: no RDATE occurrence from any of the stated values; DTSTART only PASS RDATE;TZID=Asia/Kolkata;TZID=Asia/Kolkata:20260312T090000: degraded_invalid, reason rdate_duplicate_tzid_param PASS RDATE;TZID=Asia/Kolkata;TZID=Asia/Kolkata:20260312T090000: no RDATE occurrence from any of the stated values; DTSTART only PASS parser: the scalar params map keeps its historical last-one-wins value for existing consumers PASS parser: the occurrence count records that TZID was stated twice PASS parser: two-element destructuring of the key part still works for every existing caller PASS normalised rdateLines carries paramCounts TZID=2 beside the collapsed params, so the duplication cannot pass as "TZID = Australia/Perth" PASS the same collapsed line WITHOUT the count evidence would have classified supported: the count is load-bearing PASS duplicate VALUE: rdateLines carries paramCounts VALUE=2; the collapsed last value (DATE) does not make it supported PASS EXDATE lines are unchanged in shape (no paramCounts added to an unaudited consumer) PASS unknown extension parameters remain allowed on an RDATE (X-FOO=1 beside a legal value stays supported) PASS valid DATE RDATE on a timed series: degraded_unsupported (rdate_date_on_timed_series), not invalid PASS valid DATE-TIME RDATE on an all-day series: degraded_unsupported (rdate_datetime_on_allday_series), not invalid PASS valid PERIOD form: degraded_unsupported, not invalid PASS unsupported local DATE-TIME TZID: degraded_unsupported, not invalid PASS impossible values remain degraded_invalid (control already pinned in E3) PASS mixed lines: the series is degraded_invalid (the worse state wins) PASS mixed lines: DTSTART, the legal Kolkata 12 Mar (03:30Z) and the legal UTC 14 Mar are preserved; the illegal TZID+Z 13 Mar is NOT emitted PASS mixed lines: nothing on 13 Mar at any instant PASS mixed lines: every emitted occurrence reports the series state PASS mixed all-day lines: legal 12 Mar kept, TZID-on-DATE 15 Mar and bare-DATE-under-DATE-TIME 20 Mar dropped, series invalid PASS supported RRULE + malformed RDATE: the series is degraded_invalid PASS supported RRULE + malformed RDATE: the RRULE is not stepped (no 11 / 12 Mar), the RDATE is not emitted; DTSTART only PASS supported RRULE + malformed RDATE: the DTSTART carries no recurrenceId and no link identity PASS supported RRULE + malformed RDATE + legal RDATE: rule not stepped, legal RDATE preserved PASS a rejected form certifies NO value for the expander ({"values":["20260312"],"params":{"VALUE":"DATE","TZID":"Asia\/Kolkata"}}) PASS ...and the instant reader emits nothing from it PASS a rejected form certifies NO value for the expander ({"values":["20260312T090000Z"],"params":{"TZID":"Asia\/Kolkata"}}) PASS ...and the instant reader emits nothing from it PASS a rejected form certifies NO value for the expander ({"values":["20260312"],"params":[]}) PASS ...and the instant reader emits nothing from it PASS a rejected form certifies NO value for the expander ({"values":["20260312"],"params":{"VALUE":"DATE"},"paramCounts":{"VALUE":2}}) PASS ...and the instant reader emits nothing from it PASS a rejected form certifies NO value for the expander ({"values":["20260312T090000"],"params":{"TZID":"Asia\/Kolkata"},"paramCounts":{"TZID":2}}) PASS ...and the instant reader emits nothing from it PASS the classifier module still resolves no TZID of its own (a TZID-on-DATE verdict needs no resolver) PASS an empty TZID= on a local DATE-TIME names no clock: degraded_unsupported, nothing read on another zone PASS an empty TZID= on a DATE is still a forbidden TZID parameter PASS legal RDATE occurrence: displays, still refused a portable identity PASS legal RDATE-only DTSTART: existing UID-only rule unchanged PASS rejected RDATE line: only the DTSTART exists to name; its one-off rule is unchanged (no RDATE occurrence, no RDATE identity) == F. identity: nothing minted from a refused rule == PASS a supported occurrence still mints ok with its instant PASS a one-off still mints ok with no recurrenceId PASS the degraded refusal is a contract identityStatus (no_canonical_recurrence_identity), not a new status PASS degraded DTSTART is refused under contract v1 PASS supported occurrence still mints under contract v1 PASS degraded DTSTART is refused under contract v2 PASS supported occurrence still mints under contract v2 PASS degraded DTSTART is refused under contract v3 PASS supported occurrence still mints under contract v3 == G. defence in depth: the field is never trusted == PASS normalize_event classifies HOURLY degraded PASS the single-series expander re-qualifies from source bytes: a forged supported still yields DTSTART only PASS the identity layer re-qualifies from source bytes: a forged occurrence still mints nothing PASS the batch expander (no detached path) yields the same degraded answer PASS with no raw DTSTART evidence at all an HOURLY rule is still degraded PASS with no raw DTSTART evidence a MONTHLY rule cannot be certified and is degraded (never stepped) == H. calee-hub-core#582: the RFC 5545 INTEGER bound == PASS the bound is RFC 5545 3.3.8, stated once PASS H1: every in-domain value parses to itself (1 .. 2147483647) PASS H1: every out-of-domain value is refused, shape or range PASS H1: 2147483647 is accepted PASS H1: 2147483648 is refused PASS H1b: a ten-digit out-of-range value is refused lexically, not by a cast PASS H1b: PHP_INT_MAX itself is refused, whatever it is on this build PASS H2: the expander and the classifier share one parts scan PASS H2: legal whitespace around a part is trimmed, not folded into the key PASS H2: lower-case input normalises the same way PASS H2: ... and the engine steps the INTERVAL the classifier certified, not a substituted daily series PASS H3: an out-of-range COUNT/INTERVAL is never stepped, never mints identity, invents nothing (8 rules) PASS H4: INTERVAL=2147483647 is supported and stepped PASS H4: ... and it DOES mint an identity, unlike the refused rules PASS H4: COUNT=2147483647 is bounded by the window, not by the COUNT PASS H5: ordinary INTERVAL/COUNT values are untouched and still mint identity PASS H6: absent INTERVAL and COUNT are not invalid values PASS H6: an absent INTERVAL means 1 and an absent COUNT means no bound calendar recurrence qualification tests passed