content: tree-aware validation (descends into sections) + 6 tests (416/416)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 30s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 30s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@
|
|||||||
"markdown": {"pass": 20, "fail": 0},
|
"markdown": {"pass": 20, "fail": 0},
|
||||||
"text": {"pass": 20, "fail": 0},
|
"text": {"pass": 20, "fail": 0},
|
||||||
"section": {"pass": 25, "fail": 0},
|
"section": {"pass": 25, "fail": 0},
|
||||||
"validate": {"pass": 17, "fail": 0},
|
"validate": {"pass": 23, "fail": 0},
|
||||||
"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},
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"md-import": {"pass": 24, "fail": 0},
|
"md-import": {"pass": 24, "fail": 0},
|
||||||
"fed": {"pass": 20, "fail": 0}
|
"fed": {"pass": 20, "fail": 0}
|
||||||
},
|
},
|
||||||
"total_pass": 410,
|
"total_pass": 416,
|
||||||
"total_fail": 0,
|
"total_fail": 0,
|
||||||
"total": 410
|
"total": 416
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ _Generated by `lib/content/conformance.sh`_
|
|||||||
| markdown | 20 | 0 | 20 |
|
| markdown | 20 | 0 | 20 |
|
||||||
| text | 20 | 0 | 20 |
|
| text | 20 | 0 | 20 |
|
||||||
| section | 25 | 0 | 25 |
|
| section | 25 | 0 | 25 |
|
||||||
| validate | 17 | 0 | 17 |
|
| validate | 23 | 0 | 23 |
|
||||||
| store | 29 | 0 | 29 |
|
| store | 29 | 0 | 29 |
|
||||||
| snapshot | 20 | 0 | 20 |
|
| snapshot | 20 | 0 | 20 |
|
||||||
| crdt | 34 | 0 | 34 |
|
| crdt | 34 | 0 | 34 |
|
||||||
@@ -20,4 +20,4 @@ _Generated by `lib/content/conformance.sh`_
|
|||||||
| sync | 14 | 0 | 14 |
|
| sync | 14 | 0 | 14 |
|
||||||
| md-import | 24 | 0 | 24 |
|
| md-import | 24 | 0 | 24 |
|
||||||
| fed | 20 | 0 | 20 |
|
| fed | 20 | 0 | 20 |
|
||||||
| **Total** | **410** | **0** | **410** |
|
| **Total** | **416** | **0** | **416** |
|
||||||
|
|||||||
@@ -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!)
|
(st-bootstrap-classes!)
|
||||||
(content-bootstrap-blocks!)
|
(content-bootstrap-blocks!)
|
||||||
(content-bootstrap-doc!)
|
(content-bootstrap-doc!)
|
||||||
|
(content-bootstrap-section!)
|
||||||
|
|
||||||
;; ── a fully valid document ──
|
;; ── a fully valid document ──
|
||||||
(define
|
(define
|
||||||
@@ -114,3 +116,51 @@
|
|||||||
(mk-heading "hh" 2 "H"))
|
(mk-heading "hh" 2 "H"))
|
||||||
(mk-text "tt" "T")))
|
(mk-text "tt" "T")))
|
||||||
(content-test "all well-formed types valid" (content/valid? allgood) true)
|
(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)
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
;; content-on-sx — document integrity validation.
|
;; content-on-sx — document integrity validation.
|
||||||
;;
|
;;
|
||||||
;; Guards imports, edits and federated input: checks each block's id and the
|
;; Guards imports, edits and federated input: walks the whole block TREE (into
|
||||||
;; required fields/types for its kind, plus document-level duplicate ids. Returns
|
;; nested sections) checking each block's id and required fields/types, plus
|
||||||
;; a list of issue dicts {:id :kind :detail}; an empty list means valid. Dispatch
|
;; tree-wide duplicate ids. Returns issue dicts {:id :kind :detail}; empty = ok.
|
||||||
;; on block type is a validation-boundary concern, not core behaviour.
|
;; 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.
|
;; Requires (loaded by harness): block.sx, doc.sx.
|
||||||
|
|
||||||
@@ -35,6 +36,30 @@
|
|||||||
|
|
||||||
(define ct-uniq (fn (xs) (ct-uniq-loop xs (list))))
|
(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 ──
|
;; ── id checks ──
|
||||||
(define
|
(define
|
||||||
content/-id-issues
|
content/-id-issues
|
||||||
@@ -120,32 +145,36 @@
|
|||||||
id
|
id
|
||||||
(list? (blk-get b "items"))
|
(list? (blk-get b "items"))
|
||||||
"list items must be a list")))
|
"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))))))))
|
(else (list (ct-issue id "type" (str "unknown block type: " t))))))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
content/-block-issues
|
content/-block-issues
|
||||||
(fn (b) (append (content/-id-issues b) (content/-field-issues b))))
|
(fn (b) (append (content/-id-issues b) (content/-field-issues b))))
|
||||||
|
|
||||||
;; ── document-level: duplicate ids ──
|
;; ── duplicate ids across the whole tree ──
|
||||||
(define
|
(define
|
||||||
content/-dup-issues
|
content/-dup-issues
|
||||||
(fn
|
(fn
|
||||||
(doc)
|
(ids)
|
||||||
(let
|
(map
|
||||||
((ids (doc-ids doc)))
|
(fn (id) (ct-issue id "duplicate" (str "duplicate block id: " id)))
|
||||||
(map
|
(ct-uniq (filter (fn (id) (> (ct-count-in id ids) 1)) ids)))))
|
||||||
(fn (id) (ct-issue id "duplicate" (str "duplicate block id: " id)))
|
|
||||||
(ct-uniq
|
|
||||||
(filter (fn (id) (> (ct-count-in id ids) 1)) ids))))))
|
|
||||||
|
|
||||||
;; ── public ──
|
;; ── public ──
|
||||||
(define
|
(define
|
||||||
content/validate
|
content/validate
|
||||||
(fn
|
(fn
|
||||||
(doc)
|
(doc)
|
||||||
(append
|
(let
|
||||||
(content/-dup-issues doc)
|
((all (ct-tree-blocks (doc-blocks doc))))
|
||||||
(ct-flatmap content/-block-issues (doc-blocks doc)))))
|
(append
|
||||||
|
(content/-dup-issues (map (fn (b) (blk-id b)) all))
|
||||||
|
(ct-flatmap content/-block-issues all)))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
content/valid?
|
content/valid?
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ injected adapter, not core.
|
|||||||
|
|
||||||
## Status (rolling)
|
## 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
|
## 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] asSx wire string-escaping (`String>>sxEscaped`: \ and " in SX literals)
|
||||||
- [x] Markdown render mode (`asMarkdown:` / `content/render doc "md"`)
|
- [x] Markdown render mode (`asMarkdown:` / `content/render doc "md"`)
|
||||||
- [x] durable CRDT replication (`crdt-store.sx`: ops on persist, replay + converge)
|
- [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] Markdown import adapter (`md-import.sx`: text → blocks, round-trips export)
|
||||||
- [x] snapshot cache over replay (`snapshot.sx`: cache-not-primary, transparent)
|
- [x] snapshot cache over replay (`snapshot.sx`: cache-not-primary, transparent)
|
||||||
- [x] document metadata (`meta.sx`: title/slug/tags + Ghost title plumbing)
|
- [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
|
## 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
|
- 2026-06-07 — Extension: nested block trees (`section.sx`). `CtSection` is a
|
||||||
block whose `children` ivar is a list of blocks (incl. nested sections →
|
block whose `children` ivar is a list of blocks (incl. nested sections →
|
||||||
arbitrary depth), turning the flat document into the ordered TREE from the
|
arbitrary depth), turning the flat document into the ordered TREE from the
|
||||||
|
|||||||
Reference in New Issue
Block a user