From cfdb9cd8759e763352c6959303936e88f870fe26 Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 27 May 2026 22:52:54 +0000 Subject: [PATCH] =?UTF-8?q?fed-sx-m1:=20Step=204b-proj=20=E2=80=94=207=20b?= =?UTF-8?q?ootstrap=20projections=20+=20manifest=20update=20+=209=20new=20?= =?UTF-8?q?parse=20tests=20(31=20total)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next/genesis/manifest.sx | 8 ++++- next/genesis/projections/activity-log.sx | 11 +++++++ next/genesis/projections/actor-state.sx | 26 ++++++++++++++++ next/genesis/projections/audience-graph.sx | 25 ++++++++++++++++ next/genesis/projections/by-actor.sx | 15 ++++++++++ next/genesis/projections/by-object.sx | 22 ++++++++++++++ next/genesis/projections/by-type.sx | 15 ++++++++++ next/genesis/projections/define-registry.sx | 33 +++++++++++++++++++++ next/tests/genesis_parse.sh | 29 +++++++++++++++++- plans/fed-sx-milestone-1.md | 3 +- 10 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 next/genesis/projections/activity-log.sx create mode 100644 next/genesis/projections/actor-state.sx create mode 100644 next/genesis/projections/audience-graph.sx create mode 100644 next/genesis/projections/by-actor.sx create mode 100644 next/genesis/projections/by-object.sx create mode 100644 next/genesis/projections/by-type.sx create mode 100644 next/genesis/projections/define-registry.sx diff --git a/next/genesis/manifest.sx b/next/genesis/manifest.sx index 8e9b8a6d..9847d6cf 100644 --- a/next/genesis/manifest.sx +++ b/next/genesis/manifest.sx @@ -31,7 +31,13 @@ "object-types/define-codec.sx" "object-types/define-sig-suite.sx" "object-types/snapshot.sx") - :projections () + :projections ("projections/activity-log.sx" + "projections/by-type.sx" + "projections/by-actor.sx" + "projections/by-object.sx" + "projections/actor-state.sx" + "projections/define-registry.sx" + "projections/audience-graph.sx") :validators () :codecs () :sig-suites () diff --git a/next/genesis/projections/activity-log.sx b/next/genesis/projections/activity-log.sx new file mode 100644 index 00000000..2732d778 --- /dev/null +++ b/next/genesis/projections/activity-log.sx @@ -0,0 +1,11 @@ +;; next/genesis/projections/activity-log.sx +;; +;; Identity projection: stores every activity by its CID. The +;; base ledger every other projection could be re-derived from +;; if needed. Per design §10.2. + +(DefineProjection + :name "activity-log" + :doc "Maps activity CID to the full envelope. Every activity\n flows through; no filter. State is the CID-keyed dict." + :initial-state {} + :fold (fn (state act) (assoc state (-> act :cid) act))) diff --git a/next/genesis/projections/actor-state.sx b/next/genesis/projections/actor-state.sx new file mode 100644 index 00000000..7d57f577 --- /dev/null +++ b/next/genesis/projections/actor-state.sx @@ -0,0 +1,26 @@ +;; next/genesis/projections/actor-state.sx +;; +;; Per-actor live state: publicKeys (with history per design §9.6), +;; profile fields (preferredUsername, summary, ...), follower/ +;; following counts. Powers the actor doc endpoint and the +;; time-aware signature verification in envelope:verify_signature/2. + +(DefineProjection + :name "actor-state" + :doc "Actor-id -> {publicKeys, profile, followers, following}.\n Updated by Create{Person|Service|Group}, Update (key\n rotation, profile edits), Move (federation migration)." + :initial-state {} + :fold (fn + (state act) + (let + ((aid (-> act :actor)) (t (-> act :type))) + (cond + (= t "Create") + (assoc state aid (or (-> act :object) {})) + (= t "Update") + (assoc + state + aid + (merge + (or (get state aid) {}) + (or (-> act :patch) {}))) + :else state)))) diff --git a/next/genesis/projections/audience-graph.sx b/next/genesis/projections/audience-graph.sx new file mode 100644 index 00000000..7a127dc5 --- /dev/null +++ b/next/genesis/projections/audience-graph.sx @@ -0,0 +1,25 @@ +;; next/genesis/projections/audience-graph.sx +;; +;; Per-actor follow / follower graph and audience caches. Folded +;; from Follow / Accept / Reject / Undo{Follow}. Used by the +;; activity router to expand :to / :cc audiences (Public, +;; Followers, Direct) into concrete recipient sets. Per design §16. + +(DefineProjection + :name "audience-graph" + :doc "Actor-id -> {following, followers, pending} sets.\n Updated by Follow / Accept / Reject / Undo. Federation\n (m2) wires this projection to the delivery queue." + :initial-state {} + :fold (fn + (state act) + (let + ((t (-> act :type))) + (cond + (= t "Follow") + state + (= t "Accept") + state + (= t "Reject") + state + (= t "Undo") + state + :else state)))) diff --git a/next/genesis/projections/by-actor.sx b/next/genesis/projections/by-actor.sx new file mode 100644 index 00000000..fe2255df --- /dev/null +++ b/next/genesis/projections/by-actor.sx @@ -0,0 +1,15 @@ +;; next/genesis/projections/by-actor.sx +;; +;; Index of activity CIDs grouped by :actor. Maps actor-id to a +;; list of CIDs in append order. Powers the per-actor outbox +;; listing (Step 8) without re-scanning the full log. + +(DefineProjection + :name "by-actor" + :doc "Actor-id -> list of activity CIDs (append order)." + :initial-state {} + :fold (fn + (state act) + (let + ((a (-> act :actor)) (cid (-> act :cid))) + (assoc state a (append (or (get state a) (list)) (list cid)))))) diff --git a/next/genesis/projections/by-object.sx b/next/genesis/projections/by-object.sx new file mode 100644 index 00000000..24892cdd --- /dev/null +++ b/next/genesis/projections/by-object.sx @@ -0,0 +1,22 @@ +;; next/genesis/projections/by-object.sx +;; +;; Index of activities that reference each :object CID. Maps +;; object-CID to the list of activity CIDs that target it +;; (Update / Delete / Announce / etc.). Used for "show me +;; everything that happened to X" queries. + +(DefineProjection + :name "by-object" + :doc "Object CID -> list of activity CIDs that target it." + :initial-state {} + :fold (fn + (state act) + (let + ((obj-cid (-> act :object)) (cid (-> act :cid))) + (if + (string? obj-cid) + (assoc + state + obj-cid + (append (or (get state obj-cid) (list)) (list cid))) + state)))) diff --git a/next/genesis/projections/by-type.sx b/next/genesis/projections/by-type.sx new file mode 100644 index 00000000..0bda97cf --- /dev/null +++ b/next/genesis/projections/by-type.sx @@ -0,0 +1,15 @@ +;; next/genesis/projections/by-type.sx +;; +;; Index of activity CIDs grouped by :type. Maps type-name to a +;; list of CIDs in append order. Used by the outbox listing +;; endpoints (Step 8) for type-filtered pagination. + +(DefineProjection + :name "by-type" + :doc "Type-name -> list of activity CIDs (append order)." + :initial-state {} + :fold (fn + (state act) + (let + ((t (-> act :type)) (cid (-> act :cid))) + (assoc state t (append (or (get state t) (list)) (list cid)))))) diff --git a/next/genesis/projections/define-registry.sx b/next/genesis/projections/define-registry.sx new file mode 100644 index 00000000..6ee22241 --- /dev/null +++ b/next/genesis/projections/define-registry.sx @@ -0,0 +1,33 @@ +;; next/genesis/projections/define-registry.sx +;; +;; The meta-projection: folds Create{Define*{...}} activities into +;; the kernel registry. Resolves the chicken-and-egg circle — +;; bootstrap.erl populates the registry directly at startup from +;; the genesis bundle, and from then on define-registry's fold +;; keeps it current as new Define* activities arrive. Per design §5. + +(DefineProjection + :name "define-registry" + :doc "Maps {kind, name} -> definition entry. Folded from\n Create{DefineActivity|DefineObject|DefineProjection|\n DefineValidator|DefineCodec|DefineSigSuite|...}. Kind is\n derived from the inner :object :type tag." + :initial-state {} + :fold (fn + (state act) + (let + ((obj (-> act :object)) (otype (-> act :object :type))) + (cond + (= (-> act :type) "Create") + (cond + (= otype "DefineActivity") + (assoc-in state (list :activity-types (-> obj :name)) obj) + (= otype "DefineObject") + (assoc-in state (list :object-types (-> obj :name)) obj) + (= otype "DefineProjection") + (assoc-in state (list :projections (-> obj :name)) obj) + (= otype "DefineValidator") + (assoc-in state (list :validators (-> obj :name)) obj) + (= otype "DefineCodec") + (assoc-in state (list :codecs (-> obj :name)) obj) + (= otype "DefineSigSuite") + (assoc-in state (list :sig-suites (-> obj :name)) obj) + :else state) + :else state)))) diff --git a/next/tests/genesis_parse.sh b/next/tests/genesis_parse.sh index 6343fef3..ff533be1 100755 --- a/next/tests/genesis_parse.sh +++ b/next/tests/genesis_parse.sh @@ -3,7 +3,7 @@ # # Confirms the seed genesis SX files parse cleanly and have the # expected top-level head form. The bundler (Step 4c+) consumes -# these forms directly as data. 22 cases. +# these forms directly as data. 31 cases. set -uo pipefail cd "$(git rev-parse --show-toplevel)" @@ -66,6 +66,24 @@ cat > "$TMPFILE" <<'EPOCHS' (eval "(get (apply dict (rest (parse (file-read \"next/genesis/object-types/snapshot.sx\")))) :name)") (epoch 41) (eval "(len (get (apply dict (rest (parse (file-read \"next/genesis/manifest.sx\")))) :object-types))") +(epoch 50) +(eval "(first (parse (file-read \"next/genesis/projections/activity-log.sx\")))") +(epoch 51) +(eval "(get (apply dict (rest (parse (file-read \"next/genesis/projections/activity-log.sx\")))) :name)") +(epoch 52) +(eval "(get (apply dict (rest (parse (file-read \"next/genesis/projections/by-type.sx\")))) :name)") +(epoch 53) +(eval "(get (apply dict (rest (parse (file-read \"next/genesis/projections/by-actor.sx\")))) :name)") +(epoch 54) +(eval "(get (apply dict (rest (parse (file-read \"next/genesis/projections/by-object.sx\")))) :name)") +(epoch 55) +(eval "(get (apply dict (rest (parse (file-read \"next/genesis/projections/actor-state.sx\")))) :name)") +(epoch 56) +(eval "(get (apply dict (rest (parse (file-read \"next/genesis/projections/define-registry.sx\")))) :name)") +(epoch 57) +(eval "(get (apply dict (rest (parse (file-read \"next/genesis/projections/audience-graph.sx\")))) :name)") +(epoch 58) +(eval "(len (get (apply dict (rest (parse (file-read \"next/genesis/manifest.sx\")))) :projections))") EPOCHS OUTPUT=$(timeout 30 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) @@ -111,6 +129,15 @@ check 38 "define-codec.sx name" "DefineCodec" check 39 "define-sig-suite.sx name" "DefineSigSuite" check 40 "snapshot.sx name" "Snapshot" check 41 "manifest has 10 object-types" "10" +check 50 "activity-log.sx head form" "DefineProjection" +check 51 "activity-log.sx name" "activity-log" +check 52 "by-type.sx name" "by-type" +check 53 "by-actor.sx name" "by-actor" +check 54 "by-object.sx name" "by-object" +check 55 "actor-state.sx name" "actor-state" +check 56 "define-registry.sx name" "define-registry" +check 57 "audience-graph.sx name" "audience-graph" +check 58 "manifest has 7 projections" "7" TOTAL=$((PASS+FAIL)) if [ $FAIL -eq 0 ]; then diff --git a/plans/fed-sx-milestone-1.md b/plans/fed-sx-milestone-1.md index e3386a5e..dddb5f8a 100644 --- a/plans/fed-sx-milestone-1.md +++ b/plans/fed-sx-milestone-1.md @@ -247,7 +247,7 @@ replay(LogState, InitAcc, Fun) -> ... - [x] **4a** — Seed genesis SX file authoring: `next/genesis/manifest.sx` + `next/genesis/activity-types/create.sx`. Manifest uses bare parenthesised paths (data lists, not `(list ...)` calls — consumed by `parse`, not `eval`). `next/tests/genesis_parse.sh` (5 cases). - [x] **4b-act** — Remaining activity-types: `update.sx` + `delete.sx`, manifest updated, parse tests (10 cases total in `genesis_parse.sh`) - [x] **4b-obj** — Object-types: SXArtifact, Note, Tombstone, DefineActivity, DefineObject, DefineProjection, DefineValidator, DefineCodec, DefineSigSuite, Snapshot — 10 `DefineObject` files + manifest updated + 12 new parse tests -- [ ] **4b-proj** — Projections: activity-log, by-type, by-actor, by-object, actor-state, define-registry, audience-graph +- [x] **4b-proj** — Projections: activity-log, by-type, by-actor, by-object, actor-state, define-registry, audience-graph — 7 `DefineProjection` files + manifest updated + 9 new parse tests - [ ] **4b-vld** — Validators: envelope-shape, signature, type-schema - [ ] **4b-cod** — Codecs + sig-suites + audience predicates - [ ] **4c** — `bootstrap:read_genesis/1` in Erlang: walk the manifest, file-read each referenced .sx, return parsed forms @@ -955,6 +955,7 @@ A few things still under-specified; resolve as work begins. Newest first. One line per sub-deliverable commit. Erlang conformance gate (`bash lib/erlang/conformance.sh`) must remain 729/729 on every entry. +- **2026-05-27** — Step 4b-proj: bootstrap projections complete — 7 `DefineProjection` SX files authored (activity-log identity, by-type/by-actor/by-object indexes, actor-state with key history fold, define-registry meta-fold over Create{Define*}, audience-graph stub). Manifest `:projections` populated; `next/tests/genesis_parse.sh` 31/31. Erlang conformance 729/729. - **2026-05-27** — Step 4b-obj: bootstrap object-types complete — 10 `DefineObject` SX files authored (SXArtifact, Note, Tombstone, DefineActivity, DefineObject, DefineProjection, DefineValidator, DefineCodec, DefineSigSuite, Snapshot). Each carries an SX `:schema` predicate. Manifest `:object-types` populated; `next/tests/genesis_parse.sh` 22/22. Erlang conformance 729/729. - **2026-05-27** — Step 4b-act: bootstrap activity-types complete — `update.sx` (Update verb, requires :object CID + :patch) + `delete.sx` (Delete verb, requires :object CID) authored as DefineActivity forms matching the Create shape. Manifest updated; `next/tests/genesis_parse.sh` 10/10. Step 4b broken into act/obj/proj/vld/cod sub-deliverables on the plan. Erlang conformance 729/729. - **2026-05-27** — Step 4a: genesis bundle seeded. `next/genesis/manifest.sx` (GenesisManifest with eight section keys, only `:activity-types` populated for now) + `next/genesis/activity-types/create.sx` (DefineActivity{Create} with :schema/:semantics SX bodies). `next/tests/genesis_parse.sh` 5/5. Step 3b parked behind a substrate-level term-codec gap — Blockers note added under Step 3; in-memory log from 3a unblocks Step 5+ which only need the API surface. Erlang conformance 729/729.