From 9722e97e0a9580c7dc07d7aa2979bcef4f19f452 Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 7 Jun 2026 00:42:49 +0000 Subject: [PATCH] content: trust-gated federation + conflict tests (Phase 4 complete, roadmap done, 230/230) Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/content/conformance.sh | 3 +- lib/content/fed.sx | 68 +++++++++++++++++ lib/content/scoreboard.json | 7 +- lib/content/scoreboard.md | 3 +- lib/content/tests/fed.sx | 148 ++++++++++++++++++++++++++++++++++++ plans/content-on-sx.md | 14 +++- 6 files changed, 235 insertions(+), 8 deletions(-) create mode 100644 lib/content/fed.sx create mode 100644 lib/content/tests/fed.sx diff --git a/lib/content/conformance.sh b/lib/content/conformance.sh index ac8399c9..249955da 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 store crdt sync) +SUITES=(block doc render api store crdt sync fed) OUT_JSON="lib/content/scoreboard.json" OUT_MD="lib/content/scoreboard.md" @@ -45,6 +45,7 @@ run_suite() { (load "lib/content/store.sx") (load "lib/content/crdt.sx") (load "lib/content/sync.sx") +(load "lib/content/fed.sx") (epoch 2) (eval "(define content-test-pass 0)") (eval "(define content-test-fail 0)") diff --git a/lib/content/fed.sx b/lib/content/fed.sx new file mode 100644 index 00000000..29df1ed9 --- /dev/null +++ b/lib/content/fed.sx @@ -0,0 +1,68 @@ +;; content-on-sx — federated documents: trust-gated peer-authored ops. +;; +;; A peer-authored op carries provenance (:author, and a :sig stub). We never +;; auto-accept: a peer op is applied only if it passes a trust gate. The gate is +;; a predicate (fn op -> bool) so acl-on-sx can inject real trust facts later; +;; the convenience form takes an explicit trusted-actor list (the stub). +;; +;; Accepted ops flow through the CvRDT merge (Phase 3), so concurrent local and +;; external edits reconcile deterministically (same-field LWW, order-independent). +;; +;; Requires (loaded by harness): crdt.sx (and its deps). + +;; tag an op with provenance +(define content/authored (fn (op author) (assoc op :author author))) + +(define + content/signed + (fn (op author sig) (assoc (assoc op :author author) :sig sig))) + +;; explicit trust stub: membership in a trusted-actor list +(define content/trusted? (fn (trust author) (crdt-member? author trust))) + +;; general form: accept? is a predicate (fn op -> bool). Applies accepted ops +;; through the CRDT; quarantines the rest. Returns +;; {:state :accepted (ops) :rejected (ops)}. +(define + content/-merge-peer-loop + (fn + (state accept? ops accepted rejected) + (if + (= (len ops) 0) + {:state state :accepted (reverse accepted) :rejected (reverse rejected)} + (let + ((op (first ops))) + (if + (accept? op) + (content/-merge-peer-loop + (crdt-apply state op) + accept? + (rest ops) + (cons op accepted) + rejected) + (content/-merge-peer-loop + state + accept? + (rest ops) + accepted + (cons op rejected))))))) + +(define + content/merge-peer-with + (fn + (state accept? ops) + (content/-merge-peer-loop state accept? ops (list) (list)))) + +;; convenience: trust = list of trusted actor ids +(define + content/merge-peer + (fn + (state trust ops) + (content/merge-peer-with + state + (fn (op) (content/trusted? trust (get op :author))) + ops))) + +(define content/accepted (fn (res) (get res :accepted))) +(define content/rejected (fn (res) (get res :rejected))) +(define content/peer-state (fn (res) (get res :state))) diff --git a/lib/content/scoreboard.json b/lib/content/scoreboard.json index 3c580915..fcd1eda0 100644 --- a/lib/content/scoreboard.json +++ b/lib/content/scoreboard.json @@ -6,9 +6,10 @@ "api": {"pass": 26, "fail": 0}, "store": {"pass": 29, "fail": 0}, "crdt": {"pass": 34, "fail": 0}, - "sync": {"pass": 14, "fail": 0} + "sync": {"pass": 14, "fail": 0}, + "fed": {"pass": 20, "fail": 0} }, - "total_pass": 210, + "total_pass": 230, "total_fail": 0, - "total": 210 + "total": 230 } diff --git a/lib/content/scoreboard.md b/lib/content/scoreboard.md index 7fb590aa..a84b6913 100644 --- a/lib/content/scoreboard.md +++ b/lib/content/scoreboard.md @@ -11,4 +11,5 @@ _Generated by `lib/content/conformance.sh`_ | store | 29 | 0 | 29 | | crdt | 34 | 0 | 34 | | sync | 14 | 0 | 14 | -| **Total** | **210** | **0** | **210** | +| fed | 20 | 0 | 20 | +| **Total** | **230** | **0** | **230** | diff --git a/lib/content/tests/fed.sx b/lib/content/tests/fed.sx new file mode 100644 index 00000000..6f651528 --- /dev/null +++ b/lib/content/tests/fed.sx @@ -0,0 +1,148 @@ +;; Phase 4 — federated documents: trust-gated peer ops + concurrent-external- +;; edit conflict resolution via the CRDT. + +(st-bootstrap-classes!) +(content-bootstrap-blocks!) +(content-bootstrap-doc!) +(content-bootstrap-render!) + +(define same? (fn (a b) (= (get a :elements) (get b :elements)))) + +;; base shared document, then a local edit +(define + base + (crdt-insert + (crdt-insert + (crdt-empty) + "h" + "heading" + (crdt-pos 1 0) + (list (list "level" 1) (list "text" "T")) + 1 + 0) + "p" + "text" + (crdt-pos 2 0) + (list (list "text" "Body")) + 1 + 0)) +(define local (crdt-update base "p" "text" "local" 5 1)) + +;; ── provenance ── +(content-test + "authored tags author" + (get (content/authored (crdt-op-delete "h") "ed") :author) + "ed") +(content-test + "signed tags sig" + (get (content/signed (crdt-op-delete "h") "ed" "sig1") :sig) + "sig1") +(content-test "trusted? yes" (content/trusted? (list "ed" "al") "ed") true) +(content-test "trusted? no" (content/trusted? (list "ed") "mal") false) + +;; peer ops: ed is trusted, mal is not +(define + peer-ops + (list + (content/authored + (crdt-op-update "p" "text" "peer-ed" 7 2) + "ed") + (content/authored + (crdt-op-insert + "x" + "text" + (crdt-pos 3 0) + (list (list "text" "X")) + 8 + 2) + "ed") + (content/authored (crdt-op-delete "h") "mal"))) + +(define res (content/merge-peer local (list "ed") peer-ops)) + +;; ── trust gate: only ed's ops applied ── +(content-test "accepted count" (len (content/accepted res)) 2) +(content-test "rejected count" (len (content/rejected res)) 1) +(content-test + "rejected is mal's" + (get (first (content/rejected res)) :author) + "mal") + +;; ── resulting document ── +(define rdoc (crdt-materialize "d" (content/peer-state res))) +(content-test "untrusted delete blocked: h survives" (doc-has? rdoc "h") true) +(content-test "trusted insert applied: x present" (doc-has? rdoc "x") true) +(content-test "result order" (doc-ids rdoc) (list "h" "p" "x")) +(content-test + "trusted edit wins (ts7 > ts5)" + (str (blk-send (doc-find rdoc "p") "text")) + "peer-ed") + +;; ── order-independence of accepted peer ops ── +(define res-rev (content/merge-peer local (list "ed") (reverse peer-ops))) +(content-test + "peer merge order-independent" + (same? (content/peer-state res) (content/peer-state res-rev)) + true) + +;; ── trust = nobody → nothing applied, state unchanged ── +(define res0 (content/merge-peer local (list) peer-ops)) +(content-test + "no trust accepts none" + (len (content/accepted res0)) + 0) +(content-test + "no trust rejects all" + (len (content/rejected res0)) + 3) +(content-test + "no trust state unchanged" + (same? (content/peer-state res0) local) + true) + +;; ── pluggable predicate gate (acl-on-sx hook) ── +(define + res-pred + (content/merge-peer-with + local + (fn (op) (= (get op :author) "ed")) + peer-ops)) +(content-test + "predicate gate == list gate" + (same? (content/peer-state res-pred) (content/peer-state res)) + true) + +;; ── conflict on concurrent external edit: local vs external, same field ── +;; external (peer) state edits p concurrently with a later ts; CRDT reconciles. +(define + external + (crdt-update base "p" "text" "external" 9 2)) +(content-test + "conflict LWW deterministic" + (str + (blk-send + (doc-find (crdt-materialize "d" (crdt-merge local external)) "p") + "text")) + "external") +(content-test + "conflict merge commutes" + (same? (crdt-merge local external) (crdt-merge external local)) + true) +(content-test + "conflict merge idempotent" + (same? + (crdt-merge (crdt-merge local external) external) + (crdt-merge local external)) + true) + +;; concurrent external edit with LOWER ts loses to local +(define + external-old + (crdt-update base "p" "text" "stale" 3 2)) +(content-test + "older external loses to local" + (str + (blk-send + (doc-find (crdt-materialize "d" (crdt-merge local external-old)) "p") + "text")) + "local") diff --git a/plans/content-on-sx.md b/plans/content-on-sx.md index d203bbc4..5be0b222 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` → **210/210** (Phases 1–3 complete + Phase 4 Ghost adapter) +`bash lib/content/conformance.sh` → **230/230** (Phases 1–4 COMPLETE: blocks, doc, render, api, persist op log, CRDT merge, Ghost sync, federation) ## Ground rules @@ -72,11 +72,19 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─ ## Phase 4 — External sync + federation - [x] Ghost/CMS sync via injected adapter (import/export) -- [ ] federated documents (peer-authored blocks) — trust-gated stub -- [~] tests: round-trip import/export (done), conflict on concurrent external edit (pending) +- [x] federated documents (peer-authored blocks) — trust-gated stub +- [x] tests: round-trip import/export, conflict on concurrent external edit ## Progress log +- 2026-06-07 — Phase 4 `fed.sx` (**Phase 4 COMPLETE — roadmap done**): + trust-gated federation. Peer ops carry provenance (`:author`, `:sig` stub); + none are auto-accepted. The trust gate is a pluggable predicate (acl-on-sx + hook) with a trusted-actor-list convenience stub. `content/merge-peer[-with]` + applies only accepted ops through the CvRDT and quarantines the rest + (`{:state :accepted :rejected}`). Concurrent local/external edits reconcile + deterministically: same-field LWW by (ts,actor), commutative, idempotent; + untrusted ops never touch state. 20 tests; suite 230/230. - 2026-06-07 — Phase 4 `sync.sx` (cb1): external CMS sync via an injected adapter. Core defines the shape — `{:import :export}` — and delegates; `content/import` / `content/export` / `content/round-trip` know nothing about