fed-sx-m2: Step 2b — actor_state projection fold + 19 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 28s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 28s
next/kernel/actor_state.erl mirrors define_registry's structure: a
2-arity fold_fn that plugs into projection:start_link/3, an
Erlang-fun stand-in for the genesis actor-state.sx projection body.
State shape:
[{ActorId, Profile}, ...]
Profile is a property list with :type, :name, :preferredUsername,
:summary, :icon, :public_keys, :moved_to, :created. Maps #{} aren't
registered in this substrate, so this matches the kernel bucket /
registry shape convention.
Folding rules per design §9.1-§9.4:
- Create{Person|Service|Group}: register profile, capturing object
fields + :published seq as :created. Duplicate Create no-overwrite.
- Update{Person|Service|Group, patch}: deep-merge :patch into
profile last-write-wins per key.
- Move: record :moved_to.
Other activity types and non-actor object Creates pass through.
Local find_keyed/has_keyed/set_keyed helpers (same gap as Step 1a:
no lists:keyfind/keymember in this substrate).
19/19 in next/tests/actor_state_pure.sh covering:
- new/0/has/2/lookup/2/actors/1 base cases
- Create for Person/Service/Group all three actor types
- Profile field capture (name, preferredUsername, public_keys, created)
- Duplicate Create no-overwrite
- Two independent actors
- Update field merge + per-key last-write-wins
- Update for unknown actor pass-through
- Move :moved_to
- Non-actor Creates pass through
- Activities without :actor pass through
- fold_fn/0 returns is_function(F, 2)
Conformance 761/761. Step-2-adjacent no-regression gate 106/106
across 6 suites (define_registry_pure, projection_pure,
projection_server, nx_kernel_multi, bootstrap_start, smoke_app_pure).
This commit is contained in:
178
next/kernel/actor_state.erl
Normal file
178
next/kernel/actor_state.erl
Normal file
@@ -0,0 +1,178 @@
|
||||
-module(actor_state).
|
||||
-export([fold/2, fold_fn/0, new/0, lookup/2, has/2, actors/1,
|
||||
profile_type/1, profile_name/1, profile_field/2]).
|
||||
|
||||
%% Actor-state projection fold — Erlang-fun stand-in for the
|
||||
%% genesis `actor-state.sx` projection body. Tracks per-actor
|
||||
%% profiles, key-history, and Move pointers per design §9.1-§9.4.
|
||||
%%
|
||||
%% State shape:
|
||||
%% [{ActorId, Profile}, ...]
|
||||
%%
|
||||
%% Profile = [{type, person|service|group},
|
||||
%% {name, Bin},
|
||||
%% {preferredUsername, Bin},
|
||||
%% {summary, Bin},
|
||||
%% {icon, Bin},
|
||||
%% {public_keys, [Key]},
|
||||
%% {moved_to, ActorIdOrUrl},
|
||||
%% {created, N}]
|
||||
%%
|
||||
%% Bridge note: the SX-source eval bridge would replace this fold
|
||||
%% body once available (same gap as Step 5d-pure / Step 6c-schema-pure).
|
||||
%% define_registry.erl is the structural twin.
|
||||
%%
|
||||
%% lists:keyfind/keymember aren't in this substrate (Step 1a noted
|
||||
%% same gap), so local `find_keyed`/`has_keyed`/`set_keyed` helpers
|
||||
%% handle the keyed-list ops.
|
||||
|
||||
new() -> [].
|
||||
|
||||
actors(State) -> [Id || {Id, _Profile} <- State].
|
||||
|
||||
has(ActorId, State) -> has_keyed(ActorId, State).
|
||||
|
||||
lookup(ActorId, State) ->
|
||||
case find_keyed(ActorId, State) of
|
||||
{ok, Profile} -> {ok, Profile};
|
||||
{error, _} -> not_found
|
||||
end.
|
||||
|
||||
%% ── Fold dispatch ───────────────────────────────────────────────
|
||||
|
||||
fold(Activity, State) ->
|
||||
case envelope:get_field(type, Activity) of
|
||||
{ok, create} -> fold_create(Activity, State);
|
||||
{ok, update} -> fold_update(Activity, State);
|
||||
{ok, move} -> fold_move(Activity, State);
|
||||
_ -> State
|
||||
end.
|
||||
|
||||
fold_create(Activity, State) ->
|
||||
case envelope:get_field(object, Activity) of
|
||||
{ok, Obj} ->
|
||||
case envelope:get_field(type, Obj) of
|
||||
{ok, ObjType} ->
|
||||
case is_actor_type(ObjType) of
|
||||
true -> register_actor(Activity, Obj, ObjType, State);
|
||||
false -> State
|
||||
end;
|
||||
_ -> State
|
||||
end;
|
||||
_ -> State
|
||||
end.
|
||||
|
||||
register_actor(Activity, Obj, ObjType, State) ->
|
||||
case envelope:get_field(actor, Activity) of
|
||||
{ok, ActorId} ->
|
||||
case has_keyed(ActorId, State) of
|
||||
true ->
|
||||
State;
|
||||
false ->
|
||||
Created = published_seq(Activity),
|
||||
Profile = build_profile(ObjType, Obj, Created),
|
||||
State ++ [{ActorId, Profile}]
|
||||
end;
|
||||
_ -> State
|
||||
end.
|
||||
|
||||
fold_update(Activity, State) ->
|
||||
case envelope:get_field(actor, Activity) of
|
||||
{ok, ActorId} ->
|
||||
case find_keyed(ActorId, State) of
|
||||
{ok, Profile} ->
|
||||
case envelope:get_field(patch, Activity) of
|
||||
{ok, Patch} ->
|
||||
NewProfile = merge_patch(Profile, Patch),
|
||||
set_keyed(ActorId, NewProfile, State);
|
||||
_ -> State
|
||||
end;
|
||||
_ -> State
|
||||
end;
|
||||
_ -> State
|
||||
end.
|
||||
|
||||
fold_move(Activity, State) ->
|
||||
case envelope:get_field(actor, Activity) of
|
||||
{ok, ActorId} ->
|
||||
case find_keyed(ActorId, State) of
|
||||
{ok, Profile} ->
|
||||
case envelope:get_field(moved_to, Activity) of
|
||||
{ok, Target} ->
|
||||
NewProfile = set_keyed(moved_to, Target, Profile),
|
||||
set_keyed(ActorId, NewProfile, State);
|
||||
_ -> State
|
||||
end;
|
||||
_ -> State
|
||||
end;
|
||||
_ -> State
|
||||
end.
|
||||
|
||||
%% ── Profile assembly ────────────────────────────────────────────
|
||||
|
||||
build_profile(ObjType, Obj, Created) ->
|
||||
Base = [{type, ObjType}, {created, Created}],
|
||||
Fields = [name, preferredUsername, summary, icon, public_keys],
|
||||
Base ++ collect_fields(Fields, Obj).
|
||||
|
||||
collect_fields([], _) -> [];
|
||||
collect_fields([F | Rest], Obj) ->
|
||||
case envelope:get_field(F, Obj) of
|
||||
{ok, V} -> [{F, V} | collect_fields(Rest, Obj)];
|
||||
_ -> collect_fields(Rest, Obj)
|
||||
end.
|
||||
|
||||
merge_patch(Profile, []) -> Profile;
|
||||
merge_patch(Profile, [{K, V} | Rest]) ->
|
||||
merge_patch(set_keyed(K, V, Profile), Rest);
|
||||
merge_patch(Profile, _) -> Profile.
|
||||
|
||||
published_seq(Activity) ->
|
||||
case envelope:get_field(published, Activity) of
|
||||
{ok, P} -> P;
|
||||
_ -> 0
|
||||
end.
|
||||
|
||||
is_actor_type(person) -> true;
|
||||
is_actor_type(service) -> true;
|
||||
is_actor_type(group) -> true;
|
||||
is_actor_type(_) -> false.
|
||||
|
||||
%% ── Profile accessors ───────────────────────────────────────────
|
||||
|
||||
profile_type(Profile) ->
|
||||
case find_keyed(type, Profile) of
|
||||
{ok, T} -> T;
|
||||
_ -> nil
|
||||
end.
|
||||
|
||||
profile_name(Profile) ->
|
||||
case find_keyed(name, Profile) of
|
||||
{ok, N} -> N;
|
||||
_ -> nil
|
||||
end.
|
||||
|
||||
profile_field(F, Profile) ->
|
||||
case find_keyed(F, Profile) of
|
||||
{ok, V} -> {ok, V};
|
||||
_ -> not_found
|
||||
end.
|
||||
|
||||
%% ── Projection integration ──────────────────────────────────────
|
||||
|
||||
fold_fn() ->
|
||||
fun (Activity, State) -> fold(Activity, State) end.
|
||||
|
||||
%% ── Internal ────────────────────────────────────────────────────
|
||||
|
||||
has_keyed(_, []) -> false;
|
||||
has_keyed(K, [{K, _} | _]) -> true;
|
||||
has_keyed(K, [_ | Rest]) -> has_keyed(K, Rest).
|
||||
|
||||
find_keyed(_, []) -> {error, not_found};
|
||||
find_keyed(K, [{K, V} | _]) -> {ok, V};
|
||||
find_keyed(K, [_ | Rest]) -> find_keyed(K, Rest).
|
||||
|
||||
set_keyed(K, V, []) -> [{K, V}];
|
||||
set_keyed(K, V, [{K, _} | Rest]) -> [{K, V} | Rest];
|
||||
set_keyed(K, V, [P | Rest]) -> [P | set_keyed(K, V, Rest)].
|
||||
163
next/tests/actor_state_pure.sh
Executable file
163
next/tests/actor_state_pure.sh
Executable file
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env bash
|
||||
# next/tests/actor_state_pure.sh — m2 Step 2b test.
|
||||
#
|
||||
# Exercises the Erlang-fun stand-in for the actor-state projection
|
||||
# fold. Activities flow:
|
||||
# Create{Person|Service|Group} -> profile registered
|
||||
# Update{Person|Service|Group, patch} -> patch deep-merged
|
||||
# Move -> :moved_to recorded
|
||||
# Non-actor object Creates pass through.
|
||||
|
||||
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
|
||||
|
||||
cat > "$TMPFILE" <<'EPOCHS'
|
||||
(epoch 1)
|
||||
(load "lib/erlang/tokenizer.sx")
|
||||
(load "lib/erlang/parser.sx")
|
||||
(load "lib/erlang/parser-core.sx")
|
||||
(load "lib/erlang/parser-expr.sx")
|
||||
(load "lib/erlang/parser-module.sx")
|
||||
(load "lib/erlang/transpile.sx")
|
||||
(load "lib/erlang/runtime.sx")
|
||||
(load "lib/erlang/vm/dispatcher.sx")
|
||||
(epoch 2)
|
||||
(eval "(get (erlang-load-module (file-read \"next/kernel/envelope.erl\")) :name)")
|
||||
(epoch 3)
|
||||
(eval "(get (erlang-load-module (file-read \"next/kernel/actor_state.erl\")) :name)")
|
||||
|
||||
;; new/0 returns []
|
||||
(epoch 10)
|
||||
(eval "(get (erlang-eval-ast \"actor_state:new() =:= []\") :name)")
|
||||
|
||||
;; has/2 false on empty
|
||||
(epoch 11)
|
||||
(eval "(get (erlang-eval-ast \"actor_state:has(alice, actor_state:new()) =:= false\") :name)")
|
||||
|
||||
;; lookup/2 not_found on empty
|
||||
(epoch 12)
|
||||
(eval "(get (erlang-eval-ast \"actor_state:lookup(alice, actor_state:new()) =:= not_found\") :name)")
|
||||
|
||||
;; actors/1 returns [] on empty
|
||||
(epoch 13)
|
||||
(eval "(get (erlang-eval-ast \"actor_state:actors(actor_state:new()) =:= []\") :name)")
|
||||
|
||||
;; Create{Person} registers profile
|
||||
(epoch 14)
|
||||
(eval "(get (erlang-eval-ast \"Obj = [{type, person}, {name, alice_name}, {preferredUsername, alice_local}], Act = [{actor, alice}, {type, create}, {object, Obj}, {published, 1}], S = actor_state:fold(Act, actor_state:new()), actor_state:has(alice, S)\") :name)")
|
||||
|
||||
;; Profile carries :type, :name, :preferredUsername, :created
|
||||
(epoch 15)
|
||||
(eval "(get (erlang-eval-ast \"Obj = [{type, person}, {name, alice_name}, {preferredUsername, alice_local}], Act = [{actor, alice}, {type, create}, {object, Obj}, {published, 7}], S = actor_state:fold(Act, actor_state:new()), {ok, P} = actor_state:lookup(alice, S), {actor_state:profile_type(P), actor_state:profile_name(P), actor_state:profile_field(preferredUsername, P), actor_state:profile_field(created, P)} =:= {person, alice_name, {ok, alice_local}, {ok, 7}}\") :name)")
|
||||
|
||||
;; Create{Service} also registers
|
||||
(epoch 16)
|
||||
(eval "(get (erlang-eval-ast \"Obj = [{type, service}, {name, feedbot}], Act = [{actor, feed1}, {type, create}, {object, Obj}, {published, 1}], S = actor_state:fold(Act, actor_state:new()), {ok, P} = actor_state:lookup(feed1, S), actor_state:profile_type(P) =:= service\") :name)")
|
||||
|
||||
;; Create{Group} also registers
|
||||
(epoch 17)
|
||||
(eval "(get (erlang-eval-ast \"Obj = [{type, group}, {name, working_group}], Act = [{actor, wg1}, {type, create}, {object, Obj}, {published, 1}], S = actor_state:fold(Act, actor_state:new()), {ok, P} = actor_state:lookup(wg1, S), actor_state:profile_type(P) =:= group\") :name)")
|
||||
|
||||
;; Create{Note} is pass-through (non-actor object)
|
||||
(epoch 18)
|
||||
(eval "(get (erlang-eval-ast \"Obj = [{type, note}, {content, hi}], Act = [{actor, alice}, {type, create}, {object, Obj}, {published, 1}], actor_state:fold(Act, actor_state:new()) =:= []\") :name)")
|
||||
|
||||
;; Duplicate Create doesn't overwrite an existing profile
|
||||
(epoch 19)
|
||||
(eval "(get (erlang-eval-ast \"O1 = [{type, person}, {name, alice_v1}], O2 = [{type, person}, {name, alice_v2}], A1 = [{actor, alice}, {type, create}, {object, O1}, {published, 1}], A2 = [{actor, alice}, {type, create}, {object, O2}, {published, 2}], S1 = actor_state:fold(A1, actor_state:new()), S2 = actor_state:fold(A2, S1), {ok, P} = actor_state:lookup(alice, S2), actor_state:profile_name(P) =:= alice_v1\") :name)")
|
||||
|
||||
;; Two distinct actors live side by side
|
||||
(epoch 20)
|
||||
(eval "(get (erlang-eval-ast \"PO = [{type, person}, {name, alice_n}], SO = [{type, service}, {name, bobbot_n}], A1 = [{actor, alice}, {type, create}, {object, PO}, {published, 1}], A2 = [{actor, bobbot}, {type, create}, {object, SO}, {published, 2}], S = actor_state:fold(A2, actor_state:fold(A1, actor_state:new())), actor_state:actors(S) =:= [alice, bobbot]\") :name)")
|
||||
|
||||
;; Update merges patch
|
||||
(epoch 21)
|
||||
(eval "(get (erlang-eval-ast \"PO = [{type, person}, {name, alice_n}], A1 = [{actor, alice}, {type, create}, {object, PO}, {published, 1}], A2 = [{actor, alice}, {type, update}, {patch, [{summary, new_bio}]}, {published, 2}], S = actor_state:fold(A2, actor_state:fold(A1, actor_state:new())), {ok, P} = actor_state:lookup(alice, S), actor_state:profile_field(summary, P) =:= {ok, new_bio}\") :name)")
|
||||
|
||||
;; Update overwrites individual fields (last-write-wins per key)
|
||||
(epoch 22)
|
||||
(eval "(get (erlang-eval-ast \"PO = [{type, person}, {name, alice_v1}], A1 = [{actor, alice}, {type, create}, {object, PO}, {published, 1}], A2 = [{actor, alice}, {type, update}, {patch, [{name, alice_v2}]}, {published, 2}], S = actor_state:fold(A2, actor_state:fold(A1, actor_state:new())), {ok, P} = actor_state:lookup(alice, S), actor_state:profile_name(P) =:= alice_v2\") :name)")
|
||||
|
||||
;; Update for unknown actor is pass-through
|
||||
(epoch 23)
|
||||
(eval "(get (erlang-eval-ast \"A = [{actor, ghost}, {type, update}, {patch, [{summary, x}]}, {published, 1}], actor_state:fold(A, actor_state:new()) =:= []\") :name)")
|
||||
|
||||
;; Move records :moved_to
|
||||
(epoch 24)
|
||||
(eval "(get (erlang-eval-ast \"PO = [{type, person}, {name, alice_n}], A1 = [{actor, alice}, {type, create}, {object, PO}, {published, 1}], A2 = [{actor, alice}, {type, move}, {moved_to, new_alice}, {published, 2}], S = actor_state:fold(A2, actor_state:fold(A1, actor_state:new())), {ok, P} = actor_state:lookup(alice, S), actor_state:profile_field(moved_to, P) =:= {ok, new_alice}\") :name)")
|
||||
|
||||
;; fold_fn/0 is a 2-arity Erlang fun usable by projection:start_link
|
||||
(epoch 25)
|
||||
(eval "(get (erlang-eval-ast \"F = actor_state:fold_fn(), is_function(F, 2)\") :name)")
|
||||
|
||||
;; fold ignores activities with no :actor field
|
||||
(epoch 26)
|
||||
(eval "(get (erlang-eval-ast \"Obj = [{type, person}, {name, x}], Act = [{type, create}, {object, Obj}, {published, 1}], actor_state:fold(Act, actor_state:new()) =:= []\") :name)")
|
||||
|
||||
;; public_keys field is captured at Create time
|
||||
(epoch 27)
|
||||
(eval "(get (erlang-eval-ast \"Keys = [[{id, k1}, {value, <<1,2,3,4>>}]], Obj = [{type, person}, {name, alice_n}, {public_keys, Keys}], Act = [{actor, alice}, {type, create}, {object, Obj}, {published, 1}], S = actor_state:fold(Act, actor_state:new()), {ok, P} = actor_state:lookup(alice, S), actor_state:profile_field(public_keys, P) =:= {ok, Keys}\") :name)")
|
||||
EPOCHS
|
||||
|
||||
OUTPUT=$(timeout 240 "$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="<no output for epoch $epoch>"
|
||||
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 3 "actor_state module loaded" "actor_state"
|
||||
check 10 "new/0 -> []" "true"
|
||||
check 11 "has/2 false on empty" "true"
|
||||
check 12 "lookup/2 not_found on empty" "true"
|
||||
check 13 "actors/1 [] on empty" "true"
|
||||
check 14 "Create{Person} registers actor" "true"
|
||||
check 15 "Profile carries type/name/created" "true"
|
||||
check 16 "Create{Service} registers actor" "true"
|
||||
check 17 "Create{Group} registers actor" "true"
|
||||
check 18 "Create{Note} pass-through" "true"
|
||||
check 19 "Duplicate Create no-overwrite" "true"
|
||||
check 20 "Two actors side by side" "true"
|
||||
check 21 "Update merges new fields" "true"
|
||||
check 22 "Update last-write-wins per key" "true"
|
||||
check 23 "Update unknown actor pass-through" "true"
|
||||
check 24 "Move records :moved_to" "true"
|
||||
check 25 "fold_fn/0 is fun/2" "true"
|
||||
check 26 "Activity sans :actor pass-through" "true"
|
||||
check 27 "public_keys captured at Create" "true"
|
||||
|
||||
TOTAL=$((PASS+FAIL))
|
||||
if [ $FAIL -eq 0 ]; then
|
||||
echo "ok $PASS/$TOTAL next/tests/actor_state_pure.sh passed"
|
||||
else
|
||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
||||
echo "$ERRORS"
|
||||
fi
|
||||
[ $FAIL -eq 0 ]
|
||||
@@ -169,16 +169,25 @@ publicKey rotation history, profile fields, follower counts, etc.
|
||||
`bootstrap_load.sh` 15/15, `bootstrap_populate.sh` 14/14,
|
||||
`bootstrap_start.sh` 10/10). `bootstrap_build.sh` 12/12 picks
|
||||
up the new bundle CID dynamically.
|
||||
- [ ] **2b** — Actor-state projection fold (Erlang-fun stand-in,
|
||||
mirrors Step 5d-pure's `define_registry`):
|
||||
- On `Create{Person|Service|Group}`: register the actor's profile
|
||||
in `{ActorId => #{type, name, preferredUsername, summary, icon,
|
||||
public_keys, created}}`.
|
||||
- On `Update{Person|Service|Group, patch}`: deep-merge the patch.
|
||||
- On `Move`: record `:movedTo` pointer.
|
||||
- `next/kernel/actor_state.erl` with `fold_fn/0` plugging into
|
||||
`projection:start_link/3`. Pure-functional + gen_server-bridged
|
||||
tests as a single `actor_state_pure.sh`.
|
||||
- [x] **2b** — Actor-state projection fold (Erlang-fun stand-in,
|
||||
mirrors Step 5d-pure's `define_registry`). `next/kernel/actor_state.erl`
|
||||
with state shape `[{ActorId, Profile}, ...]` where `Profile` is a
|
||||
proplist with `:type / :name / :preferredUsername / :summary /
|
||||
:icon / :public_keys / :moved_to / :created`. Maps `#{}` aren't
|
||||
registered in the substrate, so the profile is a property list
|
||||
(same shape choice as the kernel's bucket / registry state).
|
||||
Folding rules:
|
||||
- `Create{Person|Service|Group}` (from a known `:actor`):
|
||||
captures profile fields + `:created` (=`:published` seq).
|
||||
Duplicate Creates are no-overwrite.
|
||||
- `Update{Person|Service|Group, patch}`: merges `:patch` into the
|
||||
profile last-write-wins per key.
|
||||
- `Move`: records `:moved_to` on the profile.
|
||||
Other activity types and non-actor object Creates pass through.
|
||||
`fold_fn/0` plugs into `projection:start_link/3`. Local
|
||||
`find_keyed/has_keyed/set_keyed` helpers (same gap as 1a — no
|
||||
`lists:keyfind`/`keymember` in the substrate). 19 cases in
|
||||
`actor_state_pure.sh`.
|
||||
- [ ] **2c** — `nx_kernel:bootstrap_actor/4(ActorId, Profile,
|
||||
KeySpec, State)` — publishes `Create{Person{...}}` as the actor's
|
||||
first activity, exercising the full pipeline. Integration test
|
||||
@@ -691,6 +700,18 @@ proceed.
|
||||
|
||||
Newest first.
|
||||
|
||||
- **2026-06-06** — Step 2b: actor-state projection Erlang module.
|
||||
New `next/kernel/actor_state.erl` with `fold/2` over Create / Update
|
||||
/ Move activities. Profile is a property list of `:type / :name /
|
||||
:preferredUsername / :summary / :icon / :public_keys / :moved_to /
|
||||
:created`. Create captures fields and `:published` as `:created`;
|
||||
duplicate Create is no-overwrite; non-actor Creates and `:actor`-
|
||||
less envelopes pass through. Update last-write-wins per patch key.
|
||||
Move records `:moved_to`. `fold_fn/0` is a 2-arity Erlang fun for
|
||||
`projection:start_link/3` (structural twin of `define_registry`).
|
||||
`next/tests/actor_state_pure.sh` 19/19. Conformance 761/761.
|
||||
Step-2-adjacent no-regression gate 106/106 across 6 suites.
|
||||
|
||||
- **2026-06-06** — Step 2a: genesis Person/Service/Group object-
|
||||
types. Three new SX files in `next/genesis/object-types/` with
|
||||
the same shape as `note.sx` / `sx-artifact.sx` (`:name`, `:doc`,
|
||||
|
||||
Reference in New Issue
Block a user