docs: decide to build our own store and delete the vendored provider

The vendored dmfs provider was kept on the grounds that it hands us the
sync bookkeeping for free. The phase-1 sync audit measured that
bookkeeping and found most of it broken, absent, or unusable: _DIRTY not
set on delete, no home for a per-collection sync token, read-only
collections inexpressible, ACCOUNT_TYPE write-once so enabling sync is a
full migration, and cleanUpLists able to delete a user's lists after a
backup restore. Sixteen findings are provider-imposed rather than
platform- or protocol-imposed.

Costing the alternative showed the swap is far smaller than assumed.
TasksDataSource is already a 14-method, domain-shaped interface;
exactly one file above the data layer references TasksContract. The
work is a second implementation behind an interface built for it, not a
rewrite. Against ~5 weeks to build, owning the store removes 2.5-4
weeks from the sync plan, and 8,200 of the vendored 14,555 lines are
things we would never write - 23 migrations from a 2013 schema, 798
lines of full-text search the app has zero call sites for, and 1,581
lines of a type-safe layer over ContentValues that Room deletes.

External mode (OpenTasks, tasks.org) is unaffected and keeps every
file that describes somebody else's schema.

STORAGE-DECISION.md is the reasoning; OWN-STORE.md is the architecture
and the six-phase plan. :provider stays in-tree until phase 5 so
recurrence parity can be tested against it before it goes.

Also corrected here: the provider's JVM test count (51 -> 56, measured
from the test-results XML) and a fourth site of the debunked "switching
sync on is never a migration" claim, in StorageMode.kt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-13 14:46:07 +02:00
parent 98ed339346
commit 13cb27b2ab
10 changed files with 1984 additions and 39 deletions

View File

@@ -1,5 +1,15 @@
# Agendula — storage and sync
> ⚠️ **Partly superseded, 2026-08-13.** The core decision below — *vendor the
> dmfs provider in-tree as `:provider`* — has been **reversed**. Agendula builds
> its own Room store and deletes the vendored provider; External mode (OpenTasks,
> tasks.org) is unaffected and everything this document says about it still
> stands. See [`STORAGE-DECISION.md`](STORAGE-DECISION.md) for why and
> [`OWN-STORE.md`](OWN-STORE.md) for what replaces it. The permissions,
> distribution, storage-mode and dead-end sections below remain accurate; treat
> the "our own provider" sections as the historical record of a decision that was
> made, shipped, and then costed properly.
> Decided direction, captured 2026-08-01. Supersedes the earlier "Posture B =
> bundle OpenTasks" working notes, which are withdrawn (see
> [Dead ends](#dead-ends--do-not-revisit)). This is the detailed companion to
@@ -34,7 +44,7 @@
| 2 | Vendor `:provider` under our own authority | the identity, done once — and it ships a complete local-first app | ✅ done |
| 3 | Export / backup | our data now lives only in our app's private storage | ✅ backend done; no UI yet |
| 4 | File the DAVx5 issue | cheap, non-blocking, serves F-Droid users | ⬜ |
| 5 | Sync adapter | the 1.x arc; design discussion pending | ⬜ |
| 5 | Sync adapter | the 1.x arc; designed in [`SYNC.md`](SYNC.md), not yet built | ⬜ |
Everything below is the reasoning behind those choices, the alternatives that
were rejected, and the constraints they have to survive.
@@ -174,8 +184,13 @@ feature — see [Storage modes](#storage-modes--the-users-choice).
| **Synced** | our bundled provider | our sync adapter | network + an account the user configures |
| **External** | OpenTasks / tasks.org | whatever that provider's engine does (DAVx5 …) | that provider's `READ`/`WRITE_TASKS`, granted at runtime |
Local and Synced are the same store — Synced is Local with an account attached,
so switching on sync is not a migration.
Local and Synced are the same store — but ⚠️ **switching on sync *is* a
migration, contrary to what this document said until 2026-08-13.** The provider
enforces `ACCOUNT_NAME` and `ACCOUNT_TYPE` as **write-once** on a task list
(`processors/lists/Validating.java:68-76`, which throws), so a list created under
`org.dmfs.account.LOCAL` can never be re-pointed at a real account. Enabling sync
means creating new lists under the account and moving tasks into them. See
[`SYNC.md`](SYNC.md) — it is a costed deliverable there, not a free consequence.
**Resolver ordering — decided, and it went both ways as expected.**
`ProviderResolver` now takes an explicit `StorageMode` from Settings when there
@@ -235,10 +250,17 @@ compliance and a small enum-shaped addition with near-zero ongoing maintenance
for them. Say that explicitly. "Here's a change that can't break anything" lands
very differently from "please support my app."
**Open — the next discussion.** Protocol coverage ("support as much as
possible"), the account model, conflict resolution, and where the DAV/iCalendar
work lives. One constraint to settle early: we're MIT; `dav4jvm` is Apache-2.0
and fine, but **verify `ical4android`'s license** before assuming it's usable.
**The design is now written out in [`SYNC.md`](SYNC.md)** — protocol coverage,
the account model, conflict resolution, the VTODO ↔ `TaskContract` mapper, and
where the DAV/iCalendar work lives. Two corrections to what this section
originally said, both verified 2026-08-13:
- `dav4jvm` is **MPL-2.0**, not Apache-2.0. Still fine against our MIT (file-level
copyleft), but it is ⚠️ **JitPack-only**, which collides with our
`FAIL_ON_PROJECT_REPOS` + `google()`/`mavenCentral()` policy and with the
JitPack dead end below. `SYNC.md` open question 1.
- `ical4android` is **superseded by `synctools`, which is GPLv3** — so it is
unusable, and the open question below is closed. We write the mapper in-house.
---
@@ -336,6 +358,13 @@ Not on either yet; both are targets, so build *for* them rather than retrofittin
throwaway spike* (a library string resource can be overridden from the app
module, so the authority rename works), but not for anything we ship.
⚠️ **This one comes back.** It is a dead end *for the provider*, where
vendoring was mandatory anyway. `dav4jvm` is JitPack-only, so the sync adapter
has to answer the same question on its own terms — and F-Droid turns out not to
be the obstacle (its inclusion policy trusts jitpack.io for freely-licensed
artifacts); our own trust-surface policy is. See [`SYNC.md`](SYNC.md) open
question 1.
---
## Sequencing
@@ -358,8 +387,11 @@ the roadmap should say so rather than inheriting the old estimate.
## Open questions
1. **Sync protocol coverage**, account model, conflict resolution — the next
discussion. Still open.
1. **Sync protocol coverage**, account model, conflict resolution — ✅ taken up in
[`SYNC.md`](SYNC.md). The account model is answered there (`AccountManager`,
which `PROVENANCE.md` change 1 already assumes); what stays open moves to that
document's own list — dav4jvm's distribution, conflict policy, External mode's
future, and recurring-task completion.
2. **Resolver ordering** — ✅ decided, see
[Storage modes](#storage-modes--the-users-choice). The **mode-selection UX**
is still open: `autoMode()` picks a default, but the Settings override it
@@ -376,9 +408,11 @@ the roadmap should say so rather than inheriting the old estimate.
⚠️ **But that test is Robolectric, and it skips on ARM64**, where Robolectric
has no SQLite backend in either mode. It runs on x86_64 CI. It is not a
substitute for a device, and this remains on the device-verification list.
4. **`ical4android` licensing** vs our MIT. Still open — and note the export path
does *not* depend on it: `ICalendarWriter` is our own ~200 lines, no library.
The question is really about the sync adapter's iCalendar *parsing*.
4. ~~**`ical4android` licensing** vs our MIT.~~ ✅ Closed: `ical4android` is
superseded by `synctools`, which is **GPLv3**, so it is out — as is
`cert4android`. The export path never depended on it anyway (`ICalendarWriter`
is our own ~200 lines). The sync adapter's iCalendar parsing goes through an
in-house mapper over `ical4j`/`biweekly`; see [`SYNC.md`](SYNC.md).
5. **jtx Board** as an additional External-mode candidate — richer contract,
later. (`PLAN.md` decision #3, still open.)
6. **The vendored provider's timezone-change behaviour** — upstream's receiver