sync: stop deleting a note's checklist items because it isn't a "list"

`_apply_note_items` didn't ignore items on a non-list note — it deleted them.
That was survivable only because nothing in the product could produce a note
holding both a body and items.

M13 makes exactly that the normal shape: a checklist is something a note HAS,
not something a note IS. Against that shape this guard is a data-loss path — the
first sync after adding a checklist to a note would wipe it.

Landing it before the UI that can create the state, so there is never a window
where the two disagree. `kind` itself, and the rest of the merge, follow.
This commit is contained in:
2026-08-22 12:45:14 -04:00
parent ad21eac5bc
commit 229076c82d
+14 -4
View File
@@ -209,12 +209,22 @@ def _assign_note_fields(note: Note, ch: dict) -> None:
async def _apply_note_items(db, note: Note, ch: dict) -> None:
"""Replace the note's checklist items with the client's (items sync inline)."""
if note.kind != "list":
await db.execute(sa_delete(NoteItem).where(NoteItem.note_id == note.id))
return
"""Replace the note's checklist items with the client's (items sync inline).
Applies to ANY note. This used to delete every item when the note wasn't
`kind == "list"`, which was survivable only because nothing could produce a note
holding both a body and items. M13 makes that the normal shape — a checklist is
something a note HAS, not something a note IS — and against that shape the old
guard was a data-loss path: the first sync after adding a checklist to a note
would have wiped it.
Removed ahead of the UI that can create the state, deliberately, so there is no
window in which the two disagree.
"""
items = ch.get("items")
if not isinstance(items, list):
# Absent means "not telling us", not "empty". A client that omits the key
# leaves what the server has; only an explicit [] clears it.
return
await db.execute(sa_delete(NoteItem).where(NoteItem.note_id == note.id))
for pos, it in enumerate(items):