diff --git a/lib/artdag/conformance.sh b/lib/artdag/conformance.sh index 6027026e..f3928f68 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 serialize stats fault maude-optimize schedule) +SUITES=(dag analyze plan execute optimize fed cost serialize stats fault post maude-optimize schedule) OUT_JSON="lib/artdag/scoreboard.json" OUT_MD="lib/artdag/scoreboard.md" @@ -96,6 +96,7 @@ ${MK_LOADS} (load "lib/artdag/serialize.sx") (load "lib/artdag/stats.sx") (load "lib/artdag/fault.sx") +(load "lib/artdag/post.sx") (load "lib/artdag/api.sx") ${BRIDGE_LOAD} ${SCHED_LOAD} diff --git a/lib/artdag/post.sx b/lib/artdag/post.sx new file mode 100644 index 00000000..942c43b5 --- /dev/null +++ b/lib/artdag/post.sx @@ -0,0 +1,68 @@ +; lib/artdag/post.sx — project an artdag job to/from a feed "post object", so a job +; can ride as the :object of a feed activity ({:actor :verb :object :at :tags}) per the +; host loop. A post object is content-addressed and self-verifying: +; {:type "artdag/job" :id :wire wire>} +; The :id IS the post/object id (the stable structural digest = natural AP object id); +; the :wire is the self-describing, write/read-safe payload from serialize.sx whose +; records each carry their own content-id. The dag<->feed-activity wrapping (actor/verb/ +; at/tags) stays on the host/feed side; this file is only the job<->object projection. +; Depends on dag.sx + serialize.sx (and execute.sx for post-run). + +(define artdag/post-type "artdag/job") + +; a job = a dag + the output node (by author name) the post is "about". +(define artdag/job->post-object (fn (dag output-name) {:id (artdag/dag-id dag output-name) :type artdag/post-type :wire (artdag/dag->wire dag)})) + +(define + artdag/post-object? + (fn + (x) + (and + (= (type-of x) "dict") + (= (get x :type) artdag/post-type) + (has-key? x :id) + (has-key? x :wire)))) + +(define artdag/post-object-id (fn (post) (get post :id))) + +(define artdag/post-object-wire (fn (post) (get post :wire))) + +; integrity: the payload's records each verify (id == recomputed content-id) AND the +; claimed post id is actually produced by the job (present among the wire records). +(define + artdag/post-object-verify + (fn + (post) + (and + (artdag/post-object? post) + (artdag/wire-verify (get post :wire)) + (artdag/member? + (get post :id) + (map (fn (rec) (nth rec 0)) (get post :wire)))))) + +; decode the payload back into a runnable dag (pure; verify separately, mirroring +; serialize.sx's wire->dag / wire-verify split). +(define + artdag/post-object->job + (fn (post) (artdag/wire->dag (get post :wire)))) + +; ---- string transport (drop into a feed activity / SXTP body) ---- + +(define + artdag/job->post-string + (fn + (dag output-name) + (write-to-string (artdag/job->post-object dag output-name)))) + +(define artdag/post-string->object (fn (s) (read (open-input-string s)))) + +; ---- run a received post: decode -> run -> result at the post id ---- +; the peer recomputes the job (content-addressed, so a warm cache hits everything it +; already has). Returns the result of the output node the post is about. +(define + artdag/post-run + (fn + (post runner cache) + (artdag/result-of + (artdag/run (artdag/post-object->job post) runner cache) + (artdag/post-object-id post)))) diff --git a/lib/artdag/scoreboard.json b/lib/artdag/scoreboard.json index ccd74e30..6cce7310 100644 --- a/lib/artdag/scoreboard.json +++ b/lib/artdag/scoreboard.json @@ -10,10 +10,11 @@ "serialize": {"pass": 13, "fail": 0}, "stats": {"pass": 12, "fail": 0}, "fault": {"pass": 14, "fail": 0}, + "post": {"pass": 12, "fail": 0}, "maude-optimize": {"pass": 40, "fail": 0}, "schedule": {"pass": 15, "fail": 0} }, - "total_pass": 213, + "total_pass": 225, "total_fail": 0, - "total": 213 + "total": 225 } diff --git a/lib/artdag/scoreboard.md b/lib/artdag/scoreboard.md index 785cb792..29911397 100644 --- a/lib/artdag/scoreboard.md +++ b/lib/artdag/scoreboard.md @@ -14,6 +14,7 @@ _Generated by `lib/artdag/conformance.sh`_ | serialize | 13 | 0 | 13 | | stats | 12 | 0 | 12 | | fault | 14 | 0 | 14 | +| post | 12 | 0 | 12 | | maude-optimize | 40 | 0 | 40 | | schedule | 15 | 0 | 15 | -| **Total** | **213** | **0** | **213** | +| **Total** | **225** | **0** | **225** | diff --git a/lib/artdag/tests/post.sx b/lib/artdag/tests/post.sx new file mode 100644 index 00000000..94b223c0 --- /dev/null +++ b/lib/artdag/tests/post.sx @@ -0,0 +1,111 @@ +; Forward direction — artdag job as a feed "post object" (per the host loop). +; A job projects to a content-addressed, self-verifying object suitable as a feed +; activity :object; a peer decodes, verifies and runs it to the same result. + +(define po-runner (artdag/op-table-runner {:blur (fn (params inputs) (+ (first inputs) (get params :radius))) :src (fn (params inputs) 0) :over (fn (params inputs) (+ (nth inputs 0) (nth inputs 1)))})) + +(define + po-job + (artdag/build + (list + (list "s" "src" (list) {}) + (list "b" "blur" (list "s") {:radius 2}) + (list "c" "blur" (list "s") {:radius 3}) + (list "out" "over" (list "b" "c") {} true)))) +(define po-out-id (artdag/dag-id po-job "out")) +(define po-post (artdag/job->post-object po-job "out")) + +(artdag-test + "post: is a well-formed post object" + (artdag/post-object? po-post) + true) + +(artdag-test "post: type tag is artdag/job" (get po-post :type) "artdag/job") + +(artdag-test + "post: post id is the output node's content-id" + (artdag/post-object-id po-post) + po-out-id) + +(artdag-test + "post: payload is the whole dag (one record per node)" + (len (artdag/post-object-wire po-post)) + (artdag/node-count po-job)) + +(artdag-test + "post: verifies (ids intact, output present)" + (artdag/post-object-verify po-post) + true) + +; ---- round-trip: decode reconstructs the job by content-id ---- + +(define po-job2 (artdag/post-object->job po-post)) + +(artdag-test + "post: decoded job contains the output node by content-id" + (artdag/member? po-out-id (keys (artdag/dag-nodes po-job2))) + true) + +(artdag-test + "post: decoded job has the same node count" + (artdag/node-count po-job2) + (artdag/node-count po-job)) + +; ---- string transport (feed activity / SXTP body) ---- + +(define po-str (artdag/job->post-string po-job "out")) +(define po-post2 (artdag/post-string->object po-str)) + +(artdag-test + "post: survives string transport (id preserved)" + (artdag/post-object-id po-post2) + po-out-id) + +(artdag-test + "post: transported post still verifies" + (artdag/post-object-verify po-post2) + true) + +; ---- a peer runs the received post to the same result ---- + +(define + po-local-result + (artdag/result-of (artdag/run po-job po-runner (persist/open)) po-out-id)) +(define po-peer-result (artdag/post-run po-post2 po-runner (persist/open))) + +(artdag-test + "post: peer runs the received job to the same result" + (= po-peer-result po-local-result) + true) + +; ---- tamper detection: mutate a param under a stale id ---- + +(define + po-tampered + (assoc + po-post + :wire (map + (fn + (rec) + (if + (= (nth rec 1) "blur") + (list + (nth rec 0) + (nth rec 1) + (nth rec 2) + {:radius 99} + (nth rec 4)) + rec)) + (artdag/post-object-wire po-post)))) + +(artdag-test + "post: tampered payload fails verification" + (artdag/post-object-verify po-tampered) + false) + +; ---- an id not produced by the job fails verification ---- + +(artdag-test + "post: post id absent from payload fails verification" + (artdag/post-object-verify (assoc po-post :id "node:bogus")) + false) diff --git a/plans/artdag-on-sx.md b/plans/artdag-on-sx.md index 2a4300f1..60c99d06 100644 --- a/plans/artdag-on-sx.md +++ b/plans/artdag-on-sx.md @@ -31,7 +31,7 @@ edges. ## Status (rolling) -`bash lib/artdag/conformance.sh` → **213/213** (12 suites: dag, analyze, plan, execute, optimize, fed, cost, serialize, stats, fault, maude-optimize, schedule) +`bash lib/artdag/conformance.sh` → **225/225** (13 suites: dag, analyze, plan, execute, optimize, fed, cost, serialize, stats, fault, post, maude-optimize, schedule) Base roadmap (Phases 1–6) COMPLETE + Phase 7 (maude rule-based optimization) COMPLETE (only optional miniKanren scheduling remains). Now hardening only. @@ -117,10 +117,14 @@ loop's `GET/POST /feed`. The engine already has the primitives; the wrapping liv - **federation already mirrors the feed** — `fed-export`/`fed-import` are trust-gated with provenance; a re-posted job dedupes/cache-hits by global content-id. -Boundary: host loop owns the `dag ⇄ feed-object` adapter; `lib/artdag` stays the engine. -Candidate artdag-side affordance (only if wanted here): a thin `job->post-object` / -`post-object->job` projection so host never reaches into wire internals. Not yet built — -flagged, not scheduled. +Boundary: host loop owns the activity wrapping (actor/verb/at/tags); `lib/artdag` owns the +job⇄object projection. **BUILT — `lib/artdag/post.sx`** (post suite 12/12): a post object is +`{:type "artdag/job" :id :wire wire>}` — `job->post-object`, +`post-object->job`, `post-object-verify` (wire ids intact + the post id is produced by the +job; rejects tampered params and bogus ids), `job->post-string`/`post-string->object` for the +feed/SXTP body, and `post-run` (a peer decodes → runs → result at the post id, content-address +cache-hitting what it already has). The host loop drops the object into a feed activity's +`:object`; post-id = content-id = the AP object id. ## Phase 1 — DAG model + content addressing @@ -237,6 +241,19 @@ be an op token. ## Progress log +- **2026-06-28 Forward — job as a feed post object** (post suite 12/12, total 225/225). + `lib/artdag/post.sx`: the artdag-side projection for "a job is a type of post" (per the + host loop). `job->post-object dag output-name` → `{:type "artdag/job" :id :wire wire>}` — the post/object id IS the output node's content-id (= the + natural AP object id), the body is serialize.sx's self-describing wire. `post-object-verify` + binds the claimed id to the payload (every record's id recomputes + the post id is present + among them) — rejects a param tampered under a stale id and a bogus post id. + `job->post-string`/`post-string->object` carry it in a feed activity / SXTP body; + `post-object->job` decodes; `post-run post runner cache` lets a peer decode → run → read the + result at the post id (content-addressed, so a warm peer cache-hits what it already holds). + The activity wrapper (actor/verb/at/tags) stays on the host/feed side — this is only + job⇄object. Reuses serialize.sx + execute.sx; no new substrate. + - **2026-06-28 Phase 3/7 — miniKanren CLP(FD) scheduler** (schedule suite 15/15, total 213/213). `lib/artdag/schedule.sx` on `lib/minikanren` (read-only substrate): each node gets a slot var in `[1..max-slots]`, every edge `(input->node)` imposes `fd-lt