Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
|
|
|
# calcalist
|
|
|
|
|
|
|
|
|
|
Aggregates events between CalDAV, Google Calendar and iCal feeds. `README.md` is
|
|
|
|
|
the user-facing guide; the design rationale lives in the module doc comments,
|
|
|
|
|
beside the code it governs.
|
|
|
|
|
|
|
|
|
|
These conventions were `SPECS.md`'s, which has been retired now the roadmap is
|
|
|
|
|
finished. Git history holds it, and `TODO.md`, if the record is ever wanted.
|
|
|
|
|
|
|
|
|
|
## Development
|
|
|
|
|
|
|
|
|
|
Use devbox for dependency management and scripts. Use jj for version control
|
|
|
|
|
(colocated with git).
|
|
|
|
|
|
|
|
|
|
- `devbox run fmt` — format.
|
|
|
|
|
- `devbox run check` — format check, clippy with warnings denied, and tests.
|
|
|
|
|
This is the gate; jj has no commit step to hang a hook off, since the working
|
|
|
|
|
copy is itself a commit.
|
|
|
|
|
- `devbox run test` — tests only.
|
|
|
|
|
|
|
|
|
|
Keep `/target` in `.gitignore`: jj snapshots the working copy on every command,
|
|
|
|
|
with no staging step.
|
|
|
|
|
|
|
|
|
|
The integration tests in `tests/` need `radicale` and `pimsync`, both of which
|
|
|
|
|
devbox provides. Outside that shell they report what is missing and pass.
|
|
|
|
|
|
|
|
|
|
## Coding style
|
|
|
|
|
|
|
|
|
|
- Simple, readable, idiomatic.
|
|
|
|
|
- Explicit types for public APIs and important fields.
|
|
|
|
|
- Prefer immutable data structures.
|
|
|
|
|
- No global mutable state.
|
|
|
|
|
- No speculative features or dependencies.
|
|
|
|
|
- Keep functions short and single-purpose; extract helpers to avoid nesting.
|
|
|
|
|
- File and module naming: `snake_case.rs`. Types and traits: `PascalCase`.
|
|
|
|
|
Functions, variables and fields: `snake_case`. Constants:
|
|
|
|
|
`SCREAMING_SNAKE_CASE`.
|
|
|
|
|
- Run `devbox run check` before describing a change.
|
|
|
|
|
- Comment only when intent is not obvious from the code.
|
|
|
|
|
- Prefer composition and traits over deep type hierarchies.
|
|
|
|
|
|
|
|
|
|
## Decisions already taken
|
|
|
|
|
|
|
|
|
|
Settled after real design work. Reopen only with new information, not from
|
|
|
|
|
first principles.
|
|
|
|
|
|
|
|
|
|
**No configuration web interface.** Designed in full, then dropped. Its audience
|
|
|
|
|
would be people who find TOML hard — but every user must first create a Google
|
|
|
|
|
Cloud project, configure a consent screen and put a secret in a keyring, which
|
|
|
|
|
is a far higher bar than editing the config. Anyone who clears it can edit the
|
|
|
|
|
file; anyone who cannot never reaches the file. `doctor`, `status` and
|
|
|
|
|
`discover` cover what the interface was actually for.
|
|
|
|
|
|
|
|
|
|
**Bring-your-own OAuth client, not a shipped verified one.** Verification for
|
|
|
|
|
the Calendar scope needs a domain you own — a `github.io` address is rejected —
|
|
|
|
|
and Calendar API quota is per Cloud project, with charges announced for
|
|
|
|
|
exceeding it. A shipped client would pool every user's syncing into the
|
|
|
|
|
publisher's project, and their bill. Shipping an *unverified* shared client is
|
|
|
|
|
worse still: the 100-new-user cap is permanent for the project's lifetime.
|
|
|
|
|
|
|
|
|
|
**pimsync cannot reach Google, and never will.** It has no REST storage, and its
|
|
|
|
|
config exposes only HTTP Basic auth, which Google's CalDAV endpoint rejected in
|
|
|
|
|
March 2025. The Google leg is calcalist's own in every design.
|
|
|
|
|
|
|
|
|
|
**The config file is never machine-rewritten.** It is the portable,
|
|
|
|
|
hand-editable artefact. Anything that writes it must preserve comments,
|
|
|
|
|
ordering and omitted defaults — `toml::to_string` on the `Serialize` derive
|
|
|
|
|
does none of those things.
|
|
|
|
|
|
Sync WebCal and CalDAV directly; add a skip_past cutoff
WebCal is calcalist's own (src/webcal.rs), with Calendar::split_by_uid to turn
one feed document into one file per UID. Shared crate::http and crate::secret
came out of google/ so every leg uses them.
CalDAV follows, behind a per-endpoint 'native' flag so it can run beside
pimsync one collection at a time: src/caldav/client.rs speaks PROPFIND,
sync-collection, calendar-multiget and conditional PUT/DELETE, and
src/caldav/mod.rs is the pull/push pair. Radicale answers an initial
sync-collection with no members, so a cursor is only stored alongside a
complete read. doctor reports what a server advertises.
skip_past (src/horizon.rs) takes a date, not a flag: a rolling 'before now'
would retire events from the aggregate as they aged, deleting yesterday's
events on every cycle. Pinned to a date, what is mirrored stays mirrored. On
the real configuration it leaves 2600 of 3282 events behind. The filter sits
above the prior-links lookup in mirror_source, which is what makes it safe:
from there DeleteSource is unreachable, so excluding an event can never be
read as the user having deleted its mirror.
pimsync: CWD pinned so vstorage's relative unlink cannot reach the user's
files; the dry-run workspace is removed by Drop rather than by a line every
'?' stepped over.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 15:33:22 +03:00
|
|
|
**WebCal and Google are calcalist's own; CalDAV is being taken back too.** A
|
|
|
|
|
feed is one iCalendar document fetched over HTTP and read-only by protocol, so
|
|
|
|
|
routing it through a generated config, a subprocess and a status database bought
|
|
|
|
|
nothing — it cost three minutes a cycle in observed stalls. CalDAV has followed,
|
|
|
|
|
behind a per-endpoint `native` flag so the two run side by side until the
|
|
|
|
|
native client has earned the default. The argument
|
|
|
|
|
is not that pimsync is bad but that calcalist was paying for generality it never
|
|
|
|
|
used: one server, against 528 lines of adapter, a 59.6 MB flake closure, an
|
|
|
|
|
undocumented `discover` format that is scraped, a version range to maintain, and
|
|
|
|
|
a `vstorage` bug that unlinked `order` relative to the working directory.
|
|
|
|
|
|
|
|
|
|
**Radicale is the CalDAV target; other servers are untested, not unsupported.**
|
|
|
|
|
Narrowing is what makes owning the protocol reasonable: the integration tests
|
|
|
|
|
already run against Radicale, so every assumption is checked on every
|
|
|
|
|
`devbox run check`, and when the RFC is ambiguous its source settles it. The
|
|
|
|
|
discipline that keeps this honest is that server-specific accommodations stay
|
|
|
|
|
*named*, never absorbed into the general path — Radicale answers an initial
|
|
|
|
|
`sync-collection` with a token and no members, which is written as "always load
|
|
|
|
|
in full first, then go incremental", correct anywhere, rather than as a quirk
|
|
|
|
|
worked around in place. `doctor` reports what a server advertises so that
|
|
|
|
|
pointing calcalist elsewhere fails legibly.
|
|
|
|
|
|
Cut the README to a getting-started guide; retire SPECS.md and TODO.md
The README had grown to 413 lines and explained as much as it instructed:
someone trying to get calcalist running read past the architecture, the
scheduling-inertness rule and the reasoning behind retarget before reaching
anything they needed to type.
Most of the excess was duplication of SPECS.md, which is now retired along with
TODO.md since the roadmap is finished. That inverted the usual answer — the
rationale could not simply move to the design document, because there would not
be one. Checking what would actually be orphaned: the architecture rationale is
already in the module doc comments, beside the code it governs and where it
cannot drift; the M0-M2 record is history and git keeps it. Two things had no
home: the working agreement that has shaped every commit, and two decisions
that cost real design work to decline and would otherwise be re-proposed from
first principles. Both are now in CLAUDE.md, which is read at the start of every
session rather than filed and forgotten.
What the README keeps is the whole setup path, because none of it exists
anywhere else now: every configuration field, the Google OAuth walkthrough in
full, routing, and the systemd units. What it gains is a short section on what
the tool does to your calendars — deletion propagating to the source is not
something to discover by accident. What it loses is the design commentary.
Also moves `discover` out of the command list and into the CalDAV endpoint
section, since getting the url field right is its entire purpose.
Verified rather than eyeballed: the worked example was extracted and run through
doctor and status, the command table diffed against --help, and every internal
anchor resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:38:53 +03:00
|
|
|
## Known gap
|
|
|
|
|
|
Sync WebCal and CalDAV directly; add a skip_past cutoff
WebCal is calcalist's own (src/webcal.rs), with Calendar::split_by_uid to turn
one feed document into one file per UID. Shared crate::http and crate::secret
came out of google/ so every leg uses them.
CalDAV follows, behind a per-endpoint 'native' flag so it can run beside
pimsync one collection at a time: src/caldav/client.rs speaks PROPFIND,
sync-collection, calendar-multiget and conditional PUT/DELETE, and
src/caldav/mod.rs is the pull/push pair. Radicale answers an initial
sync-collection with no members, so a cursor is only stored alongside a
complete read. doctor reports what a server advertises.
skip_past (src/horizon.rs) takes a date, not a flag: a rolling 'before now'
would retire events from the aggregate as they aged, deleting yesterday's
events on every cycle. Pinned to a date, what is mirrored stays mirrored. On
the real configuration it leaves 2600 of 3282 events behind. The filter sits
above the prior-links lookup in mirror_source, which is what makes it safe:
from there DeleteSource is unreachable, so excluding an event can never be
read as the user having deleted its mirror.
pimsync: CWD pinned so vstorage's relative unlink cannot reach the user's
files; the dry-run workspace is removed by Drop rather than by a line every
'?' stepped over.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 15:33:22 +03:00
|
|
|
A failing `pimsync sync` stops every CalDAV endpoint pimsync still owns. It is
|
|
|
|
|
one process covering every pair, and a failure does not say which pair it
|
|
|
|
|
belongs to, so the endpoints cannot be told apart the way the Google ones are.
|
|
|
|
|
`native = true` takes an endpoint out of its reach entirely, which is the way
|
|
|
|
|
out rather than a narrowing of pimsync's own reporting.
|