-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, key_history/1, active_keys_at/2, find_key_by_id/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} -> Published = published_seq(Activity), NewProfile = apply_patch(Profile, Patch, Published), 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. %% apply_patch/3 — same as merge_patch but special-cases two %% key-rotation patch entries per design §9.6: %% {add_publicKey, KeyProplist} — append a new key to :public_keys, %% defaulting :created to Published. %% {supersede, OldKeyId} — mark the key with :id =:= OldKeyId %% as :superseded_at = Published. %% Other patch entries fall through to last-write-wins per key. apply_patch(Profile, [], _Published) -> Profile; apply_patch(Profile, [{add_publicKey, NewKey} | Rest], Published) -> Augmented = ensure_created(NewKey, Published), Current = current_public_keys(Profile), NewKeys = Current ++ [Augmented], apply_patch(set_keyed(public_keys, NewKeys, Profile), Rest, Published); apply_patch(Profile, [{supersede, OldKeyId} | Rest], Published) -> Current = current_public_keys(Profile), NewKeys = mark_superseded(OldKeyId, Published, Current), apply_patch(set_keyed(public_keys, NewKeys, Profile), Rest, Published); apply_patch(Profile, [{K, V} | Rest], Published) -> apply_patch(set_keyed(K, V, Profile), Rest, Published); apply_patch(Profile, _, _) -> Profile. current_public_keys(Profile) -> case find_keyed(public_keys, Profile) of {ok, Keys} -> Keys; _ -> [] end. ensure_created(Key, Published) -> case find_keyed(created, Key) of {ok, _} -> Key; _ -> set_keyed(created, Published, Key) end. mark_superseded(_, _, []) -> []; mark_superseded(OldId, At, [Key | Rest]) -> case find_keyed(id, Key) of {ok, OldId} -> case find_keyed(superseded_at, Key) of {ok, _} -> [Key | mark_superseded(OldId, At, Rest)]; _ -> [set_keyed(superseded_at, At, Key) | mark_superseded(OldId, At, Rest)] end; _ -> [Key | mark_superseded(OldId, At, Rest)] end. %% Key-history view — full :public_keys list including superseded %% entries (per §9.6: history is preserved so historical activities %% verify against keys that were active at their :published time). key_history(Profile) -> current_public_keys(Profile). %% active_keys_at/2 — the subset of :public_keys active at Now, %% mirroring envelope's is_active_at semantics (local copy: envelope %% keeps the predicate private). active_keys_at(Profile, Now) -> [K || K <- current_public_keys(Profile), key_active_at(K, Now)]. find_key_by_id(KeyId, Profile) -> find_key_by_id_in(KeyId, current_public_keys(Profile)). find_key_by_id_in(_, []) -> not_found; find_key_by_id_in(WantId, [K | Rest]) -> case find_keyed(id, K) of {ok, WantId} -> {ok, K}; _ -> find_key_by_id_in(WantId, Rest) end. key_active_at(Key, Now) -> case find_keyed(created, Key) of {ok, Created} when Now >= Created -> case find_keyed(superseded_at, Key) of {ok, SupAt} -> Now < SupAt; _ -> true end; _ -> false end. 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)].