# Roadmap Milestones from the implementation plan. See SPECS.md for the architecture and the sync semantics these items implement. ## M0 — skeleton (done) - [x] Initialise jj colocated with git; `.gitignore` written before the first build - [x] `devbox.json` pinning Rust 1.97.1, pimsync 0.5.11, jujutsu 0.44.0, radicale 3.7.8 - [x] Configuration model with referential validation, reporting every problem in one pass - [x] `calcalist doctor` — pimsync presence and version series, state directory, configuration - [x] Define the full CLI surface; unimplemented commands exit 2 rather than pretend - [x] SPECS.md: Rust naming conventions, `devbox run check` gate, recorded sync semantics ## M1 — bidirectional sync Core modules: - [x] `state.rs` — JSON sidecar, atomic temp + fsync + rename; records each aggregate's resolved target endpoint id **and** backend type - [x] `vdir.rs` — read and write vdir directories - [x] `ical.rs` — surgical line-level `.ics` editing (UID rewrite, property injection), respecting RFC 5545 folding; no parse-and-reserialize - [x] `provenance.rs` — deterministic `blake3(aggregate_id, source_id, source_uid)` UIDs - [x] `mirror.rs` — the to-aggregate and to-source transforms (added; not in the original plan, which folded these into `reconcile`) - [x] `reconcile.rs` — the aggregation engine; pure, no I/O - [x] `sync.rs` — one cycle over the local vdirs, applying what `reconcile` decides - [ ] `pimsync.rs` — generate `pimsync.conf` (with `on_empty skip` and `on_delete skip`), drive one-shot `pimsync sync` - [ ] `google/auth.rs`, `google/api.rs`, `google/convert.rs` - [x] Reintroduce `SchedulingSuppression` in `config.rs` (removed in M0 as dead code) Safety-critical behaviour: - [ ] **`events.import` gate — do this first.** Import an attendee-bearing event whose guests are on a mail sink we control and confirm no mail is emitted; repeat for update and delete under `sendUpdates=none`. The Google attendee path depends on it. Fallback if it fails: the same demotion transform used for CalDAV. - [x] `sync` refuses to run on aggregate target drift, before reconciliation - [ ] `aggregate retarget` — flush unrouted creations against the old target, then re-materialise; keep old orphans by default - [x] Mass-deletion guard (`max_delete_fraction`), overridable with `--force`, with an absolute floor so deleting a couple of events is never refused - [x] Echo suppression: derived UIDs are never re-ingested as source events Tests: - [x] `reconcile` table-driven cases: create/update/delete each direction, both-sides-changed, routing, echo suppression, mass-delete abort - [x] `ical` round-trip fixtures: recurring with overrides, all-day, TZID, unknown `X-` props - [ ] Integration against Radicale plus a `file://` WebCal fixture; assert idempotence - [x] Safety (unit level): no live `ATTENDEE`/`ORGANIZER` on a CalDAV-targeted mirror, `VALARM` intact, `PARTSTAT: DECLINED` maps to `TRANSP: TRANSPARENT`, bulk deletion aborts - [ ] Safety (integration): the same against a real Radicale instance with an SMTP sink, proving no mail is emitted - [ ] Retarget: drift makes `sync` exit non-zero having written nothing and losing no source event (verified by hand end to end; still needs an automated test) ## M2 — interface and packaging - [ ] axum configuration UI, bound to 127.0.0.1 - [ ] OAuth loopback redirect handler - [ ] systemd user units: `calcalist.service` (oneshot) and `calcalist.timer`