-module(log). -export([open/2, append/2, tip/1, replay/3, entries/1]). %% Per-actor activity log โ€” the canonical record of everything an %% actor has emitted, in chronological order. Per design ยง15.2 this %% lives on disk as a JSONL segment file; v1 starts with an in-memory %% backend so the API and seq-number machinery can be locked down %% before the on-disk format is added (Step 3b). %% %% State shape (a property list): %% [{actor, ActorId}, {base, BasePath}, {seq, NextSeq}, {entries, [Act|...]}] %% %% `entries` stores activities in append order โ€” i.e. oldest first. %% `seq` is the next sequence number that will be assigned by append. %% `base` is kept on the state for forward-compatibility with 3b %% (where it becomes the segment-file directory). %% %% open/2 takes ActorId + BasePath and returns {ok, LogState} starting %% with seq=0 and no entries. %% %% append/2 returns {ok, NewLogState, AssignedSeq}. %% %% tip/1 returns the next seq the log would assign (== count of entries). %% %% replay/3 folds Fun(Activity, AssignedSeq, Acc) over every entry in %% append order. Three-arity rather than two-arity because the plan's %% example test is "sequence numbers gap-free across replay" โ€” having %% the seq number visible in the fold makes that test direct. %% %% entries/1 is a debug accessor returning [Activity, ...] in append %% order. Not part of the public API contract. open(ActorId, BasePath) -> {ok, [{actor, ActorId}, {base, BasePath}, {seq, 0}, {entries, []}]}. append(LogState, Activity) -> Seq = field(seq, LogState), Entries = field(entries, LogState), NewState = replace_field(seq, Seq + 1, replace_field(entries, Entries ++ [Activity], LogState)), {ok, NewState, Seq}. tip(LogState) -> field(seq, LogState). replay(LogState, InitAcc, Fun) -> Entries = field(entries, LogState), replay_loop(Entries, 0, InitAcc, Fun). replay_loop([], _, Acc, _) -> Acc; replay_loop([Act | Rest], Seq, Acc, Fun) -> replay_loop(Rest, Seq + 1, Fun(Act, Seq, Acc), Fun). entries(LogState) -> field(entries, LogState). field(K, [{K, V} | _]) -> V; field(K, [_ | Rest]) -> field(K, Rest); field(_, []) -> erlang:error(badkey). replace_field(K, V, []) -> [{K, V}]; replace_field(K, V, [{K, _} | Rest]) -> [{K, V} | Rest]; replace_field(K, V, [P | Rest]) -> [P | replace_field(K, V, Rest)].