truth-engine 0.1.0

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

```rust
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.13
- `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

```bash
cargo test -p truth-engine
```

## License

MIT OR Apache-2.0