diff --git a/lib/content/conformance.sh b/lib/content/conformance.sh
index 4c4ab5b5..11a0a257 100755
--- a/lib/content/conformance.sh
+++ b/lib/content/conformance.sh
@@ -15,7 +15,7 @@ if [ ! -x "$SX_SERVER" ]; then
fi
fi
-SUITES=(block doc render api meta page page-full markdown text section compose stats table data wire validate store snapshot crdt crdt-store sync md-import md-doc fed)
+SUITES=(block doc render api meta page page-full markdown text section compose tree-edit stats table data wire validate store snapshot crdt crdt-store sync md-import md-doc fed)
OUT_JSON="lib/content/scoreboard.json"
OUT_MD="lib/content/scoreboard.md"
@@ -46,6 +46,7 @@ run_suite() {
(load "lib/content/text.sx")
(load "lib/content/section.sx")
(load "lib/content/compose.sx")
+(load "lib/content/tree-edit.sx")
(load "lib/content/stats.sx")
(load "lib/content/table.sx")
(load "lib/content/data.sx")
diff --git a/lib/content/scoreboard.json b/lib/content/scoreboard.json
index 6a90f654..1da17166 100644
--- a/lib/content/scoreboard.json
+++ b/lib/content/scoreboard.json
@@ -11,6 +11,7 @@
"text": {"pass": 20, "fail": 0},
"section": {"pass": 25, "fail": 0},
"compose": {"pass": 17, "fail": 0},
+ "tree-edit": {"pass": 17, "fail": 0},
"stats": {"pass": 17, "fail": 0},
"table": {"pass": 15, "fail": 0},
"data": {"pass": 21, "fail": 0},
@@ -25,7 +26,7 @@
"md-doc": {"pass": 12, "fail": 0},
"fed": {"pass": 20, "fail": 0}
},
- "total_pass": 534,
+ "total_pass": 551,
"total_fail": 0,
- "total": 534
+ "total": 551
}
diff --git a/lib/content/scoreboard.md b/lib/content/scoreboard.md
index 9497efba..1eefa8f5 100644
--- a/lib/content/scoreboard.md
+++ b/lib/content/scoreboard.md
@@ -15,6 +15,7 @@ _Generated by `lib/content/conformance.sh`_
| text | 20 | 0 | 20 |
| section | 25 | 0 | 25 |
| compose | 17 | 0 | 17 |
+| tree-edit | 17 | 0 | 17 |
| stats | 17 | 0 | 17 |
| table | 15 | 0 | 15 |
| data | 21 | 0 | 21 |
@@ -28,4 +29,4 @@ _Generated by `lib/content/conformance.sh`_
| md-import | 38 | 0 | 38 |
| md-doc | 12 | 0 | 12 |
| fed | 20 | 0 | 20 |
-| **Total** | **534** | **0** | **534** |
+| **Total** | **551** | **0** | **551** |
diff --git a/lib/content/tests/tree-edit.sx b/lib/content/tests/tree-edit.sx
new file mode 100644
index 00000000..43c41911
--- /dev/null
+++ b/lib/content/tests/tree-edit.sx
@@ -0,0 +1,91 @@
+;; Extension — deep tree editing (update/delete/insert into nested sections).
+
+(st-bootstrap-classes!)
+(content/bootstrap!)
+(content-bootstrap-section!)
+
+;; doc: top / sec[ a, inner[ b ] ]
+(define
+ d
+ (doc-append
+ (doc-append (doc-empty "d") (mk-text "top" "T"))
+ (mk-section
+ "sec"
+ (list
+ (mk-text "a" "A")
+ (mk-section "inner" (list (mk-text "b" "B")))))))
+
+;; ── deep-update a nested block ──
+(define d1 (doc-deep-update d "b" "text" "Edited"))
+(content-test
+ "deep-update nested"
+ (str (blk-send (doc-deep-find d1 "b") "text"))
+ "Edited")
+(content-test
+ "deep-update immutable"
+ (str (blk-send (doc-deep-find d "b") "text"))
+ "B")
+(content-test
+ "deep-update top-level"
+ (str
+ (blk-send
+ (doc-deep-find (doc-deep-update d "top" "text" "X") "top")
+ "text"))
+ "X")
+(content-test
+ "deep-update mid-section"
+ (str
+ (blk-send (doc-deep-find (doc-deep-update d "a" "text" "AA") "a") "text"))
+ "AA")
+(content-test
+ "deep-update preserves tree"
+ (doc-tree-ids d1)
+ (doc-tree-ids d))
+
+;; ── deep-replace ──
+(define d2 (doc-deep-replace d "b" (mk-heading "b" 3 "H")))
+(content-test
+ "deep-replace type"
+ (blk-type (doc-deep-find d2 "b"))
+ "heading")
+(content-test
+ "deep-replace render"
+ (asHTML d2)
+ "
T
")
+
+;; ── deep-delete ──
+(define d3 (doc-deep-delete d "b"))
+(content-test "deep-delete removes nested" (doc-deep-find d3 "b") nil)
+(content-test
+ "deep-delete tree-ids"
+ (doc-tree-ids d3)
+ (list "top" "sec" "a" "inner"))
+(content-test "deep-delete immutable" (doc-tree-count d) 5)
+(content-test
+ "deep-delete mid-section"
+ (doc-tree-ids (doc-deep-delete d "a"))
+ (list "top" "sec" "inner" "b"))
+(content-test
+ "deep-delete top-level"
+ (doc-tree-ids (doc-deep-delete d "top"))
+ (list "sec" "a" "inner" "b"))
+
+;; ── deep-insert-into a nested section ──
+(define d4 (doc-deep-insert-into d "inner" (mk-text "c" "C")))
+(content-test
+ "insert-into nested"
+ (doc-tree-ids d4)
+ (list "top" "sec" "a" "inner" "b" "c"))
+(content-test
+ "insert-into found"
+ (str (blk-send (doc-deep-find d4 "c") "text"))
+ "C")
+(content-test
+ "insert-into outer section"
+ (doc-tree-ids (doc-deep-insert-into d "sec" (mk-divider "dv")))
+ (list "top" "sec" "a" "inner" "b" "dv"))
+(content-test "insert-into immutable" (doc-tree-count d) 5)
+(content-test
+ "insert-into render"
+ (asHTML d4)
+ "T
")
diff --git a/lib/content/tree-edit.sx b/lib/content/tree-edit.sx
new file mode 100644
index 00000000..7f424862
--- /dev/null
+++ b/lib/content/tree-edit.sx
@@ -0,0 +1,96 @@
+;; content-on-sx — deep tree editing.
+;;
+;; Mutate blocks anywhere in the nested tree (descending into CtSection children),
+;; complementing the top-level doc ops and the deep-find read path. All return
+;; new documents (immutable).
+;;
+;; Requires (loaded by harness): doc.sx, section.sx (section? / section-children /
+;; section-with-children / section-append).
+
+;; map f over every block in the tree, replacing the one whose id matches.
+(define
+ block-tree-update
+ (fn
+ (blocks id f)
+ (map
+ (fn
+ (b)
+ (if
+ (= (blk-id b) id)
+ (f b)
+ (if
+ (section? b)
+ (section-with-children
+ b
+ (block-tree-update (section-children b) id f))
+ b)))
+ blocks)))
+
+;; remove the block with id from anywhere in the tree.
+(define
+ block-tree-delete
+ (fn
+ (blocks id)
+ (map
+ (fn
+ (b)
+ (if
+ (section? b)
+ (section-with-children
+ b
+ (block-tree-delete (section-children b) id))
+ b))
+ (filter (fn (b) (if (= (blk-id b) id) false true)) blocks))))
+
+;; append a block into the children of the section with section-id.
+(define
+ block-tree-insert-into
+ (fn
+ (blocks section-id block)
+ (map
+ (fn
+ (b)
+ (if
+ (section? b)
+ (if
+ (= (blk-id b) section-id)
+ (section-append b block)
+ (section-with-children
+ b
+ (block-tree-insert-into (section-children b) section-id block)))
+ b))
+ blocks)))
+
+;; ── document-level deep ops ──
+(define
+ doc-deep-update
+ (fn
+ (doc id field value)
+ (doc-with-blocks
+ doc
+ (block-tree-update
+ (doc-blocks doc)
+ id
+ (fn (b) (blk-set b field value))))))
+
+(define
+ doc-deep-replace
+ (fn
+ (doc id newblock)
+ (doc-with-blocks
+ doc
+ (block-tree-update (doc-blocks doc) id (fn (b) newblock)))))
+
+(define
+ doc-deep-delete
+ (fn
+ (doc id)
+ (doc-with-blocks doc (block-tree-delete (doc-blocks doc) id))))
+
+(define
+ doc-deep-insert-into
+ (fn
+ (doc section-id block)
+ (doc-with-blocks
+ doc
+ (block-tree-insert-into (doc-blocks doc) section-id block))))
diff --git a/plans/content-on-sx.md b/plans/content-on-sx.md
index b11f284b..4dbca23f 100644
--- a/plans/content-on-sx.md
+++ b/plans/content-on-sx.md
@@ -19,7 +19,7 @@ injected adapter, not core.
## Status (rolling)
-`bash lib/content/conformance.sh` → **534/534** (Phases 1–4 COMPLETE + extensions: HTML/SX escaping, Markdown render + import/export incl. tables & frontmatter (full round-trip), CRDT replication, tree-aware validation, snapshot cache, doc metadata, plain-text render, nested block trees, doc stats, table block, HTML page wrapper + SEO page, doc composition, portable data + wire serialization)
+`bash lib/content/conformance.sh` → **551/551** (Phases 1–4 COMPLETE + extensions: HTML/SX escaping, Markdown render + import/export incl. tables & frontmatter (full round-trip), CRDT replication, tree-aware validation, snapshot cache, doc metadata, plain-text render, nested block trees + deep tree editing, doc stats, table block, HTML page wrapper + SEO page, doc composition, portable data + wire serialization)
## Ground rules
@@ -92,11 +92,17 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─
- [x] HTML page wrapper (`page.sx`: content/page, escaped title from metadata)
- [x] SEO page (`page-full.sx`: content/page-full, lang + meta description from excerpt)
- [x] document composition (`compose.sx`: concat/prepend/concat-all/wrap-section)
+- [x] deep tree editing (`tree-edit.sx`: doc-deep-update/replace/delete/insert-into)
- [x] portable data serialization (`data.sx`: content/to-data + from-data, round-trips tree)
- [x] wire serialization (`wire.sx`: content/to-wire + from-wire, SX-text on the wire)
## Progress log
+- 2026-06-07 — Extension: deep tree editing (`tree-edit.sx`). `doc-deep-update`
+ / `doc-deep-replace` / `doc-deep-delete` / `doc-deep-insert-into` mutate blocks
+ anywhere in the nested tree (descending into CtSection children), completing
+ tree mutation to match the deep-find read path; all immutable. 17 tests; suite
+ 551/551.
- 2026-06-07 — Extension: on-the-wire serialization (`wire.sx`).
`content/to-wire` serialises a document to a transmittable SX-text string (data
form + SX serializer); `content/from-wire` parses it back into a live document.