docs: record the parentage merge landing in ADR 0049, and what is left in TODO
Build Debug APK / build (push) Successful in 3m59s

This commit is contained in:
Greg Shuflin
2026-08-30 03:41:32 -07:00
parent 66f47504c4
commit 7475e52aa4
2 changed files with 26 additions and 13 deletions
+8
View File
@@ -554,6 +554,14 @@ How it works: `rust/lib/src/stores/lock.rs` and
`docs/adr/0018-agent-settings-documents.md` solved for Notes.
### Documents
- **Concurrent moves can still disagree about which one survived.**
`parent_modified_at` merges a reparent last-write-wins, and both `set_parent`
and `sync_receive_parent` refuse to *store* a cycle, so every tree walk
terminates. What is not converged is the case where two nodes each make a
locally legal move that is only a cycle together: each keeps its own and
drops the other's, until someone makes a newer move. Converging it wants a
move log with undo/redo by timestamp (Kleppmann et al., *A highly-available
move operation for replicated trees*). See ADR 0049.
- Look at the Papra open-source project for document UI design ideas; also PdfDing
- Some documents get misleading titles when imported naively (e.g. comics) — importers
should produce sensible titles, since those are what the Documents view shows
+18 -13
View File
@@ -121,19 +121,24 @@ not have to be "parentage becomes an edge."
structural fields are written once, on first arrival, and never updated from
a peer. `DocumentStore::set_parent` therefore has no propagation at all:
`NotebookManager::move_item` moves a note on the node the user is sitting
at, and the ensemble never learns. That is a live gap, not a hypothetical
one, and this decision inherits it. The fix is a merge rule for the field —
a `parent_modified_at` reconciled last-write-wins is the smallest thing that
matches how the rest of the record behaves — and it is now the outstanding
work this ADR points at.
- **Acyclicity is the field's invariant to enforce, and nothing enforces it.**
`set_parent` writes whatever it is given; `NotebookManager::move_item`
checks only that the destination is a collection, not that it is outside the
moved subtree. Moving a collection into its own descendant produces a cycle,
and `apply_tombstone`/`delete_recursive` recurse over children with no
visited set. An edge model would not have fixed this either — it is
pre-existing — but owning the field means owning the invariant, and it
should be checked where the move is made rather than survived downstream.
at, and the ensemble never learns. That was a live gap, not a hypothetical
one, and this decision inherited it. **Closed on the same day this ADR was
written**: `documents.parent_modified_at` is the field's own merge stamp and
`DocumentStore::sync_receive_parent` reconciles a peer's move
last-write-wins, declining when our stamp is newer, when the destination has
not reached us yet, or when the move would close a cycle.
- **Acyclicity is the field's invariant to enforce**, and it is now enforced
where a single node can decide it: `set_parent` refuses a move into the
document's own descendant. What one node cannot decide is two nodes making
moves that are a cycle only once merged; there the merge path refuses to
store the second and logs it, which keeps every tree walk terminating —
`apply_tombstone`, `delete_recursive` and `collect_ancestor_collections` all
recurse without a visited set — at the cost of the two nodes disagreeing
about which move survived until someone makes a newer one. Converging that
properly needs a move log with undo/redo by timestamp (Kleppmann et al., *A
highly-available move operation for replicated trees*) and is deliberately
not built. An edge model would not have fixed this either: it is a property
of concurrent tree moves, not of how the link is stored.
- **Query shape is unchanged**: `idx_documents_parent_id` serves
`list_children`, `only_roots` stays `parent_id IS NULL`, and no tree query
gains a join.