content: tree-CRDT orphan reparenting (no content loss on concurrent delete-section) + 4 tests (742/742)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 32s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 32s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,13 +4,14 @@
|
||||
;; (the id of its containing section, "" = root) alongside its Logoot position.
|
||||
;; Merge is still a join — it reuses crdt.sx's position/register/field merges and
|
||||
;; adds parent (immutable, set once at insert). Materialisation rebuilds the
|
||||
;; ordered tree: root = elements with parent "", a section's children = elements
|
||||
;; whose parent is that section's id, each sorted by position. Commutative,
|
||||
;; associative, idempotent like the flat layer; concurrent inserts into the same
|
||||
;; or different parents converge deterministically.
|
||||
;; ordered tree: root = elements with parent "" (plus ORPHANS — elements whose
|
||||
;; parent is not a live section, e.g. after a concurrent delete-section +
|
||||
;; insert-child, so content is never silently lost); a section's children =
|
||||
;; elements whose parent is that section's id. Commutative/associative/idempotent
|
||||
;; like the flat layer.
|
||||
;;
|
||||
;; Requires (loaded by harness): crdt.sx (merge helpers + live/sort/materialise
|
||||
;; bits), block.sx, doc.sx, section.sx (mk-section).
|
||||
;; bits + crdt-member?), block.sx, doc.sx, section.sx (mk-section).
|
||||
|
||||
(define ctt-merge-parent (fn (p1 p2) (if (= p1 nil) p2 p1)))
|
||||
|
||||
@@ -129,6 +130,34 @@
|
||||
(crdt-tree-apply-all (crdt-tree-apply state (first ops)) (rest ops)))))
|
||||
|
||||
;; ── materialise to a Phase-1 document (rebuild the ordered tree) ──
|
||||
(define
|
||||
ctt-live-section-ids
|
||||
(fn
|
||||
(state)
|
||||
(map
|
||||
(fn (e) (get e :id))
|
||||
(filter
|
||||
(fn (e) (= (get e :type) "section"))
|
||||
(crdt-live-elements state)))))
|
||||
|
||||
;; an element belongs at root if its parent is "" or its parent is not a live
|
||||
;; section (orphan-reparenting: don't lose content when its section is deleted).
|
||||
(define
|
||||
ctt-roots
|
||||
(fn
|
||||
(state)
|
||||
(let
|
||||
((secids (ctt-live-section-ids state)))
|
||||
(crdt-sort-by-pos
|
||||
(filter
|
||||
(fn
|
||||
(e)
|
||||
(if
|
||||
(= (get e :parent) "")
|
||||
true
|
||||
(if (crdt-member? (get e :parent) secids) false true)))
|
||||
(crdt-live-elements state))))))
|
||||
|
||||
(define
|
||||
ctt-children
|
||||
(fn
|
||||
@@ -157,8 +186,8 @@
|
||||
(doc-id state)
|
||||
(doc-new
|
||||
doc-id
|
||||
(map (fn (e) (ctt-element->block state e)) (ctt-children state "")))))
|
||||
(map (fn (e) (ctt-element->block state e)) (ctt-roots state)))))
|
||||
|
||||
(define
|
||||
crdt-tree-order
|
||||
(fn (state) (map (fn (e) (get e :id)) (ctt-children state ""))))
|
||||
(fn (state) (map (fn (e) (get e :id)) (ctt-roots state))))
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"store": {"pass": 29, "fail": 0},
|
||||
"snapshot": {"pass": 20, "fail": 0},
|
||||
"crdt": {"pass": 34, "fail": 0},
|
||||
"crdt-tree": {"pass": 17, "fail": 0},
|
||||
"crdt-tree": {"pass": 21, "fail": 0},
|
||||
"crdt-blocks": {"pass": 7, "fail": 0},
|
||||
"crdt-store": {"pass": 14, "fail": 0},
|
||||
"sync": {"pass": 14, "fail": 0},
|
||||
@@ -42,7 +42,7 @@
|
||||
"md-doc": {"pass": 12, "fail": 0},
|
||||
"fed": {"pass": 20, "fail": 0}
|
||||
},
|
||||
"total_pass": 738,
|
||||
"total_pass": 742,
|
||||
"total_fail": 0,
|
||||
"total": 738
|
||||
"total": 742
|
||||
}
|
||||
|
||||
@@ -38,11 +38,11 @@ _Generated by `lib/content/conformance.sh`_
|
||||
| store | 29 | 0 | 29 |
|
||||
| snapshot | 20 | 0 | 20 |
|
||||
| crdt | 34 | 0 | 34 |
|
||||
| crdt-tree | 17 | 0 | 17 |
|
||||
| crdt-tree | 21 | 0 | 21 |
|
||||
| crdt-blocks | 7 | 0 | 7 |
|
||||
| crdt-store | 14 | 0 | 14 |
|
||||
| sync | 14 | 0 | 14 |
|
||||
| md-import | 38 | 0 | 38 |
|
||||
| md-doc | 12 | 0 | 12 |
|
||||
| fed | 20 | 0 | 20 |
|
||||
| **Total** | **738** | **0** | **738** |
|
||||
| **Total** | **742** | **0** | **742** |
|
||||
|
||||
@@ -251,3 +251,39 @@
|
||||
"two-replica tree ids"
|
||||
(doc-tree-ids (crdt-tree-materialize "d" (crdt-tree-merge repl1 repl2)))
|
||||
(list "s1" "p1" "s2" "p2"))
|
||||
|
||||
;; ── orphan reparenting: concurrent delete-section + insert-child ──
|
||||
;; A deletes section s; B inserts a child into s. After merge, s is gone but the
|
||||
;; child must survive (reparented to root), not silently vanish.
|
||||
(define delA (crdt-tree-delete base "s"))
|
||||
(define
|
||||
insB
|
||||
(crdt-tree-insert
|
||||
base
|
||||
"c"
|
||||
"text"
|
||||
(crdt-pos 9 0)
|
||||
"s"
|
||||
(list (list "text" "kept"))
|
||||
5
|
||||
2))
|
||||
(define orphan-merge (crdt-tree-merge delA insB))
|
||||
(content-test
|
||||
"orphan survives delete-section"
|
||||
(doc-tree-ids (crdt-tree-materialize "d" orphan-merge))
|
||||
(list "h" "c"))
|
||||
(content-test
|
||||
"orphan reparent commutes"
|
||||
(same? orphan-merge (crdt-tree-merge insB delA))
|
||||
true)
|
||||
(content-test
|
||||
"orphan content preserved"
|
||||
(str
|
||||
(blk-send
|
||||
(doc-deep-find (crdt-tree-materialize "d" orphan-merge) "c")
|
||||
"text"))
|
||||
"kept")
|
||||
(content-test
|
||||
"orphan render at root"
|
||||
(asHTML (crdt-tree-materialize "d" orphan-merge))
|
||||
"<h2>Sub</h2><p>kept</p>")
|
||||
|
||||
@@ -19,7 +19,7 @@ injected adapter, not core.
|
||||
|
||||
## Status (rolling)
|
||||
|
||||
`bash lib/content/conformance.sh` → **738/738** (Phases 1–4 COMPLETE + ~34 extensions, hardened: HTML/SX escaping, Markdown render + import/export incl. tables & frontmatter (full round-trip), CvRDT flat + nested-tree + durable replication, tree-aware validation, snapshot cache, doc metadata, plain-text render, nested block trees + deep editing + flatten + relative reorder, doc stats + summary + multi-doc index, table + callout + media blocks, HTML page wrapper + SEO page, doc composition + id-remap, portable data + wire serialization, block query + transforms + find/replace, TOC + anchored headings + outline, normalization)
|
||||
`bash lib/content/conformance.sh` → **742/742** (Phases 1–4 COMPLETE + ~34 extensions, hardened: HTML/SX escaping, Markdown render + import/export incl. tables & frontmatter (full round-trip), CvRDT flat + nested-tree + durable replication, tree-aware validation, snapshot cache, doc metadata, plain-text render, nested block trees + deep editing + flatten + relative reorder, doc stats + summary + multi-doc index, table + callout + media blocks, HTML page wrapper + SEO page, doc composition + id-remap, portable data + wire serialization, block query + transforms + find/replace, TOC + anchored headings + outline, normalization)
|
||||
|
||||
## Ground rules
|
||||
|
||||
@@ -113,6 +113,13 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─
|
||||
|
||||
## Progress log
|
||||
|
||||
- 2026-06-07 — Hardening: tree-CRDT orphan reparenting. Concurrent
|
||||
delete-section + insert-child previously orphaned the child (its parent no
|
||||
longer a live section) → silently dropped from materialise. Fixed
|
||||
`crdt-tree-materialize`/`crdt-tree-order` to root any element whose parent is
|
||||
"" OR not a live section, so content is never lost on concurrent edits. +4
|
||||
tests (orphan survives, commutes, content preserved, renders at root). Suite
|
||||
742/742.
|
||||
- 2026-06-07 — Hardening: regression suite `crdt-blocks` (7 tests) locking that
|
||||
non-core block types (callout/table/media/section) survive both the flat and
|
||||
nested-tree CvRDT materialise paths (insert → merge → materialise → render),
|
||||
|
||||
Reference in New Issue
Block a user