truth-engine 0.1.1

Deterministic RRULE expansion with DST handling for AI calendar agents
Documentation

truth-engine

Deterministic RRULE expansion with DST handling for AI calendar agents.

LLMs struggle with recurrence rule math — they hallucinate dates, mishandle DST transitions, and can't reliably compute "3rd Tuesday of each month." The Truth Engine replaces inference with deterministic computation using the rrule crate and chrono-tz.

Usage

use truth_engine::{expand_rrule, find_conflicts, find_free_slots, ExpandedEvent};

// Expand a recurrence rule into concrete instances
let events = expand_rrule(
    "FREQ=MONTHLY;BYDAY=TU;BYSETPOS=3",       // 3rd Tuesday of each month
    "2026-02-17T14:00:00",                      // start date (local time)
    60,                                          // 60-minute duration
    "America/Los_Angeles",                       // IANA timezone
    Some("2026-06-30T23:59:59"),                 // expand until
    None,                                        // no count limit
).unwrap();

// Each event has start/end as DateTime<Utc>
for event in &events {
    println!("{}{}", event.start, event.end);
}

// Detect overlapping events between two schedules
let conflicts = find_conflicts(&schedule_a, &schedule_b);
for c in &conflicts {
    println!("Overlap: {} minutes", c.overlap_minutes);
}

// Find free slots in a time window
let free = find_free_slots(
    &busy_events,
    window_start,  // DateTime<Utc>
    window_end,    // DateTime<Utc>
);

Features

RRULE Expansion

  • Full RFC 5545 recurrence rule support via the rrule crate v0.14
  • FREQ: DAILY, WEEKLY, MONTHLY, YEARLY
  • BYDAY, BYMONTH, BYMONTHDAY, BYSETPOS, INTERVAL, COUNT, UNTIL
  • EXDATE exclusions via expand_rrule_with_exdates()
  • DST-aware: events at 14:00 Pacific stay at 14:00 Pacific across DST transitions (UTC offset shifts automatically)
  • Leap year handling: BYMONTHDAY=29 in February correctly skips non-leap years

Conflict Detection

  • Pairwise overlap detection between two event lists
  • Overlap defined as a.start < b.end && b.start < a.end
  • Adjacent events (end == start) are NOT conflicts
  • Returns overlap duration in minutes

Free/Busy Computation

  • Merges overlapping busy periods
  • Computes free gaps within a time window
  • find_first_free_slot() for minimum-duration search

API

expand_rrule(rrule, dtstart, duration_minutes, timezone, until, count)

Expands an RRULE string into concrete ExpandedEvent instances.

expand_rrule_with_exdates(rrule, dtstart, duration_minutes, timezone, until, count, exdates)

Same as above but excludes specific dates (RFC 5545 EXDATE).

find_conflicts(events_a, events_b) -> Vec<Conflict>

Finds all pairwise overlaps between two event lists.

find_free_slots(events, window_start, window_end) -> Vec<FreeSlot>

Computes free time slots within a window, merging overlapping busy periods.

find_first_free_slot(events, window_start, window_end, min_duration_minutes) -> Option<FreeSlot>

Finds the earliest free slot of at least the given duration.

Architecture

expander.rs  ← RRULE string → Vec<ExpandedEvent> (wraps rrule + chrono-tz)
conflict.rs  ← Two event lists → Vec<Conflict> (pairwise overlap detection)
freebusy.rs  ← Events + window → Vec<FreeSlot> (gap computation)
dst.rs       ← DstPolicy enum (Skip, ShiftForward, WallClock)
error.rs     ← TruthError enum (InvalidRule, InvalidTimezone, Expansion)

Testing

33 tests across four suites:

  • 11 expander tests — CTO's monthly 3rd Tuesday example, DST transitions, daily/weekly/biweekly, COUNT, UNTIL, duration
  • 7 conflict tests — overlapping, non-overlapping, adjacent, contained, multiple, empty
  • 7 free/busy tests — single event, merged overlapping, empty, min-duration, fully booked
  • 8 RFC 5545 compliance vectors — biweekly multi-day, yearly, leap year Feb 29, EXDATE, COUNT, INTERVAL, BYSETPOS last weekday, multi-rule intersection
cargo test -p truth-engine

License

MIT OR Apache-2.0