;; identity/membership.sx — coop membership state + per-app projection.
;;
;; Membership is canonical subject state held by one process, a guarded
;; state machine (invalid transitions are explicit errors, never silent
;; no-ops):
;;
;;   none --request--> pending --approve--> active
;;   active --lapse--> lapsed --reinstate--> active
;;   {pending|active|lapsed} --revoke--> revoked   (terminal)
;;
;; A per-app GRANT PROJECTION renders that one canonical state into the
;; view a given client app consumes — mirroring rose-ash's per-app grant
;; verification. The projection is pure identity: it reports WHAT the
;; subject's membership is for that app; it does NOT decide whether the
;; app should let them in. That permission question is acl's, keyed off
;; this projection.
;;
;;   project(Subject, App) ->
;;     active  -> {member, Tier, App}
;;     pending -> {pending, App}
;;     lapsed  -> {lapsed, App}
;;     revoked -> {denied, App}
;;     none    -> {non_member, App}

(define
  identity-membership-source
  "-module(identity_membership).\n\n   start() ->\n     spawn(fun () -> loop([]) end).\n\n   request(M, Subject, Tier) ->\n     M ! {request, Subject, Tier, self()},\n     receive {membership_reply, R} -> R end.\n\n   approve(M, Subject) ->\n     M ! {approve, Subject, self()},\n     receive {membership_reply, R} -> R end.\n\n   lapse(M, Subject) ->\n     M ! {lapse, Subject, self()},\n     receive {membership_reply, R} -> R end.\n\n   reinstate(M, Subject) ->\n     M ! {reinstate, Subject, self()},\n     receive {membership_reply, R} -> R end.\n\n   revoke(M, Subject) ->\n     M ! {revoke, Subject, self()},\n     receive {membership_reply, R} -> R end.\n\n   status(M, Subject) ->\n     M ! {status, Subject, self()},\n     receive {membership_reply, R} -> R end.\n\n   project(M, Subject, App) ->\n     M ! {project, Subject, App, self()},\n     receive {membership_reply, R} -> R end.\n\n   loop(Members) ->\n     receive\n       {request, Subject, Tier, From} ->\n         case find(Subject, Members) of\n           none ->\n             From ! {membership_reply, ok},\n             loop([{Subject, {pending, Tier}} | Members]);\n           {ok, _} ->\n             From ! {membership_reply, {error, exists}},\n             loop(Members)\n         end;\n       {approve, Subject, From} ->\n         case find(Subject, Members) of\n           none ->\n             From ! {membership_reply, {error, not_found}},\n             loop(Members);\n           {ok, {pending, Tier}} ->\n             From ! {membership_reply, ok},\n             loop(set_record(Subject, {active, Tier}, Members));\n           {ok, {St, _}} ->\n             From ! {membership_reply, {error, St}},\n             loop(Members)\n         end;\n       {lapse, Subject, From} ->\n         case find(Subject, Members) of\n           none ->\n             From ! {membership_reply, {error, not_found}},\n             loop(Members);\n           {ok, {active, Tier}} ->\n             From ! {membership_reply, ok},\n             loop(set_record(Subject, {lapsed, Tier}, Members));\n           {ok, {St, _}} ->\n             From ! {membership_reply, {error, St}},\n             loop(Members)\n         end;\n       {reinstate, Subject, From} ->\n         case find(Subject, Members) of\n           none ->\n             From ! {membership_reply, {error, not_found}},\n             loop(Members);\n           {ok, {lapsed, Tier}} ->\n             From ! {membership_reply, ok},\n             loop(set_record(Subject, {active, Tier}, Members));\n           {ok, {St, _}} ->\n             From ! {membership_reply, {error, St}},\n             loop(Members)\n         end;\n       {revoke, Subject, From} ->\n         case find(Subject, Members) of\n           none ->\n             From ! {membership_reply, {error, not_found}},\n             loop(Members);\n           {ok, {_, Tier}} ->\n             From ! {membership_reply, ok},\n             loop(set_record(Subject, {revoked, Tier}, Members))\n         end;\n       {status, Subject, From} ->\n         case find(Subject, Members) of\n           none -> From ! {membership_reply, {none}};\n           {ok, {St, Tier}} -> From ! {membership_reply, {ok, St, Tier}}\n         end,\n         loop(Members);\n       {project, Subject, App, From} ->\n         From ! {membership_reply, project_view(Subject, App, Members)},\n         loop(Members);\n       {stop, From} ->\n         From ! {membership_reply, ok}\n     end.\n\n   project_view(Subject, App, Members) ->\n     case find(Subject, Members) of\n       none -> {non_member, App};\n       {ok, {active, Tier}} -> {member, Tier, App};\n       {ok, {pending, _}} -> {pending, App};\n       {ok, {lapsed, _}} -> {lapsed, App};\n       {ok, {revoked, _}} -> {denied, App}\n     end.\n\n   set_record(_, _, []) -> [];\n   set_record(Subject, Rec, [{S, Old} | Rest]) ->\n     case S =:= Subject of\n       true -> [{S, Rec} | Rest];\n       false -> [{S, Old} | set_record(Subject, Rec, Rest)]\n     end.\n\n   find(_, []) -> none;\n   find(Key, [{K, V} | Rest]) ->\n     case K =:= Key of\n       true -> {ok, V};\n       false -> find(Key, Rest)\n     end.")

(define
  identity-load-membership!
  (fn () (erlang-load-module identity-membership-source)))
