Index: docs/ward-outbound-follows-design.md
===================================================================
--- docs/ward-outbound-follows-design.md	(revision af035e6da85ac9676b70f27aeb37777f0f3e3003)
+++ docs/ward-outbound-follows-design.md	(revision af035e6da85ac9676b70f27aeb37777f0f3e3003)
@@ -0,0 +1,113 @@
+# Uitgaande follows van een ward — ontwerp
+
+FEP-633c §5.3 houdt follows *naar* een ward tegen tot de guardians beslissen.
+Follows *van* een ward gaan ongehinderd de deur uit. `ingestOutboxActivity`
+roept bij `case 'Follow'` meteen `followActor()` aan: geen ward-check, geen
+guardian, geen wachtrij. `mee` heeft een vastgelegde guardian en kan vandaag
+iedereen op de fediverse volgen zonder dat iemand het ziet.
+
+De asymmetrie is half te verdedigen. `Block` (Shaers "Orbit") is uitgaand ook
+ongated, en dat is de veilige richting: een kind dat zijn eigen wereld kleiner
+maakt heeft geen toestemming nodig. Volgen is de richting die hem opent.
+
+Er staat hierover niets in de docs of de FEP-notities. Dit is dus een gat in het
+ontwerp, niet een ongeschreven stuk implementatie.
+
+## De regel
+
+**Per geval goedkeuring, met automatische goedkeuring bij wederkerigheid.**
+
+Volgt de ward iemand die de ward al volgt, dan hoeft er niemand meer naar te
+kijken. Die actor is namelijk al door de inkomende poort gekomen, en dat betekent
+dat een guardian er al ja tegen heeft gezegd. Nog een keer vragen is dezelfde
+vraag twee keer stellen, en elke overbodige vraag is er een die de volgende keer
+minder aandacht krijgt.
+
+Het predicaat is `target_uri ∈ ap_followers(slug)`. Kort, maar het klopt alleen
+zolang lidmaatschap van die tabel écht een guardian-besluit impliceert — zie de
+eerste open vraag.
+
+## Beslissingen
+
+- **Een eigen tabel, niet `ap_pending_follows`.** Die tabel is gesleuteld op
+  `(ward_slug, follower_uri)`: de ward is daar het *doel*. Uitgaand draait dat
+  om. Een `direction`-kolom erbij zou elke bestaande query dubbelzinnig maken,
+  inclusief `listForWard`, die nu simpelweg "wie wil mij volgen" betekent. Dus
+  `ap_pending_outgoing_follows (id, ward_slug, target_uri, target_inbox,
+  target_name, target_handle, target_icon, quorum, status, created_at)` met
+  dezelfde vorm en dezelfde `decide()`-semantiek, maar apart.
+
+- **`ap_following.status` is al bezet en betekent iets anders.** Daar staat
+  `pending` voor "wij hebben de Follow verstuurd, hun Accept moet nog komen".
+  Een guardian-pending follow is nog helemaal niet verstuurd. Twee verschillende
+  wachttoestanden op één kolom is precies het soort dubbelzinnigheid dat later
+  een bug wordt. Daarom: **de `ap_following`-rij ontstaat pas bij goedkeuring**,
+  op het moment dat de Follow daadwerkelijk uitgaat. Vóór die tijd bestaat het
+  verzoek alleen in de nieuwe tabel.
+
+- **Wederkerigheid is een momentopname.** Getoetst bij het verzoek, niet
+  doorlopend bewaakt. Ontvolgt de ander later, dan wordt een al goedgekeurde
+  follow niet met terugwerkende kracht ingetrokken. Anders krijg je een relatie
+  die stilletjes verdwijnt door een actie van een derde.
+
+- **Quorum: `any` hergebruiken, maar het eindelijk ergens vastleggen.** De kolom
+  `quorum` bestaat op `ap_pending_follows`, maar de aanroep in
+  `ActivityPubService.js` geeft hem nooit mee. Alles valt dus terug op de default
+  `'any'`, en `'all'` en `'none'` zijn in de praktijk dood. Een uitgaand beleid
+  heeft een echte plek nodig — per ward, niet per verzoek — en dat is het moment
+  om de inkomende kant dezelfde plek te laten gebruiken.
+
+- **De wachtrij splitsen.** `shaer:queues.follows` staat al op het actor-document.
+  Als beide richtingen daarin landen, kan een guardian "iemand wil Mee volgen"
+  niet onderscheiden van "Mee wil iemand volgen" — twee vragen die in de
+  interface verschillende woorden verdienen. Dus `follows` blijft inkomend
+  (compatibel) en er komt `shaer:queues.outgoingFollows` naast.
+
+  Let op: nieuwe termen moeten in `AP_CONTEXT` of `test/activitypub-as2.test.js`
+  valt om. Die test eist dat elke uitgestuurde sleutel AS2-core is of in de
+  federatie-context staat, en dat is precies de bedoeling ervan.
+
+- **Cross-instance: spiegel het `Offer(Follow)`-patroon.** Een lokale guardian
+  leest `/guardian` rechtstreeks; een guardian op een andere server krijgt een
+  Offer afgeleverd zodat zijn instance een kopie opslaat, net als bij de
+  adoptie-offer en bij `ap_follow_reviews`. Voor `mee` (ward op `loop`) en
+  `boiert` (guardian op `boiert`) zijn dat twee instances op dezelfde machine,
+  dus die weg wordt meteen echt gelopen en niet gesimuleerd.
+
+- **Een tegengehouden follow mag er niet uitzien als een gelukte.** Dit is
+  dezelfde les als in de bestaande `case 'Follow'`: *"De error REACHT de app
+  (Robins melding, 31-7): het wegslikken maakte een mislukte follow precies
+  gelijk aan een gelukte."* Een verzoek in de wacht is een derde uitkomst en de
+  app moet die kunnen tonen. Voorstel: `202` met een expliciete status in de body
+  (`{ ok: true, status: 'awaiting_guardian', id }`), zodat Shaer "wacht op
+  toestemming" kan laten zien in plaats van een tegel die er al volgend uitziet.
+
+## Open vragen
+
+1. **Wat doen we met followers van vóór de adoptie?** Was de ward eerst een vrije
+   actor, dan zijn zijn bestaande followers nooit door een guardian gezien. Bij de
+   regel hierboven worden dat stuk voor stuk automatisch goed te keuren doelen.
+   Ofwel we accepteren dat, ofwel `ap_followers` krijgt een markering "door de
+   poort gekomen" en alleen die telt mee. Dit is de belangrijkste vraag van dit
+   document.
+
+2. **Mag de ward zijn eigen verzoek intrekken** zolang het in de wacht staat? Lijkt
+   vanzelfsprekend ja, maar het is een Undo op iets dat nooit verstuurd is.
+
+3. **De guardian zelf als doel.** Een ward die zijn eigen guardian volgt, hoort
+   niet te hoeven wachten. Dat is dezelfde uitzondering als inkomend, waar de
+   Follow van een vastgelegde guardian de poort overslaat.
+
+4. **Interactie met Block/Orbit.** Blokkeert de ward iemand terwijl er nog een
+   verzoek voor die persoon open staat, dan moet dat verzoek verdwijnen.
+
+5. **Emancipatie.** Wat gebeurt er met openstaande verzoeken als de
+   guardianship eindigt? Automatisch goedkeuren of laten vervallen.
+
+## Daemon-pariteit
+
+De daemon-README is er stellig over: de apps moeten zich tegen beide backends
+hetzelfde gedragen, en als daemon en Klonkt uit elkaar lopen is de UI die je
+lokaal test een leugen. Wat hier landt, landt dus ook in `shaer-daemon`
+(`gate.rs` heeft nu alleen de inkomende kant), en de wachtrij-namen en de
+`awaiting_guardian`-status moeten letterlijk gelijk zijn.
