sync(chunk 1): VTODO mapper with an unknown-property round-trip
A hand-rolled content-line model instead of ical4j: a raw (name, params, value) tree is what the round-trip requirement wants, and a typed model normalises away exactly what has to survive. lib-recur already does RRULE and java.time is native at minSdk 29, so the 2.2 MB of zone data and the registry shims buy nothing. Deviates from SYNC.md's library table — see SYNC-PLAN.md decision 4. - domain/ical: parser, serialiser, value codecs. No Android, no data types, so the floret-kit extraction stays a file move. - data/tasks/ical/VTodoMapper: VTODO <-> TaskEntity. Claims a property only when it can reproduce it exactly; everything else round-trips verbatim through TaskEntity.unknown_properties, which already existed at v1 — no migration needed, and BEGIN/END lines carry the nesting the plan thought needed a second table. - 19 fixtures as the specification, with the canonical comparison harness from SYNC.md: no property lost, modulo the enumerated allowlist. - ICalendarWriter now delegates folding and escaping rather than carrying its own copy. VALARM ownership settled: neither side writes the other's alarms. VALARMs round-trip in the residue, local reminders stay in task_alarms. The setAlarm collision was the provider's; the two stores are now disjoint.
This commit is contained in:
+37
-1
@@ -24,7 +24,7 @@ and says so rather than quietly picking the other branch.
|
||||
| 1 | dav4jvm + cert4android distribution, and the Java-21 wall | **Vendor a known-good tree**, recompiled at our Java 17 target | `FAIL_ON_PROJECT_REPOS` stays; no Ktor/guava/xpp3 tail; MPL-2.0 headers retained and the attribution screen becomes mandatory, not optional. Freezes the API churn that shipped two breaking majors nineteen days apart |
|
||||
| 2 | Conflict policy on 412 | **Server wins, local edit discarded** | DAVx5's policy. Terminates, always. The discarded edit is *surfaced* — a sync report the user can read — because silence is what makes this policy feel like data loss. The draft's "preserve as a duplicate" stays withdrawn: RFC 4791 §4.1 makes it unuploadable |
|
||||
| 3 | External mode's future | **Kept, unchanged** | The mapper and the UI stay dual-capable. Retiring it is a separate decision that does not block sync, and it ships and works today |
|
||||
| 4 | iCalendar library | **ical4j 4.3.0**, with the shims `SYNC.md` § *Libraries* enumerates | BSD-3 (attribution screen again). Costs the 2.2 MB of duplicated zone data, the mandatory `ical4j.properties` + `MapTimeZoneCache` + registry shim, and R8 keep rules. biweekly's missing tz database is the disqualifier for CalDAV round-tripping |
|
||||
| 4 | iCalendar library | **None — a hand-rolled content-line model.** ⚠️ Overturned during chunk 1; `SYNC.md` § *Libraries* assumed a typed library was the only option | A raw `(name, params, value)` content-line tree is what the unknown-property requirement actually wants: a typed model normalises away exactly what must survive. `ICalendarWriter` already folds in 204 lines, `lib-recur` is already a direct `:app` dependency and does `RRULE`, and `java.time` is native at minSdk 29 — so ical4j's 2.2 MB of duplicated zone data, its production-exhausting `ZoneRulesProvider`, its mandatory properties/cache/registry shims and its release-only R8 failure buy nothing. VTIMEZONE bodies are already on the round-trip allowlist, so they round-trip opaquely. **Cost accepted: we own the lexer.** biweekly was rejected on the same ground as before |
|
||||
|
||||
---
|
||||
|
||||
@@ -134,6 +134,42 @@ local reminder edit destroys every server-side alarm on that task.
|
||||
**Done when:** the corpus round-trips, the migration test passes both directions,
|
||||
and no fixture loses an unknown property.
|
||||
|
||||
### Settled while building it
|
||||
|
||||
- ⚠️ **No schema migration was needed.** `TaskEntity.unknown_properties` already
|
||||
exists at v1, reserved for exactly this and written by nothing. The residue
|
||||
fills it. It holds leftover properties followed by whole sub-component blocks —
|
||||
`BEGIN`/`END` lines are content lines like any other, so nesting and unknown
|
||||
sub-components fall out of the same representation, which is what the plan
|
||||
thought needed a second table.
|
||||
- **`VALARM` ownership: neither side writes the other's alarms.** `VALARM`s
|
||||
round-trip in the residue and are never authored; local reminders live in
|
||||
`task_alarms` and are never serialised. The collision `SYNC.md` describes was
|
||||
`AndroidTasksDataSource.setAlarm` bulk-deleting *provider property rows* — in
|
||||
the own-store world the two stores are disjoint and neither can destroy the
|
||||
other. Merging them is a later decision; the point is that it is now a
|
||||
decision, not an accident.
|
||||
- **The mapper claims a property only when it can reproduce it exactly.**
|
||||
Everything else stays in the residue: a value it cannot parse or that is out of
|
||||
range (`PRIORITY:11`, `PERCENT-COMPLETE:150`, `SEQUENCE:x`, `STATUS:X-DEFERRED`),
|
||||
a time it can read but not reproduce (floating, or a `TZID` this device's tzdb
|
||||
lacks), and a `DTSTART`/`DUE` pair that disagrees on value type or timezone —
|
||||
one `is_all_day` flag and one `timezone` column cannot author both, and
|
||||
flattening the odd one out destroys it. Clamping or defaulting any of these
|
||||
would be a silent rewrite of somebody's data. `SUPPRESSED_BY_RESIDUE` makes
|
||||
"present in the residue" mean "do not author this".
|
||||
- ⚠️ **Suppression is conditional, or editing breaks.** A residue property
|
||||
suppresses its column only while the two still agree. If the user edits a field
|
||||
whose stamp was unreproducible, the stale residue copy is evicted and the
|
||||
column is authored — otherwise changing the due date of an imported task would
|
||||
silently do nothing on the server. `RELATED-TO` is the deliberate exception: a
|
||||
parent we have not fetched is not the same as no parent, so an unresolvable
|
||||
link is kept rather than destroyed.
|
||||
- **One normalisation is allowed beyond the stated allowlist**, and it is an
|
||||
equivalence rather than a concession: an absent `STATUS` and
|
||||
`STATUS:NEEDS-ACTION` mean the same thing for a to-do. Any other `STATUS`
|
||||
compares exactly, so losing a completion still fails the corpus.
|
||||
|
||||
---
|
||||
|
||||
## Chunk 2 — auth, discovery, account
|
||||
|
||||
Reference in New Issue
Block a user