diff --git a/lib/content/conformance.sh b/lib/content/conformance.sh
index 52eae428..762ec963 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 tree-edit move clone query toc anchor outline flatten transform normalize find-replace stats summary index table callout media data wire validate store snapshot crdt crdt-tree crdt-store sync md-import md-doc fed)
+SUITES=(block doc render api meta page page-full markdown text section compose tree-edit move clone query toc anchor outline flatten transform normalize find-replace stats summary index table callout media data wire validate store snapshot crdt crdt-tree crdt-blocks crdt-store sync md-import md-doc fed)
OUT_JSON="lib/content/scoreboard.json"
OUT_MD="lib/content/scoreboard.md"
diff --git a/lib/content/scoreboard.json b/lib/content/scoreboard.json
index bd3c1bc1..b8c4625b 100644
--- a/lib/content/scoreboard.json
+++ b/lib/content/scoreboard.json
@@ -35,13 +35,14 @@
"snapshot": {"pass": 20, "fail": 0},
"crdt": {"pass": 34, "fail": 0},
"crdt-tree": {"pass": 17, "fail": 0},
+ "crdt-blocks": {"pass": 7, "fail": 0},
"crdt-store": {"pass": 14, "fail": 0},
"sync": {"pass": 14, "fail": 0},
"md-import": {"pass": 38, "fail": 0},
"md-doc": {"pass": 12, "fail": 0},
"fed": {"pass": 20, "fail": 0}
},
- "total_pass": 731,
+ "total_pass": 738,
"total_fail": 0,
- "total": 731
+ "total": 738
}
diff --git a/lib/content/scoreboard.md b/lib/content/scoreboard.md
index 65aeabd4..9cdf12c2 100644
--- a/lib/content/scoreboard.md
+++ b/lib/content/scoreboard.md
@@ -39,9 +39,10 @@ _Generated by `lib/content/conformance.sh`_
| snapshot | 20 | 0 | 20 |
| crdt | 34 | 0 | 34 |
| crdt-tree | 17 | 0 | 17 |
+| 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** | **731** | **0** | **731** |
+| **Total** | **738** | **0** | **738** |
diff --git a/lib/content/tests/crdt-blocks.sx b/lib/content/tests/crdt-blocks.sx
new file mode 100644
index 00000000..7d4619ad
--- /dev/null
+++ b/lib/content/tests/crdt-blocks.sx
@@ -0,0 +1,136 @@
+;; Hardening — non-core block types (callout/table/media/section) survive the
+;; flat and tree CvRDT materialise paths (regression for the ct-class-for-type
+;; fix: these route through crdt-element->block -> mk-block).
+
+(st-bootstrap-classes!)
+(content-bootstrap-blocks!)
+(content-bootstrap-doc!)
+(content-bootstrap-render!)
+(content-bootstrap-section!)
+(content-bootstrap-callout!)
+(content-bootstrap-table!)
+(content-bootstrap-media!)
+
+;; ── flat CRDT: callout / table / media leaves ──
+(define
+ s
+ (crdt-apply-all
+ (crdt-empty)
+ (list
+ (crdt-op-insert
+ "co"
+ "callout"
+ (crdt-pos 1 0)
+ (list (list "kind" "note") (list "text" "hi"))
+ 1
+ 0)
+ (crdt-op-insert
+ "tb"
+ "table"
+ (crdt-pos 2 0)
+ (list (list "headers" (list "A")) (list "rows" (list (list "1"))))
+ 1
+ 0)
+ (crdt-op-insert
+ "vid"
+ "media"
+ (crdt-pos 3 0)
+ (list (list "kind" "video") (list "src" "/v.mp4"))
+ 1
+ 0))))
+(content-test
+ "flat crdt callout render"
+ (asHTML (crdt-materialize "d" s))
+ "
")
+(content-test "flat crdt order" (crdt-order s) (list "co" "tb" "vid"))
+
+;; ── flat CRDT: callout field via LWW update ──
+(define s2 (crdt-update s "co" "text" "edited" 5 1))
+(content-test
+ "flat crdt callout update"
+ (str (blk-send (doc-find (crdt-materialize "d" s2) "co") "text"))
+ "edited")
+
+;; ── tree CRDT: callout/table inside a section ──
+(define
+ t
+ (crdt-tree-apply-all
+ (crdt-empty)
+ (list
+ (crdt-tree-op-insert
+ "sec"
+ "section"
+ (crdt-pos 1 0)
+ ""
+ (list)
+ 1
+ 0)
+ (crdt-tree-op-insert
+ "co"
+ "callout"
+ (crdt-pos 1 0)
+ "sec"
+ (list (list "kind" "tip") (list "text" "T"))
+ 1
+ 0)
+ (crdt-tree-op-insert
+ "tb"
+ "table"
+ (crdt-pos 2 0)
+ "sec"
+ (list (list "headers" (list "H")) (list "rows" (list)))
+ 1
+ 0))))
+(content-test
+ "tree crdt nested blocks"
+ (doc-tree-ids (crdt-tree-materialize "d" t))
+ (list "sec" "co" "tb"))
+(content-test
+ "tree crdt nested render"
+ (asHTML (crdt-tree-materialize "d" t))
+ "")
+
+;; ── tree CRDT: concurrent callout inserts into a section converge ──
+(define
+ base
+ (crdt-tree-insert
+ (crdt-empty)
+ "sec"
+ "section"
+ (crdt-pos 1 0)
+ ""
+ (list)
+ 1
+ 0))
+(define
+ rA
+ (crdt-tree-insert
+ base
+ "x"
+ "callout"
+ (crdt-pos 5 1)
+ "sec"
+ (list (list "kind" "note") (list "text" "A"))
+ 2
+ 1))
+(define
+ rB
+ (crdt-tree-insert
+ base
+ "y"
+ "media"
+ (crdt-pos 5 2)
+ "sec"
+ (list (list "kind" "audio") (list "src" "/a.mp3"))
+ 2
+ 2))
+(content-test
+ "tree crdt mixed converge"
+ (=
+ (get (crdt-tree-merge rA rB) :elements)
+ (get (crdt-tree-merge rB rA) :elements))
+ true)
+(content-test
+ "tree crdt mixed ids"
+ (doc-tree-ids (crdt-tree-materialize "d" (crdt-tree-merge rA rB)))
+ (list "sec" "x" "y"))
diff --git a/plans/content-on-sx.md b/plans/content-on-sx.md
index d30126bc..69d5cb74 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` → **731/731** (Phases 1–4 COMPLETE + ~34 extensions: 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` → **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)
## Ground rules
@@ -113,6 +113,12 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─
## Progress log
+- 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),
+ the integration the ct-class-for-type fix repaired. Verified flat + tree,
+ including concurrent mixed-type inserts into a section converging. Suite
+ 738/738.
- 2026-06-07 — Hardening: fixed `ct-class-for-type` (block.sx) to map all block
tags (added section/table/callout/media). Latent bug: `content/from-data` and
CRDT materialise of callout/media blocks failed with "unknown block type" (they