diff --git a/lib/content/scoreboard.json b/lib/content/scoreboard.json index d59732a3..12236831 100644 --- a/lib/content/scoreboard.json +++ b/lib/content/scoreboard.json @@ -8,7 +8,7 @@ "markdown": {"pass": 20, "fail": 0}, "text": {"pass": 20, "fail": 0}, "section": {"pass": 25, "fail": 0}, - "validate": {"pass": 17, "fail": 0}, + "validate": {"pass": 23, "fail": 0}, "store": {"pass": 29, "fail": 0}, "snapshot": {"pass": 20, "fail": 0}, "crdt": {"pass": 34, "fail": 0}, @@ -17,7 +17,7 @@ "md-import": {"pass": 24, "fail": 0}, "fed": {"pass": 20, "fail": 0} }, - "total_pass": 410, + "total_pass": 416, "total_fail": 0, - "total": 410 + "total": 416 } diff --git a/lib/content/scoreboard.md b/lib/content/scoreboard.md index 9b570f2b..d311d047 100644 --- a/lib/content/scoreboard.md +++ b/lib/content/scoreboard.md @@ -12,7 +12,7 @@ _Generated by `lib/content/conformance.sh`_ | markdown | 20 | 0 | 20 | | text | 20 | 0 | 20 | | section | 25 | 0 | 25 | -| validate | 17 | 0 | 17 | +| validate | 23 | 0 | 23 | | store | 29 | 0 | 29 | | snapshot | 20 | 0 | 20 | | crdt | 34 | 0 | 34 | @@ -20,4 +20,4 @@ _Generated by `lib/content/conformance.sh`_ | sync | 14 | 0 | 14 | | md-import | 24 | 0 | 24 | | fed | 20 | 0 | 20 | -| **Total** | **410** | **0** | **410** | +| **Total** | **416** | **0** | **416** | diff --git a/lib/content/tests/validate.sx b/lib/content/tests/validate.sx index 0cd113be..d01f935c 100644 --- a/lib/content/tests/validate.sx +++ b/lib/content/tests/validate.sx @@ -1,8 +1,10 @@ -;; Extension — document integrity validation. +;; Extension — document integrity validation (tree-aware: descends into sections). +;; (Conformance loads section.sx before this suite.) (st-bootstrap-classes!) (content-bootstrap-blocks!) (content-bootstrap-doc!) +(content-bootstrap-section!) ;; ── a fully valid document ── (define @@ -114,3 +116,51 @@ (mk-heading "hh" 2 "H")) (mk-text "tt" "T"))) (content-test "all well-formed types valid" (content/valid? allgood) true) + +;; ── tree-aware: descends into sections ── +(define + nested + (doc-append + (doc-empty "d") + (mk-section + "s" + (list (mk-heading "nh" 1 "H") (mk-text "np" "ok"))))) +(content-test "valid nested section" (content/valid? nested) true) + +(define + nested-bad + (doc-append + (doc-empty "d") + (mk-section "s" (list (mk-heading "nh" "notnum" "H"))))) +(content-test + "nested bad field detected" + (content/issue-kinds nested-bad) + (list "field")) + +;; valid section block itself +(content-test + "section valid" + (content/valid? (doc-append (doc-empty "d") (mk-section "s" (list)))) + true) +(content-test + "section bad children" + (content/issue-kinds + (doc-append + (doc-empty "d") + (st-iv-set! (mk-section "s" (list)) "children" "nope"))) + (list "field")) + +;; duplicate id across a section boundary (top-level id == nested id) +(define + dup-tree + (doc-append + (doc-append (doc-empty "d") (mk-text "x" "top")) + (mk-section "s" (list (mk-text "x" "nested"))))) +(content-test + "tree-wide duplicate detected" + (len + (filter + (fn (i) (= (get i :kind) "duplicate")) + (content/validate dup-tree))) + 1) +(content-test "tree dup not valid" (content/valid? dup-tree) false) diff --git a/lib/content/validate.sx b/lib/content/validate.sx index 3a37cefc..537053b6 100644 --- a/lib/content/validate.sx +++ b/lib/content/validate.sx @@ -1,9 +1,10 @@ ;; content-on-sx — document integrity validation. ;; -;; Guards imports, edits and federated input: checks each block's id and the -;; required fields/types for its kind, plus document-level duplicate ids. Returns -;; a list of issue dicts {:id :kind :detail}; an empty list means valid. Dispatch -;; on block type is a validation-boundary concern, not core behaviour. +;; Guards imports, edits and federated input: walks the whole block TREE (into +;; nested sections) checking each block's id and required fields/types, plus +;; tree-wide duplicate ids. Returns issue dicts {:id :kind :detail}; empty = ok. +;; Tree detection is inline (class + st-iv-get) so this file needs no section.sx. +;; Dispatch on block type is a validation-boundary concern, not core behaviour. ;; ;; Requires (loaded by harness): block.sx, doc.sx. @@ -35,6 +36,30 @@ (define ct-uniq (fn (xs) (ct-uniq-loop xs (list)))) +;; ── tree flatten (descends into CtSection children; guards malformed children) ── +(define + ct-section-block? + (fn (b) (and (st-instance? b) (= (get b :class) "CtSection")))) +(define + ct-tree-blocks + (fn + (blocks) + (if + (= (len blocks) 0) + (list) + (let + ((b (first blocks))) + (append + (cons + b + (if + (ct-section-block? b) + (let + ((ch (st-iv-get b "children"))) + (if (list? ch) (ct-tree-blocks ch) (list))) + (list))) + (ct-tree-blocks (rest blocks))))))) + ;; ── id checks ── (define content/-id-issues @@ -120,32 +145,36 @@ id (list? (blk-get b "items")) "list items must be a list"))) + ((= t "section") + (ct-field-issue + id + (list? (blk-get b "children")) + "section children must be a list")) (else (list (ct-issue id "type" (str "unknown block type: " t)))))))) (define content/-block-issues (fn (b) (append (content/-id-issues b) (content/-field-issues b)))) -;; ── document-level: duplicate ids ── +;; ── duplicate ids across the whole tree ── (define content/-dup-issues (fn - (doc) - (let - ((ids (doc-ids doc))) - (map - (fn (id) (ct-issue id "duplicate" (str "duplicate block id: " id))) - (ct-uniq - (filter (fn (id) (> (ct-count-in id ids) 1)) ids)))))) + (ids) + (map + (fn (id) (ct-issue id "duplicate" (str "duplicate block id: " id))) + (ct-uniq (filter (fn (id) (> (ct-count-in id ids) 1)) ids))))) ;; ── public ── (define content/validate (fn (doc) - (append - (content/-dup-issues doc) - (ct-flatmap content/-block-issues (doc-blocks doc))))) + (let + ((all (ct-tree-blocks (doc-blocks doc)))) + (append + (content/-dup-issues (map (fn (b) (blk-id b)) all)) + (ct-flatmap content/-block-issues all))))) (define content/valid? diff --git a/plans/content-on-sx.md b/plans/content-on-sx.md index d25236d7..4e89ba25 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` → **410/410** (Phases 1–4 COMPLETE + 10 extensions: HTML/SX escaping, Markdown render+import, CRDT replication, validation, snapshot cache, doc metadata, plain-text render, nested block trees) +`bash lib/content/conformance.sh` → **416/416** (Phases 1–4 COMPLETE + 10 extensions: HTML/SX escaping, Markdown render+import, CRDT replication, tree-aware validation, snapshot cache, doc metadata, plain-text render, nested block trees) ## Ground rules @@ -80,7 +80,7 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─ - [x] asSx wire string-escaping (`String>>sxEscaped`: \ and " in SX literals) - [x] Markdown render mode (`asMarkdown:` / `content/render doc "md"`) - [x] durable CRDT replication (`crdt-store.sx`: ops on persist, replay + converge) -- [x] document validation (`validate.sx`: ids, per-type fields, duplicate ids) +- [x] document validation (`validate.sx`: ids, per-type fields, duplicate ids; tree-aware — descends into sections, tree-wide dup ids, section field check) - [x] Markdown import adapter (`md-import.sx`: text → blocks, round-trips export) - [x] snapshot cache over replay (`snapshot.sx`: cache-not-primary, transparent) - [x] document metadata (`meta.sx`: title/slug/tags + Ghost title plumbing) @@ -89,6 +89,12 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─ ## Progress log +- 2026-06-07 — Refinement: tree-aware validation. `validate.sx` now flattens the + whole block tree (descending into `CtSection` children, guarding malformed + non-list children) so field checks and duplicate-id detection cover nested + blocks and span section boundaries; added a `section` field-type case. Inline + tree detection (class + st-iv-get) keeps it free of a section.sx dependency. + +6 tests; suite 416/416. - 2026-06-07 — Extension: nested block trees (`section.sx`). `CtSection` is a block whose `children` ivar is a list of blocks (incl. nested sections → arbitrary depth), turning the flat document into the ordered TREE from the