diff --git a/next/genesis/manifest.sx b/next/genesis/manifest.sx index 607197f2..8e9b8a6d 100644 --- a/next/genesis/manifest.sx +++ b/next/genesis/manifest.sx @@ -21,7 +21,16 @@ :activity-types ("activity-types/create.sx" "activity-types/update.sx" "activity-types/delete.sx") - :object-types () + :object-types ("object-types/sx-artifact.sx" + "object-types/note.sx" + "object-types/tombstone.sx" + "object-types/define-activity.sx" + "object-types/define-object.sx" + "object-types/define-projection.sx" + "object-types/define-validator.sx" + "object-types/define-codec.sx" + "object-types/define-sig-suite.sx" + "object-types/snapshot.sx") :projections () :validators () :codecs () diff --git a/next/genesis/object-types/define-activity.sx b/next/genesis/object-types/define-activity.sx new file mode 100644 index 00000000..a4699c95 --- /dev/null +++ b/next/genesis/object-types/define-activity.sx @@ -0,0 +1,12 @@ +;; next/genesis/object-types/define-activity.sx +;; +;; Meta-object that registers a new activity verb. Published as +;; Create{DefineActivity{...}}; the define-registry projection +;; folds it into the activity-types registry. Per design §5. + +(DefineObject + :name "DefineActivity" + :doc "Activity-type registration. :name is the verb (e.g.\n \"Pin\"); :schema is an SX predicate over activity\n envelopes; :semantics is an optional state-fold body." + :schema (fn + (obj) + (and (string? (-> obj :name)) (not (nil? (-> obj :schema)))))) diff --git a/next/genesis/object-types/define-codec.sx b/next/genesis/object-types/define-codec.sx new file mode 100644 index 00000000..dcead7a2 --- /dev/null +++ b/next/genesis/object-types/define-codec.sx @@ -0,0 +1,15 @@ +;; next/genesis/object-types/define-codec.sx +;; +;; Meta-object that registers a content codec — an encode/decode +;; pair. The bootstrap bundle ships dag-cbor, raw, and dag-json +;; codecs; new codecs can be added via Create{DefineCodec{...}}. + +(DefineObject + :name "DefineCodec" + :doc "Codec registration. :name identifies the codec ('dag-cbor',\n 'raw', 'dag-json', ...); :encode and :decode are the\n SX bodies the kernel calls when serialising / parsing\n artifacts under this codec." + :schema (fn + (obj) + (and + (string? (-> obj :name)) + (not (nil? (-> obj :encode))) + (not (nil? (-> obj :decode)))))) diff --git a/next/genesis/object-types/define-object.sx b/next/genesis/object-types/define-object.sx new file mode 100644 index 00000000..1ee7566a --- /dev/null +++ b/next/genesis/object-types/define-object.sx @@ -0,0 +1,12 @@ +;; next/genesis/object-types/define-object.sx +;; +;; Meta-object that registers a new object-type. Bootstrap-level — +;; runtime registration of new object types (e.g. DefineSubscription +;; in the Step 9b smoke test) flows through this. + +(DefineObject + :name "DefineObject" + :doc "Object-type registration. :name is the type tag (e.g.\n \"PinSpec\"); :schema is an SX predicate over object\n forms of that type." + :schema (fn + (obj) + (and (string? (-> obj :name)) (not (nil? (-> obj :schema)))))) diff --git a/next/genesis/object-types/define-projection.sx b/next/genesis/object-types/define-projection.sx new file mode 100644 index 00000000..31bac635 --- /dev/null +++ b/next/genesis/object-types/define-projection.sx @@ -0,0 +1,16 @@ +;; next/genesis/object-types/define-projection.sx +;; +;; Meta-object that registers a new projection. The projection +;; scheduler (Step 7) spawns one gen_server per registered +;; projection and feeds activities through its :fold body in +;; sandbox mode. + +(DefineObject + :name "DefineProjection" + :doc "Projection registration. :name is the projection key;\n :initial-state is the empty state value; :fold is the\n pure (state activity) -> state function evaluated in\n sandbox mode per activity." + :schema (fn + (obj) + (and + (string? (-> obj :name)) + (not (nil? (-> obj :initial-state))) + (not (nil? (-> obj :fold)))))) diff --git a/next/genesis/object-types/define-sig-suite.sx b/next/genesis/object-types/define-sig-suite.sx new file mode 100644 index 00000000..fdb229b3 --- /dev/null +++ b/next/genesis/object-types/define-sig-suite.sx @@ -0,0 +1,12 @@ +;; next/genesis/object-types/define-sig-suite.sx +;; +;; Meta-object that registers a signature suite. Bootstrap ships +;; rsa-sha256-2018 and ed25519-2020; the suite name maps an +;; algorithm to a :verify body and a :key-format predicate. + +(DefineObject + :name "DefineSigSuite" + :doc "Signature suite registration. :name identifies the suite\n ('rsa-sha256-2018', 'ed25519-2020', ...); :verify is the\n SX (canonical-bytes signature key) -> bool body; the\n envelope-signature validator dispatches by suite name." + :schema (fn + (obj) + (and (string? (-> obj :name)) (not (nil? (-> obj :verify)))))) diff --git a/next/genesis/object-types/define-validator.sx b/next/genesis/object-types/define-validator.sx new file mode 100644 index 00000000..c487d508 --- /dev/null +++ b/next/genesis/object-types/define-validator.sx @@ -0,0 +1,12 @@ +;; next/genesis/object-types/define-validator.sx +;; +;; Meta-object that registers a validator predicate. The validation +;; pipeline (Step 6) consults registered validators by name when +;; running its stages. + +(DefineObject + :name "DefineValidator" + :doc "Validator registration. :name is the validator key (e.g.\n \"envelope-shape\"); :predicate is the SX (activity) ->\n ok|{error, R} body." + :schema (fn + (obj) + (and (string? (-> obj :name)) (not (nil? (-> obj :predicate)))))) diff --git a/next/genesis/object-types/note.sx b/next/genesis/object-types/note.sx new file mode 100644 index 00000000..bc9de7c2 --- /dev/null +++ b/next/genesis/object-types/note.sx @@ -0,0 +1,10 @@ +;; next/genesis/object-types/note.sx +;; +;; Short message intended for an audience, ActivityPub-Note-compatible. +;; Used by the Step 9b reactive smoke test (Note tagged "smoketest" +;; matches the Topic subscription). + +(DefineObject + :name "Note" + :doc "Short authored message. :content is the body text;\n :tags is a list of subscription-routable tags." + :schema (fn (obj) (string? (-> obj :content)))) diff --git a/next/genesis/object-types/snapshot.sx b/next/genesis/object-types/snapshot.sx new file mode 100644 index 00000000..81786b69 --- /dev/null +++ b/next/genesis/object-types/snapshot.sx @@ -0,0 +1,13 @@ +;; next/genesis/object-types/snapshot.sx +;; +;; Projection state checkpoint. The projection scheduler emits +;; Snapshot{projection-name, state-cid, log-seq} periodically; +;; cold starts read the most recent Snapshot and replay only +;; activities after :log-seq. Per design §10.5. + +(DefineObject + :name "Snapshot" + :doc "Projection-state checkpoint. :projection-name identifies\n the projection; :state-cid is the content-address of\n the snapshotted state value; :log-seq is the activity\n sequence number the snapshot was taken at." + :schema (fn + (obj) + (and (string? (-> obj :projection-name)) (string? (-> obj :state-cid))))) diff --git a/next/genesis/object-types/sx-artifact.sx b/next/genesis/object-types/sx-artifact.sx new file mode 100644 index 00000000..3541a65d --- /dev/null +++ b/next/genesis/object-types/sx-artifact.sx @@ -0,0 +1,10 @@ +;; next/genesis/object-types/sx-artifact.sx +;; +;; Content-addressed SX source — a library, component, or +;; executable form published via Create{SXArtifact{...}}. +;; Consumers reference an artifact by its CID. Per design §3.4. + +(DefineObject + :name "SXArtifact" + :doc "Published SX source. :source carries the form text;\n :language is optional ('sx' by default); :imports lists\n CIDs the artifact depends on." + :schema (fn (obj) (string? (-> obj :source)))) diff --git a/next/genesis/object-types/tombstone.sx b/next/genesis/object-types/tombstone.sx new file mode 100644 index 00000000..05897fb2 --- /dev/null +++ b/next/genesis/object-types/tombstone.sx @@ -0,0 +1,9 @@ +;; next/genesis/object-types/tombstone.sx +;; +;; Replacement for an object that has been Delete'd. Lets projection +;; folds keep a marker without retaining the deleted content. + +(DefineObject + :name "Tombstone" + :doc "Marker for a deleted object. :former-cid carries the CID\n of the object that was removed. Projections fold Tombstone\n by replacing the cached entry (not by omitting it)." + :schema (fn (obj) (string? (-> obj :former-cid)))) diff --git a/next/tests/genesis_parse.sh b/next/tests/genesis_parse.sh index eb1fc5d7..6343fef3 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. 10 cases. +# these forms directly as data. 22 cases. set -uo pipefail cd "$(git rev-parse --show-toplevel)" @@ -42,6 +42,30 @@ cat > "$TMPFILE" <<'EPOCHS' (eval "(get (apply dict (rest (parse (file-read \"next/genesis/activity-types/delete.sx\")))) :name)") (epoch 19) (eval "(len (get (apply dict (rest (parse (file-read \"next/genesis/manifest.sx\")))) :activity-types))") +(epoch 30) +(eval "(first (parse (file-read \"next/genesis/object-types/sx-artifact.sx\")))") +(epoch 31) +(eval "(get (apply dict (rest (parse (file-read \"next/genesis/object-types/sx-artifact.sx\")))) :name)") +(epoch 32) +(eval "(get (apply dict (rest (parse (file-read \"next/genesis/object-types/note.sx\")))) :name)") +(epoch 33) +(eval "(get (apply dict (rest (parse (file-read \"next/genesis/object-types/tombstone.sx\")))) :name)") +(epoch 34) +(eval "(get (apply dict (rest (parse (file-read \"next/genesis/object-types/define-activity.sx\")))) :name)") +(epoch 35) +(eval "(get (apply dict (rest (parse (file-read \"next/genesis/object-types/define-object.sx\")))) :name)") +(epoch 36) +(eval "(get (apply dict (rest (parse (file-read \"next/genesis/object-types/define-projection.sx\")))) :name)") +(epoch 37) +(eval "(get (apply dict (rest (parse (file-read \"next/genesis/object-types/define-validator.sx\")))) :name)") +(epoch 38) +(eval "(get (apply dict (rest (parse (file-read \"next/genesis/object-types/define-codec.sx\")))) :name)") +(epoch 39) +(eval "(get (apply dict (rest (parse (file-read \"next/genesis/object-types/define-sig-suite.sx\")))) :name)") +(epoch 40) +(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))") EPOCHS OUTPUT=$(timeout 30 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) @@ -75,6 +99,18 @@ check 16 "update.sx name is Update" "Update" check 17 "delete.sx head form" "DefineActivity" check 18 "delete.sx name is Delete" "Delete" check 19 "manifest has 3 activity-types" "3" +check 30 "sx-artifact.sx head form" "DefineObject" +check 31 "sx-artifact.sx name" "SXArtifact" +check 32 "note.sx name" "Note" +check 33 "tombstone.sx name" "Tombstone" +check 34 "define-activity.sx name" "DefineActivity" +check 35 "define-object.sx name" "DefineObject" +check 36 "define-projection.sx name" "DefineProjection" +check 37 "define-validator.sx name" "DefineValidator" +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" 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 5c0eca16..e3386a5e 100644 --- a/plans/fed-sx-milestone-1.md +++ b/plans/fed-sx-milestone-1.md @@ -246,7 +246,7 @@ replay(LogState, InitAcc, Fun) -> ... **Sub-deliverables:** - [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`) -- [ ] **4b-obj** — Object-types: SXArtifact, Note, Tombstone, DefineActivity, DefineObject, DefineProjection, DefineValidator, DefineCodec, DefineSigSuite, Snapshot +- [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 - [ ] **4b-vld** — Validators: envelope-shape, signature, type-schema - [ ] **4b-cod** — Codecs + sig-suites + audience predicates @@ -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-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. - **2026-05-27** — Step 3a: `log:open/2 append/2 tip/1 replay/3 entries/1` over an in-memory state (per-actor seq, replay in append order, round-trip activities). `next/tests/log_memory.sh` 12/12. Pivoted from on-disk in this iteration: this port's `atom_to_list`/`integer_to_list` return SX strings rather than Erlang charlists, `binary_to_list` is unregistered, and `$X` char literals decode to nil — so a term codec needs a workaround. Captured as the Step 3b risk note in the plan. Erlang conformance 729/729.