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) <noreply@anthropic.com>
This commit is contained in:
randogoth 2026-09-10 09:34:42 +03:00
parent 2bc93262f8
commit ef399db4d8

57
TODO.md Normal file
View file

@ -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`