From c093fdcb5406001c863f7a61cdb7ce1aa12aae39 Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 7 Jun 2026 03:35:28 +0000 Subject: [PATCH] content: id remapping / clone (clone.sx) + 10 tests (561/561) Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/content/clone.sx | 34 +++++++++++++++++++++++ lib/content/conformance.sh | 3 +- lib/content/scoreboard.json | 5 ++-- lib/content/scoreboard.md | 3 +- lib/content/tests/clone.sx | 55 +++++++++++++++++++++++++++++++++++++ plans/content-on-sx.md | 9 +++++- 6 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 lib/content/clone.sx create mode 100644 lib/content/tests/clone.sx diff --git a/lib/content/clone.sx b/lib/content/clone.sx new file mode 100644 index 00000000..3da5a293 --- /dev/null +++ b/lib/content/clone.sx @@ -0,0 +1,34 @@ +;; content-on-sx — block id remapping / clone. +;; +;; Deep-rewrite every block id in the tree (descending into sections) by applying +;; a function. Enables collision-free composition: prefix one document's ids +;; before concatenating it with another. Immutable; content is unchanged, only +;; ids. +;; +;; Requires (loaded by harness): doc.sx, section.sx (section? / +;; section-children / section-with-children). + +(define + block-remap-id + (fn + (b f) + (let + ((nb (blk-set b "id" (f (blk-id b))))) + (if + (section? nb) + (section-with-children + nb + (map (fn (c) (block-remap-id c f)) (section-children nb))) + nb)))) + +(define + content/remap-ids + (fn + (doc f) + (doc-with-blocks + doc + (map (fn (b) (block-remap-id b f)) (doc-blocks doc))))) + +(define + content/prefix-ids + (fn (doc prefix) (content/remap-ids doc (fn (id) (str prefix id))))) diff --git a/lib/content/conformance.sh b/lib/content/conformance.sh index 11a0a257..2f233e49 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 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 clone 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" @@ -47,6 +47,7 @@ run_suite() { (load "lib/content/section.sx") (load "lib/content/compose.sx") (load "lib/content/tree-edit.sx") +(load "lib/content/clone.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 1da17166..b714d62e 100644 --- a/lib/content/scoreboard.json +++ b/lib/content/scoreboard.json @@ -12,6 +12,7 @@ "section": {"pass": 25, "fail": 0}, "compose": {"pass": 17, "fail": 0}, "tree-edit": {"pass": 17, "fail": 0}, + "clone": {"pass": 10, "fail": 0}, "stats": {"pass": 17, "fail": 0}, "table": {"pass": 15, "fail": 0}, "data": {"pass": 21, "fail": 0}, @@ -26,7 +27,7 @@ "md-doc": {"pass": 12, "fail": 0}, "fed": {"pass": 20, "fail": 0} }, - "total_pass": 551, + "total_pass": 561, "total_fail": 0, - "total": 551 + "total": 561 } diff --git a/lib/content/scoreboard.md b/lib/content/scoreboard.md index 1eefa8f5..4499a6ac 100644 --- a/lib/content/scoreboard.md +++ b/lib/content/scoreboard.md @@ -16,6 +16,7 @@ _Generated by `lib/content/conformance.sh`_ | section | 25 | 0 | 25 | | compose | 17 | 0 | 17 | | tree-edit | 17 | 0 | 17 | +| clone | 10 | 0 | 10 | | stats | 17 | 0 | 17 | | table | 15 | 0 | 15 | | data | 21 | 0 | 21 | @@ -29,4 +30,4 @@ _Generated by `lib/content/conformance.sh`_ | md-import | 38 | 0 | 38 | | md-doc | 12 | 0 | 12 | | fed | 20 | 0 | 20 | -| **Total** | **551** | **0** | **551** | +| **Total** | **561** | **0** | **561** | diff --git a/lib/content/tests/clone.sx b/lib/content/tests/clone.sx new file mode 100644 index 00000000..e5dcb292 --- /dev/null +++ b/lib/content/tests/clone.sx @@ -0,0 +1,55 @@ +;; Extension — block id remapping / clone. + +(st-bootstrap-classes!) +(content/bootstrap!) +(content-bootstrap-section!) + +(define + d + (doc-append + (doc-append (doc-empty "d") (mk-heading "h" 1 "Title")) + (mk-section "s" (list (mk-text "a" "A") (mk-text "b" "B"))))) + +;; ── prefix-ids rewrites every id in the tree ── +(define p (content/prefix-ids d "x-")) +(content-test "prefix top-level ids" (doc-ids p) (list "x-h" "x-s")) +(content-test + "prefix tree-ids" + (doc-tree-ids p) + (list "x-h" "x-s" "x-a" "x-b")) +(content-test "prefix immutable" (doc-tree-ids d) (list "h" "s" "a" "b")) +(content-test "prefix preserves content" (asHTML p) (asHTML d)) +(content-test + "prefix preserves nested content" + (str (blk-send (doc-deep-find p "x-a") "text")) + "A") + +;; ── custom remap fn ── +(define u (content/remap-ids d (fn (id) (str id "!")))) +(content-test "remap suffix" (doc-tree-ids u) (list "h!" "s!" "a!" "b!")) + +;; ── collision-free composition ── +(define + d2 + (doc-append (doc-empty "d2") (mk-heading "h" 2 "Other"))) +(define + combined + (content/concat + (content/prefix-ids d "left-") + (content/prefix-ids d2 "right-"))) +(content-test + "combined ids unique" + (doc-tree-ids combined) + (list "left-h" "left-s" "left-a" "left-b" "right-h")) +(content-test "combined validates" (content/valid? combined) true) +;; without prefixing, the shared id "h" collides +(content-test + "unprefixed collides" + (content/valid? (content/concat d d2)) + false) + +;; ── render of combined ── +(content-test + "combined render" + (asHTML combined) + "

Title

A

B

Other

") diff --git a/plans/content-on-sx.md b/plans/content-on-sx.md index 4dbca23f..0ab3a5eb 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` → **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) +`bash lib/content/conformance.sh` → **561/561** (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 + id-remap, portable data + wire serialization) ## Ground rules @@ -93,11 +93,18 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─ - [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] id remapping / clone (`clone.sx`: content/remap-ids + prefix-ids, collision-free compose) - [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: id remapping / clone (`clone.sx`). + `content/remap-ids` deep-rewrites every block id across the tree (sections + recurse) via a function; `content/prefix-ids` prefixes them. Enables + collision-free composition (prefix each doc before concat → validates clean, + where the unprefixed concat has duplicate ids). Content unchanged, only ids; + immutable. 10 tests; suite 561/561. - 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