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.
|
;; (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
|
;; 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
|
;; adds parent (immutable, set once at insert). Materialisation rebuilds the
|
||||||
;; ordered tree: root = elements with parent "", a section's children = elements
|
;; ordered tree: root = elements with parent "" (plus ORPHANS — elements whose
|
||||||
;; whose parent is that section's id, each sorted by position. Commutative,
|
;; parent is not a live section, e.g. after a concurrent delete-section +
|
||||||
;; associative, idempotent like the flat layer; concurrent inserts into the same
|
;; insert-child, so content is never silently lost); a section's children =
|
||||||
;; or different parents converge deterministically.
|
;; 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
|
;; 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)))
|
(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)))))
|
(crdt-tree-apply-all (crdt-tree-apply state (first ops)) (rest ops)))))
|
||||||
|
|
||||||
;; ── materialise to a Phase-1 document (rebuild the ordered tree) ──
|
;; ── 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
|
(define
|
||||||
ctt-children
|
ctt-children
|
||||||
(fn
|
(fn
|
||||||
@@ -157,8 +186,8 @@
|
|||||||
(doc-id state)
|
(doc-id state)
|
||||||
(doc-new
|
(doc-new
|
||||||
doc-id
|
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
|
(define
|
||||||
crdt-tree-order
|
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},
|
"store": {"pass": 29, "fail": 0},
|
||||||
"snapshot": {"pass": 20, "fail": 0},
|
"snapshot": {"pass": 20, "fail": 0},
|
||||||
"crdt": {"pass": 34, "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-blocks": {"pass": 7, "fail": 0},
|
||||||
"crdt-store": {"pass": 14, "fail": 0},
|
"crdt-store": {"pass": 14, "fail": 0},
|
||||||
"sync": {"pass": 14, "fail": 0},
|
"sync": {"pass": 14, "fail": 0},
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
"md-doc": {"pass": 12, "fail": 0},
|
"md-doc": {"pass": 12, "fail": 0},
|
||||||
"fed": {"pass": 20, "fail": 0}
|
"fed": {"pass": 20, "fail": 0}
|
||||||
},
|
},
|
||||||
"total_pass": 738,
|
"total_pass": 742,
|
||||||
"total_fail": 0,
|
"total_fail": 0,
|
||||||
"total": 738
|
"total": 742
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,11 +38,11 @@ _Generated by `lib/content/conformance.sh`_
|
|||||||
| store | 29 | 0 | 29 |
|
| store | 29 | 0 | 29 |
|
||||||
| snapshot | 20 | 0 | 20 |
|
| snapshot | 20 | 0 | 20 |
|
||||||
| crdt | 34 | 0 | 34 |
|
| crdt | 34 | 0 | 34 |
|
||||||
| crdt-tree | 17 | 0 | 17 |
|
| crdt-tree | 21 | 0 | 21 |
|
||||||
| crdt-blocks | 7 | 0 | 7 |
|
| crdt-blocks | 7 | 0 | 7 |
|
||||||
| crdt-store | 14 | 0 | 14 |
|
| crdt-store | 14 | 0 | 14 |
|
||||||
| sync | 14 | 0 | 14 |
|
| sync | 14 | 0 | 14 |
|
||||||
| md-import | 38 | 0 | 38 |
|
| md-import | 38 | 0 | 38 |
|
||||||
| md-doc | 12 | 0 | 12 |
|
| md-doc | 12 | 0 | 12 |
|
||||||
| fed | 20 | 0 | 20 |
|
| fed | 20 | 0 | 20 |
|
||||||
| **Total** | **738** | **0** | **738** |
|
| **Total** | **742** | **0** | **742** |
|
||||||
|
|||||||
@@ -251,3 +251,39 @@
|
|||||||
"two-replica tree ids"
|
"two-replica tree ids"
|
||||||
(doc-tree-ids (crdt-tree-materialize "d" (crdt-tree-merge repl1 repl2)))
|
(doc-tree-ids (crdt-tree-materialize "d" (crdt-tree-merge repl1 repl2)))
|
||||||
(list "s1" "p1" "s2" "p2"))
|
(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)
|
## 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
|
## Ground rules
|
||||||
|
|
||||||
@@ -113,6 +113,13 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─
|
|||||||
|
|
||||||
## Progress log
|
## 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
|
- 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
|
non-core block types (callout/table/media/section) survive both the flat and
|
||||||
nested-tree CvRDT materialise paths (insert → merge → materialise → render),
|
nested-tree CvRDT materialise paths (insert → merge → materialise → render),
|
||||||
|
|||||||
Reference in New Issue
Block a user