From 4947d1f5aa808549b7705ddf842b484544a9c1a2 Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 7 Jun 2026 12:22:17 +0000 Subject: [PATCH] =?UTF-8?q?artdag:=20DAG=20wire=20serialization=20?= =?UTF-8?q?=E2=80=94=20portable=20record=20form=20+=20integrity=20+=2013?= =?UTF-8?q?=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit serialize.sx emits a topo-ordered (id op inputs params commutative) record list that survives write/read (string-keyed node dicts do not; empty inputs read back as nil and are normalized). wire->dag reconstructs a runnable dag by content-id; wire-verify recomputes ids to reject tampering. dag->string/string->dag for text transport. serialize 13/13, total 128/128. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/artdag/conformance.sh | 3 +- lib/artdag/scoreboard.json | 7 ++- lib/artdag/scoreboard.md | 3 +- lib/artdag/serialize.sx | 62 ++++++++++++++++++ lib/artdag/tests/serialize.sx | 115 ++++++++++++++++++++++++++++++++++ plans/artdag-on-sx.md | 13 +++- 6 files changed, 197 insertions(+), 6 deletions(-) create mode 100644 lib/artdag/serialize.sx create mode 100644 lib/artdag/tests/serialize.sx diff --git a/lib/artdag/conformance.sh b/lib/artdag/conformance.sh index ff32eaf2..050b6687 100755 --- a/lib/artdag/conformance.sh +++ b/lib/artdag/conformance.sh @@ -13,7 +13,7 @@ if [ ! -x "$SX_SERVER" ]; then exit 1 fi -SUITES=(dag analyze plan execute optimize fed cost) +SUITES=(dag analyze plan execute optimize fed cost serialize) OUT_JSON="lib/artdag/scoreboard.json" OUT_MD="lib/artdag/scoreboard.md" @@ -48,6 +48,7 @@ run_suite() { (load "lib/artdag/optimize.sx") (load "lib/artdag/federation.sx") (load "lib/artdag/cost.sx") +(load "lib/artdag/serialize.sx") (epoch 2) (eval "(define artdag-test-pass 0)") (eval "(define artdag-test-fail 0)") diff --git a/lib/artdag/scoreboard.json b/lib/artdag/scoreboard.json index e707a6d3..619b39e7 100644 --- a/lib/artdag/scoreboard.json +++ b/lib/artdag/scoreboard.json @@ -6,9 +6,10 @@ "execute": {"pass": 15, "fail": 0}, "optimize": {"pass": 18, "fail": 0}, "fed": {"pass": 15, "fail": 0}, - "cost": {"pass": 13, "fail": 0} + "cost": {"pass": 13, "fail": 0}, + "serialize": {"pass": 13, "fail": 0} }, - "total_pass": 115, + "total_pass": 128, "total_fail": 0, - "total": 115 + "total": 128 } diff --git a/lib/artdag/scoreboard.md b/lib/artdag/scoreboard.md index 6440f9d5..781cfcd3 100644 --- a/lib/artdag/scoreboard.md +++ b/lib/artdag/scoreboard.md @@ -11,4 +11,5 @@ _Generated by `lib/artdag/conformance.sh`_ | optimize | 18 | 0 | 18 | | fed | 15 | 0 | 15 | | cost | 13 | 0 | 13 | -| **Total** | **115** | **0** | **115** | +| serialize | 13 | 0 | 13 | +| **Total** | **128** | **0** | **128** | diff --git a/lib/artdag/serialize.sx b/lib/artdag/serialize.sx new file mode 100644 index 00000000..f8f337e9 --- /dev/null +++ b/lib/artdag/serialize.sx @@ -0,0 +1,62 @@ +; lib/artdag/serialize.sx — portable wire form for whole DAGs, so a peer can +; receive and run a graph it did not author. The form is a topo-ordered list of +; node records (id op inputs params commutative) — plain lists with keyword-keyed +; param dicts, which survive write/read (unlike string-keyed node dicts). The id +; is the content-id, so the form is self-verifying. Depends on dag.sx. + +(define + artdag/node->record + (fn + (dag id) + (let + ((n (artdag/dag-get dag id))) + (list + id + (artdag/node-op n) + (artdag/node-inputs n) + (artdag/node-params n) + (get n :commutative))))) + +; dag -> list of records, in topological order. +(define + artdag/dag->wire + (fn + (dag) + (map (fn (id) (artdag/node->record dag id)) (artdag/dag-order dag)))) + +; an empty input list reads back as nil; normalize it. +(define + artdag/-rec-inputs + (fn (rec) (let ((i (nth rec 2))) (if (nil? i) (list) i)))) + +(define artdag/-rec->node (fn (rec) {:inputs (artdag/-rec-inputs rec) :commutative (nth rec 4) :op (nth rec 1) :params (nth rec 3)})) + +; records -> dag. Local author names are not part of the wire form; the receiver +; works by content-id. :names is left empty. +(define + artdag/wire->dag + (fn + (records) + (reduce + (fn (dag rec) (let ((id (nth rec 0))) {:names (get dag :names) :order (concat (get dag :order) (list id)) :ok true :nodes (assoc (get dag :nodes) id (artdag/-rec->node rec))})) + {:names {} :order (list) :ok true :nodes {}} + records))) + +; integrity: each record's id must equal the content-id recomputed from its spec. +(define + artdag/wire-verify + (fn + (records) + (every? + (fn + (rec) + (= (nth rec 0) (artdag/content-id (artdag/-rec->node rec)))) + records))) + +; string transport. +(define + artdag/dag->string + (fn (dag) (write-to-string (artdag/dag->wire dag)))) +(define + artdag/string->dag + (fn (s) (artdag/wire->dag (read (open-input-string s))))) diff --git a/lib/artdag/tests/serialize.sx b/lib/artdag/tests/serialize.sx new file mode 100644 index 00000000..f788c329 --- /dev/null +++ b/lib/artdag/tests/serialize.sx @@ -0,0 +1,115 @@ +; portable wire form: dag <-> records <-> string, with content-id integrity. + +(define ser-RT (artdag/op-table-runner {:in (fn (p i) (get p :v)) :add (fn (p i) (+ (nth i 0) (nth i 1))) :inc (fn (p i) (+ 1 (first i)))})) + +(define + ser-D + (artdag/build + (list + (list "a" "in" (list) {:v 10}) + (list "b" "inc" (list "a") {}) + (list "c" "add" (list "a" "b") {} true)))) + +(define ser-cid (artdag/dag-id ser-D "c")) + +; ---- wire form ---- + +(artdag-test + "wire has one record per node" + (len (artdag/dag->wire ser-D)) + 3) + +(artdag-test + "wire records follow topological order" + (map (fn (rec) (nth rec 0)) (artdag/dag->wire ser-D)) + (artdag/dag-order ser-D)) + +(artdag-test + "wire record carries the content-id" + (nth (nth (artdag/dag->wire ser-D) 0) 0) + (artdag/dag-id ser-D "a")) + +; ---- reconstruction ---- + +(artdag-test + "wire->dag restores node count" + (artdag/node-count (artdag/wire->dag (artdag/dag->wire ser-D))) + 3) + +(artdag-test + "wire->dag restores order" + (artdag/dag-order (artdag/wire->dag (artdag/dag->wire ser-D))) + (artdag/dag-order ser-D)) + +(artdag-test + "reconstructed leaf inputs normalize to empty list" + (artdag/node-inputs + (artdag/dag-get + (artdag/wire->dag (artdag/dag->wire ser-D)) + (artdag/dag-id ser-D "a"))) + (list)) + +(artdag-test + "reconstructed node preserves inputs" + (artdag/node-inputs + (artdag/dag-get (artdag/wire->dag (artdag/dag->wire ser-D)) ser-cid)) + (artdag/node-inputs (artdag/dag-get ser-D ser-cid))) + +(artdag-test + "reconstructed node id matches recomputed content-id" + (artdag/content-id + (artdag/dag-get (artdag/wire->dag (artdag/dag->wire ser-D)) ser-cid)) + ser-cid) + +; ---- execution equivalence ---- + +(artdag-test + "reconstructed dag executes to same result" + (let + ((c1 (persist/open)) (c2 (persist/open))) + (= + (artdag/result-of (artdag/run ser-D ser-RT c1) ser-cid) + (artdag/result-of + (artdag/run (artdag/wire->dag (artdag/dag->wire ser-D)) ser-RT c2) + ser-cid))) + true) + +(artdag-test + "string round-trip executes to same result" + (let + ((cache (persist/open))) + (artdag/result-of + (artdag/run + (artdag/string->dag (artdag/dag->string ser-D)) + ser-RT + cache) + ser-cid)) + 21) + +; ---- integrity ---- + +(artdag-test + "wire-verify accepts a genuine wire form" + (artdag/wire-verify (artdag/dag->wire ser-D)) + true) + +(artdag-test + "wire-verify rejects a tampered id" + (artdag/wire-verify + (list (list "node:bogus" "in" (list) {:v 1} false))) + false) + +(artdag-test + "wire-verify rejects mutated params under a stale id" + (artdag/wire-verify + (map + (fn + (rec) + (list + (nth rec 0) + (nth rec 1) + (nth rec 2) + {:v 999} + (nth rec 4))) + (artdag/dag->wire ser-D))) + false) diff --git a/plans/artdag-on-sx.md b/plans/artdag-on-sx.md index af9ee3a7..fcfade9a 100644 --- a/plans/artdag-on-sx.md +++ b/plans/artdag-on-sx.md @@ -30,7 +30,7 @@ edges. ## Status (rolling) -`bash lib/artdag/conformance.sh` → **115/115** (7 suites: dag, analyze, plan, execute, optimize, fed, cost) +`bash lib/artdag/conformance.sh` → **128/128** (8 suites: dag, analyze, plan, execute, optimize, fed, cost, serialize) Base roadmap (Phases 1–6) COMPLETE. Now extending. @@ -138,6 +138,17 @@ lib/artdag/optimize.sx lib/artdag/federation.sx ## Progress log +- **Ext: DAG wire serialization** (serialize suite 13/13, total 128/128). + `lib/artdag/serialize.sx`: `dag->wire` emits a topo-ordered list of + `(id op inputs params commutative)` records — plain lists with keyword-keyed param + dicts, which survive `write-to-string`/`read` (string-keyed node dicts do NOT; and + `()` reads back as nil, so `wire->dag` normalizes empty inputs). `wire->dag` + reconstructs a runnable dag by content-id (author names dropped); executes + identically to the original. `wire-verify` recomputes each record's content-id and + rejects tampered ids or mutated params under a stale id (self-verifying transport). + `dag->string`/`string->dag` for text transport. Gotcha logged: `sx-parse` primitive + is unbound in the server env — use `(read (open-input-string s))`. + - **Ext: cost-based scheduling** (cost suite 13/13, total 115/115). `lib/artdag/cost.sx`: an injected `cost-fn (op params)` keeps media-op costs opaque (`const-cost`, `op-cost table`). `critical-path` = longest weighted path (finish-time