CalCalist/TODO.md
randogoth ef399db4d8 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>
2026-09-10 09:34:50 +03:00

3 KiB

Roadmap

Milestones from the implementation plan. See SPECS.md for the architecture and the sync semantics these items implement.

M0 — skeleton (done)

  • Initialise jj colocated with git; .gitignore written before the first build
  • devbox.json pinning Rust 1.97.1, pimsync 0.5.11, jujutsu 0.44.0, radicale 3.7.8
  • Configuration model with referential validation, reporting every problem in one pass
  • calcalist doctor — pimsync presence and version series, state directory, configuration
  • Define the full CLI surface; unimplemented commands exit 2 rather than pretend
  • 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