From ef399db4d8f65a526fc190c74f04c1200eb0dbe2 Mon Sep 17 00:00:00 2001 From: randogoth Date: Thu, 10 Sep 2026 09:34:42 +0300 Subject: [PATCH] Add TODO.md tracking the M0-M2 roadmap The implementation plan lives outside the repo, so it neither travels with the project nor appears in history. Record the milestones here instead, with M0 checked off and the safety-critical M1 items called out separately from the module work. Co-Authored-By: Claude Opus 5 (1M context) --- TODO.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..0b782f2 --- /dev/null +++ b/TODO.md @@ -0,0 +1,57 @@ +# 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: + +- [ ] `state.rs` — JSON sidecar, atomic temp + fsync + rename; records each aggregate's + resolved target endpoint id **and** backend type +- [ ] `vdir.rs` — read and write vdir directories +- [ ] `ical.rs` — surgical line-level `.ics` editing (UID rewrite, property injection), + respecting RFC 5545 folding; no parse-and-reserialize +- [ ] `provenance.rs` — deterministic `blake3(aggregate_id, source_id, source_uid)` UIDs +- [ ] `reconcile.rs` — the aggregation engine; pure, no I/O +- [ ] `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` +- [ ] 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. +- [ ] `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 +- [ ] Mass-deletion guard (`max_delete_fraction`), overridable with `--force` +- [ ] Echo suppression: derived UIDs are never re-ingested as source events + +Tests: + +- [ ] `reconcile` table-driven cases: create/update/delete each direction, both-sides-changed, + routing, echo suppression, mass-delete abort +- [ ] `ical` round-trip fixtures: recurring with overrides, all-day, TZID, unknown `X-` props +- [ ] Integration against Radicale plus a `file://` WebCal fixture; assert idempotence +- [ ] Safety: no live `ATTENDEE`/`ORGANIZER` on a CalDAV-targeted mirror, `VALARM` intact, + `PARTSTAT: DECLINED` maps to `TRANSP: TRANSPARENT`, emptying a source aborts +- [ ] Retarget: drift makes `sync` exit non-zero having written nothing and losing no source event + +## 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`