fed-sx-m2: Step 6a — follower_graph projection + 18 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 42s

New next/kernel/follower_graph.erl is the Erlang-fun stand-in for
the genesis follower-graph.sx projection body, mirroring the
shape of actor_state.erl and define_registry.erl.

State shape (substrate has no maps, so a proplist):
  [{ActorId, [{following,        [PeerId, ...]},
              {followers,        [PeerId, ...]},
              {pending_outbound, [PeerId, ...]},
              {pending_inbound,  [PeerId, ...]}]}, ...]

Fold rules per design §13.2:
  Follow{actor: A, object: B}
      add B to A.pending_outbound
      add A to B.pending_inbound
  Accept{actor: B, object: Follow{A->B}}
      A moves from B.pending_inbound -> B.followers
      B moves from A.pending_outbound -> A.following
  Reject{actor: B, object: Follow{A->B}}
      clear A from B.pending_inbound, B from A.pending_outbound
  Undo{actor: A, object: Follow{A->B}}
      drop A<->B from every list on either side
      only the Follow's original actor may Undo it

Edge cases handled:
  - self-follow (alice -> alice) is a no-op
  - duplicate Follow is idempotent (list sets)
  - Accept/Reject/Undo whose :object isn't a Follow proplist
    passes through
  - Undo by the wrong actor (carol Undoing Follow{alice->bob})
    is a no-op

Public API:
  new/0, lookup/2, actors/1
  following/2, followers/2,
  pending_outbound/2, pending_inbound/2
  is_following/3, has_follower/3,
  is_pending_outbound/3, is_pending_inbound/3
  fold/2, fold_fn/0

fold_fn/0 returns the standard 2-arity Erlang fun for
projection:start_link/3 (same plug shape as actor_state and
define_registry).

Local find_keyed/set_keyed/contains/remove_member helpers — no
lists:keyfind/keymember/member in this substrate (same gap as
Step 1a/2b/5a/5c).

18/18 in next/tests/follower_graph.sh covering all four verbs,
predicates, edge cases (self-follow, duplicate Follow, untyped
activity, non-Follow :object, wrong-actor Undo).

Step 6b wires this into the inbox handler so a peer Follow lands,
fires auto-Accept publish (open-world policy per §13.2; manual
moderation deferred to v3).

Conformance 761/761. 130/130 across 9 Step-6-adjacent suites
(inbox, inbox_bucket, inbox_pipeline, inbox_peer_resolution,
actor_state_pure, define_registry_pure, projection_pure,
nx_kernel_multi, smoke_app_pure).
This commit is contained in:
2026-06-06 20:47:01 +00:00
parent 6231a82be0
commit e890380a1a
3 changed files with 433 additions and 17 deletions

View File

@@ -419,23 +419,28 @@ tracks the state. `Undo{Follow}` reverses it.
**Deliverables:**
- New activity-types (runtime via DefineActivity, ideally):
Follow, Accept, Reject, Undo.
- Follower-graph projection (Erlang-fun stand-in): tracks
`{ActorId => #{following => [PeerId], followers => [PeerId],
pending_outbound => [PeerId], pending_inbound => [PeerId]}}`.
- Accept-handling fold logic: when A receives `Accept{Follow A→B}`,
move B from `pending_outbound` to `following`.
- Reciprocal: when B receives `Follow A→B`, automatically queue an
outbound `Accept` (auto-accept policy; manual moderation v3).
**Tests:**
- Follow → 202; sender's pending_outbound includes target.
- Auto-Accept on receiving Follow; both sides' graphs update.
- Reject leaves no following relationship.
- Undo{Follow} removes the following.
- Self-follow rejected.
- [x] **6a** — `follower_graph.erl` Erlang-fun stand-in for the
genesis `follower-graph.sx` projection body. State shape is a
property-list keyed by ActorId (maps `#{}` not in substrate),
each entry carries `{following, followers, pending_outbound,
pending_inbound}` lists. Fold rules:
- `Follow{actor: A, object: B}` — A → pending_outbound(B);
B → pending_inbound(A).
- `Accept{actor: B, object: F=Follow{A→B}}` — A → following(B)
on A's bucket; B → followers(A) on B's bucket; pendings cleared.
- `Reject{actor: B, object: F}` — pendings cleared, no promote.
- `Undo{actor: A, object: F}` — drops A↔B from every list; only
F's original actor can Undo (carol can't Undo F{A→B}).
Self-follows are no-ops; duplicate Follows are idempotent;
Accept/Reject/Undo of non-Follow `:object`s pass through.
18 cases in `follower_graph.sh`. The `fold_fn/0` 2-arity fun
plugs into `projection:start_link/3` exactly like
`define_registry:fold_fn/0` and `actor_state:fold_fn/0`.
- [ ] **6b** — Wire follower-graph fold to the inbox handler so a
peer Follow lands, fires auto-Accept publish (open-world policy
per §13.2; manual moderation deferred to v3). Acceptance test
in `follow_lifecycle.sh` covering the end-to-end
Follow → inbox → auto-Accept → projection-state-converges flow.
**Acceptance:** `bash next/tests/follow_lifecycle.sh` passes 14+ cases.
@@ -808,6 +813,21 @@ proceed.
Newest first.
- **2026-06-06** — Step 6a: follower-graph projection
(`follower_graph.erl`). Pure-functional fold over Follow /
Accept / Reject / Undo activities per design §13.2. State is a
proplist keyed by ActorId carrying `{following, followers,
pending_outbound, pending_inbound}` lists. Follow pushes onto
pendings; Accept moves both sides from pendings into the
permanent lists; Reject just clears pendings; Undo drops the
pair everywhere (and only the Follow's original actor can Undo).
Self-follow is a no-op; duplicate Follow is idempotent;
Accept/Reject/Undo of a non-Follow `:object` passes through.
`fold_fn/0` is the standard 2-arity fun for
`projection:start_link/3` (same shape as `actor_state` and
`define_registry`). 18/18 in `follower_graph.sh`. Conformance
761/761.
- **2026-06-06** — Step 5d: POST /actors/<id>/inbox real ingestion.
`route/2` now special-cases POST `/actors/<id>/inbox` next to POST
`/activity` so the body + full Cfg reach the new