diff --git a/next/kernel/bootstrap.erl b/next/kernel/bootstrap.erl index 5e86eaa6..80fcad63 100644 --- a/next/kernel/bootstrap.erl +++ b/next/kernel/bootstrap.erl @@ -5,7 +5,8 @@ build_genesis/1, verify_genesis/2, cidhash_path/1, write_cidhash/2, read_cidhash/1, load_genesis/1, strip_sx_suffix/1, - populate_registry/0]). + populate_registry/0, + start/3]). %% Genesis bundle reader per design §12.2. %% @@ -205,3 +206,18 @@ populate_entries(Kind, [{Name, Bytes} | Rest], Count) -> BaseName = strip_sx_suffix(Name), ok = registry:register(Kind, BaseName, Bytes), populate_entries(Kind, Rest, Count + 1). + +%% start/3 — one-call bring-up of the kernel substrate. Starts +%% the registry gen_server, populates it from the canonical +%% genesis bundle, then starts the nx_kernel gen_server with the +%% supplied actor identity / key / state. Returns the nx_kernel +%% Pid (gen_server start_link convention in this port returns the +%% raw Pid, not {ok, Pid}). +%% +%% Tests + production bring-up share this entry point. The +%% caller is still responsible for starting any application-level +%% projections and wiring them via nx_kernel:with_projections/1. +start(ActorId, KeySpec, ActorState) -> + registry:start_link(), + populate_registry(), + nx_kernel:start_link(ActorId, KeySpec, ActorState). diff --git a/next/tests/bootstrap_start.sh b/next/tests/bootstrap_start.sh new file mode 100755 index 00000000..8467e60c --- /dev/null +++ b/next/tests/bootstrap_start.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# next/tests/bootstrap_start.sh — Step 4f-consolidate test. +# +# bootstrap:start/3 is the one-call kernel bring-up: starts the +# registry gen_server, populates it from the genesis bundle, +# and starts the nx_kernel gen_server. Each test inlines the +# start call with downstream operations because spawned +# processes don't survive across separate erlang-eval-ast calls. +# 11 cases. + +set -uo pipefail +cd "$(git rev-parse --show-toplevel)" + +SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}" +if [ ! -x "$SX_SERVER" ]; then + SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe" +fi +if [ ! -x "$SX_SERVER" ]; then + echo "ERROR: sx_server.exe not found." >&2 + exit 1 +fi + +VERBOSE="${1:-}" +PASS=0; FAIL=0; ERRORS="" +TMPFILE=$(mktemp); trap "rm -f $TMPFILE" EXIT + +PRELUDE='KM = <<1,2,3,4>>, KS = [{key_id,k1},{algorithm,ed25519},{value,KM}], AS = [{public_keys,[[{id,k1},{created,0},{value,KM}]]}], bootstrap:start(alice, KS, AS),' + +cat > "$TMPFILE" < length(registry:list(K)) end, registry:kinds()), lists:foldl(fun (X, A) -> X + A end, 0, L)\")") + +;; nx_kernel fresh log_tip = 0 +(epoch 25) +(eval "(erlang-eval-ast \"${PRELUDE} nx_kernel:log_tip()\")") + +;; nx_kernel publish advances log_tip +(epoch 26) +(eval "(erlang-eval-ast \"${PRELUDE} nx_kernel:publish([{type, create}, {object, nil}]), nx_kernel:log_tip()\")") + +;; nx_kernel state carries the supplied actor_id +(epoch 27) +(eval "(get (erlang-eval-ast \"${PRELUDE} nx_kernel:actor_id(nx_kernel:query()) =:= alice\") :name)") + +;; Registry lookup works after start (canonical entry: Create) +(epoch 28) +(eval "(get (erlang-eval-ast \"${PRELUDE} case registry:lookup(activity_types, <<99,114,101,97,116,101>>) of {ok, _} -> ok; _ -> bad end\") :name)") +EPOCHS + +OUTPUT=$(timeout 300 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) + +check() { + local epoch="$1" desc="$2" expected="$3" + local actual + actual=$(echo "$OUTPUT" | awk -v e="$epoch" ' + $0 ~ "^\\(ok-len " e " " { getline; print; exit } + $0 ~ "^\\(ok " e " " { print; exit } + $0 ~ "^\\(error " e " " { print; exit } + ') + [ -z "$actual" ] && actual="" + if echo "$actual" | grep -qF -- "$expected"; then + PASS=$((PASS+1)) + [ "$VERBOSE" = "-v" ] && echo " ok $desc" + else + FAIL=$((FAIL+1)) + ERRORS+=" FAIL [$desc] (epoch $epoch) expected: $expected | actual: $actual +" + fi +} + +check 10 "bootstrap module loaded" "bootstrap" +check 20 "whereis(nx_kernel) is Pid" "true" +check 21 "activity_types count = 3" "3" +check 22 "object_types count = 10" "10" +check 23 "projections count = 7" "7" +check 24 "total entries = 31" "31" +check 25 "fresh log_tip = 0" "0" +check 26 "publish advances tip to 1" "1" +check 27 "actor_id = alice" "true" +check 28 "registry has create" "ok" + +TOTAL=$((PASS+FAIL)) +if [ $FAIL -eq 0 ]; then + echo "ok $PASS/$TOTAL next/tests/bootstrap_start.sh passed" +else + echo "FAIL $PASS/$TOTAL passed, $FAIL failed:" + echo "$ERRORS" +fi +[ $FAIL -eq 0 ] diff --git a/plans/fed-sx-milestone-1.md b/plans/fed-sx-milestone-1.md index de78a0ec..7a35a1b0 100644 --- a/plans/fed-sx-milestone-1.md +++ b/plans/fed-sx-milestone-1.md @@ -253,6 +253,7 @@ replay(LogState, InitAcc, Fun) -> ... - [x] **4c** — `bootstrap:read_genesis/0,1` + `read_section/2` + `sections/0` + `section_subdir/1` + `ends_with_sx/1` in Erlang: walk seven hardcoded section subdirs, filter `.sx` files via byte-pattern suffix match, read each into a binary. Returns `{ok, [{Section, [{Name, Bytes}, ...]}, ...]}`. Skips SX parsing — the substrate has no in-Erlang binary→SX-term path (same gap as Step 3b); bundle CID over raw bytes is enough for Step 4d. `next/tests/bootstrap_read.sh` (15 cases). - [x] **4d** — `bootstrap:build_genesis/1` + `verify_genesis/2` + `cidhash_path/1` + `write_cidhash/2` + `read_cidhash/1`: bundle CID via host `cid:to_string` over `{genesis_bundle, Sections}`; mismatch returns `{error, {cid_mismatch, Got, Expected}}`; `.cidhash` sibling file persists between runs. `next/tests/bootstrap_build.sh` (12 cases). - [x] **4e** — `bootstrap:load_genesis/1` + `strip_sx_suffix/1`: bridges `read_genesis` output into `registry` entries. Section atom = registry kind; entry name = filename minus `.sx` (binary); entry value = raw file bytes (parsed forms replace these once an SX-parser bridge exists). `next/tests/bootstrap_load.sh` (15 cases). +- [x] **4f-consolidate** — `bootstrap:start/3(ActorId, KeySpec, ActorState)` — one-call bring-up: `registry:start_link/0` → `populate_registry/0` → `nx_kernel:start_link/3`. Returns the kernel Pid. `next/tests/bootstrap_start.sh` (10 cases). **Deliverables:** @@ -1002,6 +1003,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-28** — Step 4f-consolidate: `bootstrap:start/3(ActorId, KeySpec, ActorState)` brings up the full kernel substrate in one call — starts the registry gen_server, populates it from the canonical genesis bundle (31 entries across 7 kinds), then starts nx_kernel. Returns the kernel Pid (gen_server convention in this port returns raw Pid not `{ok, Pid}`). Tests verify whereis(nx_kernel), per-kind counts (3/10/7/3/3/2/3), registry lookup of a known entry (`create`), publish + log_tip advance. `next/tests/bootstrap_start.sh` 10/10. Erlang conformance 729/729. - **2026-05-28** — Step 7d-pure: `next/kernel/sandbox.erl` — `eval_pure/2(Fun, Arg)` and `eval_pure/3(Fun, Activity, State)`. try/catch envelope returns `{ok, Result}` on success and `{error, {Class, Reason}}` for each of the three exception classes (throw, error, exit). The 3-arity variant matches the projection-fold shape so the scheduler can wrap fold bodies. Port note: this Erlang implementation catches by explicit class names rather than the open `Class:Reason` pattern — wrappers enumerate `throw:Reason / error:Reason / exit:Reason` explicitly. Real gas budget + IO denial + env-stripping lands with SX-source eval; the wrapper API doesn't change. `next/tests/sandbox_eval.sh` 13/13. Erlang conformance 729/729. - **2026-05-28** — Step 9b-pure: **reactive application extensibility, proven end-to-end.** Mirrors §Step 9b structurally without TCP/curl/JSON. A trigger projection (Erlang-fun fold over `{Captured, Count}` state) matches Note activities whose `:object :tags` contains `smoketest`, constructs a derived `TestEcho` activity with `:object :echoes` pointing at the Note's `:id`, and captures it into projection state. Order-independent; non-Note + non-smoketest + Note-without-tags + sig-failed publishes all suppressed correctly. Multi-tag (e.g. `[smoketest, foo, bar]`) still matches. Cascade publish (the trigger actually publishing the derived activity back through outbox) is deferred — the gen_server reentrancy that introduces is a v2 concern; the projection-state capture is sufficient proof of the match-then-derive mechanism. `next/tests/smoke_app_pure.sh` 12/12. Erlang conformance 729/729. - **2026-05-28** — Step 9a-pure: **the first verb-extensibility smoke test, proven end-to-end.** Mirrors §Step 9a structurally without TCP/curl/JSON. Two projections wired into `nx_kernel:with_projections([define_reg, pin_state])` — `define_reg` uses `define_registry:fold_fn/0` (Step 5d-pure), `pin_state` uses an Erlang-fun fold that records `{Path, Cid}` from Pin activities. Publish `Create{DefineActivity{name: pin}}` → registry update visible via `registry:lookup(activity_types, pin, projection:query(define_reg))`; publish `Pin{path: docs_intro, cid: qm_cid_1}` → `projection:query(pin_state) =:= [{docs_intro, qm_cid_1}]`. Order-independent (DefineActivity-then-Pin and Pin-then-DefineActivity both succeed); Note + non-Define types are pass-throughs in both projections. The TCP/curl variant (Step 9a-tcp) layers on Step 8b-start. `next/tests/smoke_pin_pure.sh` 13/13. Erlang conformance 729/729.