Files
rose-ash/next/kernel/announce_state.erl
giles b2b61a0112
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 20s
fed-sx-m2: Step 11b — Announce + Endorse projection folds + 19 tests
Two new projection modules for the rich verbs landed in Step 11a:

  next/kernel/announce_state.erl
    Per-target-Cid announcer set.
    State: [{TargetCid, [AnnouncerActorId, ...]}, ...]
    Set semantics — duplicate Announce by the same actor on the
    same target is a no-op.

    Public API:
      new/0, fold/2, fold_fn/0
      announcers_for/2, announce_count/2, announced_cids/1
      has_announced/3

  next/kernel/endorsement_state.erl
    Per-target-Cid + per-kind + per-actor endorsement counter.
    State: [{TargetCid, [{Kind, [{ActorId, Count}, ...]}, ...]}, ...]
    Additive semantics — re-endorse by the same actor under the
    same kind bumps the counter. Undo{Endorse} retraction defers
    to a follow-up.

    Public API:
      new/0, fold/2, fold_fn/0
      counters_for/2, total_for/2, kinds_for/2
      endorsers_for/3, has_endorsed/4

Both fold_fn/0 returns a 2-arity Erlang fun for
projection:start_link/3 (same plug shape as actor_state /
follower_graph / delivery_state). Non-matching activity types
pass through unchanged.

Read-side accessors cover both enumeration (announcers_for,
endorsers_for) and predicates (has_announced, has_endorsed) so
the feed/timeline projection layer doesn't have to re-implement
that logic on every consumer.

19/19 in next/tests/rich_verbs.sh:

  announce_state:
    - new/0 -> []
    - Announce -> announcer added
    - Two announces same target -> both in set
    - Duplicate announce by same actor -> no-op
    - announce_count + announced_cids
    - has_announced predicate
    - fold_fn/0 is fun/2
    - Non-Announce activity passes through

  endorsement_state:
    - new/0 -> []
    - Endorse -> counter 1
    - Two likes by different actors -> total 2
    - like + share -> two kinds tracked
    - endorsers_for(Cid, Kind)
    - has_endorsed predicate
    - fold_fn/0 is fun/2
    - Non-Endorse activity passes through
    - Same actor endorsing twice -> total = 2 (additive)

Conformance preserved at 761/761.
2026-06-07 05:06:27 +00:00

80 lines
2.6 KiB
Erlang

-module(announce_state).
-export([new/0, fold/2, fold_fn/0,
announcers_for/2, announce_count/2, announced_cids/1,
has_announced/3]).
%% Announce-fanout projection. Folds Announce activities into a
%% per-target-Cid set of announcer ActorIds so projections can
%% answer "who re-broadcast this activity" / "how many announces
%% does this Note have" / "what activities has X announced".
%%
%% Announce envelope shape (per next/genesis/activity-types/announce.sx):
%% [{type, announce},
%% {actor, AnnouncerActorId},
%% {object, TargetCidBinary},
%% ...]
%%
%% State shape:
%% [{TargetCid, [Announcer1, Announcer2, ...]}, ...]
%%
%% Set semantics — the same actor announcing the same target twice
%% is a no-op (already in the list). Undo{Announce} retraction
%% defers to a follow-up.
new() -> [].
fold_fn() ->
fun (Activity, State) -> fold(Activity, State) end.
fold(Activity, State) ->
case envelope:get_field(type, Activity) of
{ok, announce} -> fold_announce(Activity, State);
_ -> State
end.
fold_announce(Activity, State) ->
case {envelope:get_field(actor, Activity),
envelope:get_field(object, Activity)} of
{{ok, Actor}, {ok, Cid}} -> add_announcer(Cid, Actor, State);
_ -> State
end.
add_announcer(Cid, Actor, State) ->
Current = case find_keyed(Cid, State) of
{ok, Set} -> Set;
_ -> []
end,
case contains(Actor, Current) of
true -> State;
false -> set_keyed(Cid, Current ++ [Actor], State)
end.
%% ── Read-side accessors ───────────────────────────────────────
announcers_for(Cid, State) ->
case find_keyed(Cid, State) of
{ok, Set} -> Set;
_ -> []
end.
announce_count(Cid, State) -> length(announcers_for(Cid, State)).
announced_cids(State) -> [C || {C, _} <- State].
has_announced(Actor, Cid, State) ->
contains(Actor, announcers_for(Cid, State)).
%% ── Internal ──────────────────────────────────────────────────
contains(_, []) -> false;
contains(X, [X | _]) -> true;
contains(X, [_ | Rest]) -> contains(X, 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)].