Peer Review
Stand: 2026-10-26. Regeln: VERFASSUNG Titel VII (Art. 25–31). Kriterien: ARGUMENT_CRITERIA.md. Ein- und ausgeschaltet wird es pro Client (siehe Ein- und Ausschalten).
Argumente von Teilnehmenden (source_type = 'user') gehen durch ein Peer Review,
bevor sie als angenommen gelten. Eine kleine, per Los bestimmte Gruppe stimmt ab, die
Mehrheit entscheidet. Offizielle Argumente und solche von Organisationen haben kein
Peer Review.
Das Los macht Absprachen teuer: Wer ein Argument durchbringen will, braucht etwa
1 / INVITE_PROBABILITY-mal so viele Beteiligte, um im Gremium zu landen.
Ein- und Ausschalten (pro Client)
Abschnitt betitelt „Ein- und Ausschalten (pro Client)“Ob es ein Peer Review gibt und mit welchem Quorum, legt der Client fest, dem das
Dossier gehört: Registrar setPeerReviewSettings mit {"settings": {"enabled": true, "quorum": 10}} (getPeerReviewSettings liest sie). Gespeichert in
registry.clients.peer_review, jede Änderung in registry.peer_review_log. Ohne
Einstellung gilt: aus, Quorum 10. Erlaubt sind Quoren von 1 bis 100.
Entschieden wird pro Argument, beim Indexieren: Ist Peer Review für den Client an,
bekommt das Argument eine Zeile in app_peerreviews mit dem aktuellen Quorum
(registry.peer_review_settings), sonst keine. Danach zählt nur noch diese Zeile:
- Einladungen gibt es nur für Argumente mit offenem Review.
- AppView und Crosspost zeigen
peerreviewStatusbzw.[Preliminary]nur für Argumente mit Review. - Einschalten und ein neues Quorum gelten für neue User-Argumente: Erst wenn der Indexer ein neues Argument anlegt, bekommt es eine Peer-Review-Zeile. Bestehende Argumente laufen nicht nachträglich ins Review. Ob die Einladungen tatsächlich verschickt werden, sieht man deshalb erst beim ersten neuen Argument.
- Ausschalten stoppt laufende Reviews nicht, sie werden normal abgeschlossen.
Urteil und Kriterien
Abschnitt betitelt „Urteil und Kriterien“Gutachter:innen geben ein Gesamturteil ab (vote = APPROVE/REJECT): Soll
das Argument in den Katalog? Dazu je Kriterium ein Flag ok/beanstandet für die
fünf Kriterien Stimmigkeit, Umgangston, Thematik, Fokus, Kein Duplikat
(Art. 25). „Kein Duplikat“ erscheint nur, wenn der Live-Check
(peerreview.duplicateCandidate) ein ähnliches Argument gleicher Position findet.
Die KI-Bewertung der Vorprüfung sehen sie nicht (Art. 28), ein Begründungsfeld gibt
es nicht. Die Richtigkeit einer Meinung ist kein Kriterium.
Person reicht Argument ein (Record im eigenen Repo) → Writer: Annahmeschleuse → kanonische Kopie im Community-Konto → Indexer: app_arguments + app_peerreviews (state = open, quorum des Clients), nur wenn der Client Peer Review eingeschaltet hat │ ▼Aktive Personen schicken peerreview.request (Client, höchstens 1× pro aktivem Tag) → Writer: Zuteilung per Los, nur wer nach den Regeln des Dossiers begutachten darf → peerreview.invitation im Community-Konto (invited = true | false) │ ▼Eingeladene checken ein (peerreview.checkIn), Tippen meldet peerreview.activity │ ▼Gutachten: peerreview.response im eigenen Repo → Writer: Annahmeschleuse (Regeln, app_response_gate) → Kopie im Community-Konto → Indexer: zählt; bei Quorum oder feststehendem Ergebnis → provisional_closed, Nachfrist beginnt │ ▼Nachfrist: keine neuen Check-ins, Eingecheckte können noch abgeben,Tippen verlängert die Frist │ Frist abgelaufen, nächster Lauf des Finalisers (1× pro Minute) ▼state = closed, ch.atvoto.peerreview.result im Community-Konto Mehrheit APPROVE → approved, sonst (auch Gleichstand) → rejectedZwei Zustände: app_peerreviews.state (open → provisional_closed → closed)
und das Ergebnis app_arguments.peerreview_status (preliminary → approved /
rejected).
Zuteilung
Abschnitt betitelt „Zuteilung“Es gibt keinen Hintergrund-Worker für die Auswahl. Ausgelöst wird sie durch
Aktivität: Der Client schreibt bei aktiver Session einen peerreview.request ins
Repo der Person, der Writer nimmt ihn an (irgendeine aktive Mitgliedschaft genügt)
und ruft maybe_assign_reviews_for_user auf
(services/writer/src/arguments/peer_review_assign.py).
pro Aufruf (gedrosselt: 30 s pro Person): Kandidaten = Argumente mit state = open AND nicht von der Person AND noch nie für diese Person ausgelost AND participation_check(person, dossier, 'peerreview') erlaubt ORDER BY created_at LIMIT 100 pro Kandidat: Dossier übersprungen, wenn heute schon DAILY_LIMIT Einladungen oder OPEN_LIMIT offene Einladungen in diesem Dossier ausgelost = random() <= INVITE_PROBABILITY invitation(argument, person, invited = ausgelost) schreiben- Nieten werden gespeichert (
invited = false): Dieselbe Person wird für dasselbe Argument nie neu gelost. Sonst würde Wiederholen das Los aushebeln. - Idempotent: rkey =
compose_review_rkey(argument, did), nurcreateRecord. - Quorum begrenzt keine Einladungen, nur den Abschluss. Langsame Reviews bekommen weiter Einladungen, bis genug Gutachten da sind.
- Grenzen pro Dossier:
DAILY_LIMIT(neue Einladungen in 24 h) undOPEN_LIMIT(gleichzeitig offene), damit ein Dossier ein anderes nicht verdrängt und sich bei inaktiven Personen nichts aufstaut.
Check-in
Abschnitt betitelt „Check-in“Wer eingeladen ist, checkt vor dem Gutachten ein. Das schützt laufende Arbeit vor dem Abschluss.
| Methode (AppView, Service-Auth) | Verhalten |
|---|---|
peerreview.checkIn |
setzt checked_in_at (einmalig) und last_activity_at. Abgelehnt: not_invited; too_late bei provisional_closed ohne frühere Anmeldung; closed |
peerreview.activity |
bei echter Eingabe (gedrosselt); verschiebt während provisional_closed die Frist auf jetzt + Nachfrist |
peerreview.status |
Zustand, Quorum, Fristen, Stimmen, eigener Check-in |
peerreview.pending |
offene Einladungen der Person |
peerreview.list |
Reviews eines Dossiers (scope = mine / all) |
Gutachten
Abschnitt betitelt „Gutachten“Der Client schreibt ch.atvoto.peerreview.response (Argument, Kriterien, vote)
ins Repo der Person. Der Writer prüft (_accept_response):
voteistAPPROVEoderREJECT(invalid_vote)- Argument bekannt (
argument_not_found) - Teilnahmeregeln des Dossiers (
participation_check, Aktionpeerreview) - Phase des Dossiers (
dossier_not_open;paused,closed,withdrawn= warten, die Review ruht bis zum Wieder öffnen) - Zustand der Review, in fester Reihenfolge aus der SQL-Funktion
app_response_gate:no_peerreview,not_invited,review_closed,not_checked_in
Wer vor provisional_closed eingecheckt hat, kann während der Nachfrist noch
abgeben. Erst danach entsteht die Kopie im Community-Konto (rkey wie bei der
Einladung, also höchstens ein Gutachten pro Person und Argument).
Abschluss
Abschnitt betitelt „Abschluss“Schritt 1: nach jedem Gutachten (Indexer)
Abschnitt betitelt „Schritt 1: nach jedem Gutachten (Indexer)“checkReviewQuorum (services/indexer/src/db.js) zählt nur Gutachten mit aktiver
Einladung und setzt provisional_closed + Nachfrist, sobald eine Bedingung gilt
(remaining = max(0, quorum − total)):
| Auslöser | Bedingung |
|---|---|
quorum |
total ≥ quorum |
locked_approve |
approvals > rejections + remaining |
locked_reject |
rejections ≥ approvals + remaining (Gleichstand zählt als Ablehnung) |
Beispiele bei quorum = 10: 5R/0A → locked_reject; 6A/0R → locked_approve;
4A/4R → offen. Das UPDATE … WHERE state = 'open' verhindert doppeltes Umschalten;
mehr Gutachten als das Quorum sind erlaubt.
Ein „feststehendes“ Ergebnis gilt zum Zeitpunkt des Auslösens: Eingecheckte können in der Nachfrist noch abstimmen, und ihre Stimmen zählen.
Schritt 2: Finaliser (Writer, jede Minute)
Abschnitt betitelt „Schritt 2: Finaliser (Writer, jede Minute)“services/writer/src/arguments/peer_review_finalize.py:
provisional_closedmit abgelaufener Frist →closed. Ab dann antwortetapp_response_gatemitreview_closed, die Zahlen stehen fest.- Für jede geschlossene Review ohne Ergebnis: Gutachten zählen und
ch.atvoto.peerreview.resultins Community-Konto schreiben (rkey = rkey des Arguments;argument,outcome,approvals,rejections,quorum,createdAt), dannresult_uriund Ergebnis in der DB spiegeln. Nach einem Absturz wird der vorhandene Record wiederverwendet, nie doppelt geschrieben.
Der Record ist der verbindliche, öffentliche Entscheid (Art. 29): nachzählbar gegen Einladungen und Gutachten im selben Repo, und Clients erfahren ihn über den Firehose.
Konfiguration
Abschnitt betitelt „Konfiguration“Ein/aus und Quorum sind Client-Einstellungen (siehe oben). Für den Betrieb:
| Variable | Standard | Dienst | Bedeutung |
|---|---|---|---|
PEER_REVIEW_DAILY_LIMIT |
3 |
Writer | neue Einladungen pro Person und Dossier in 24 h |
PEER_REVIEW_OPEN_LIMIT |
4 |
Writer | offene Einladungen pro Person und Dossier |
PEER_REVIEW_INVITE_PROBABILITY |
0.35 |
Writer | Los pro Kandidat |
PEER_REVIEW_HOOK_THROTTLE_SECONDS |
30 |
Writer | Drosselung pro Person |
PEER_REVIEW_FINALIZE_INTERVAL_SECONDS |
60 |
Writer | Takt des Finalisers |
APPVIEW_PEER_REVIEW_GRACE_PERIOD_SECONDS |
600 |
Indexer, AppView | Nachfrist und Verlängerung |
APPVIEW_PEER_REVIEW_CRITERIA |
fünf Kriterien | AppView | Kriterientexte |
Faustregel: DAILY_LIMIT × aktive Personen × INVITE_PROBABILITY sollte deutlich
über QUORUM × neue Argumente pro Tag liegen, sonst dauert der Abschluss lange.
Bei den Standardwerten: etwa 10 aktive Personen pro neuem Argument und Tag.
Wer begutachten darf
Abschnitt betitelt „Wer begutachten darf“- Die Teilnahmeregeln des Clients, dem das Dossier gehört (Aktion
peerreview, ROADMAP.md): Mitgliedschaft, Prüfstufe, Herkunft. - Nicht das eigene Argument, nie zweimal für dasselbe Argument (Art. 27).
- Nicht vorgesehen: Reputation, Sperrfristen.
Randfälle
Abschnitt betitelt „Randfälle“- Parallele Anfragen derselben Person können die Drosselung umgehen und im schlimmsten Fall doppelt so viele Einladungen erzeugen. Selten, akzeptiert.
- Keine Aktivität: Ein neues Argument wartet, bis jemand aktiv ist.
- Eingecheckt und verschwunden: Die Frist läuft trotzdem ab, kein Hängen.
- Tippen kurz vor dem Finaliser: kann noch geschlossen werden; bei 1 min Takt und 10 min Nachfrist vernachlässigbar.
- Zurück nach dem Abschluss: Die Person sieht den Status mit Ergebnis.
| Tabelle | Inhalt | Geschrieben von |
|---|---|---|
app_peerreviews |
Zustand, Quorum, Fristen, result_uri |
Indexer, Writer (Finaliser) |
app_peerreview_invitations |
Los-Einträge, Einladungen, Check-in (checked_in_at, last_activity_at) |
Writer (Zuteilung), Indexer, AppView (Check-in) |
app_peerreview_responses |
Gutachten | Indexer (aus der Kopie im Community-Konto) |
app_arguments.peerreview_status |
Ergebnis | Finaliser, Indexer |
registry.clients.peer_review, registry.peer_review_log |
Ein/aus und Quorum pro Client | Registrar |
| Teil | Datei |
|---|---|
| Einstellung pro Client | services/writer/src/registrar/peer_review.py, SQL registry.peer_review_settings |
| Zuteilung | services/writer/src/arguments/peer_review_assign.py |
| Annahme von Anfrage und Gutachten | services/writer/src/atproto/acceptance.py (_accept_request, _accept_response) |
| Abschluss und Ergebnis | services/writer/src/arguments/peer_review_finalize.py |
| Quorum-Auslöser, Projektion des Ergebnisses | services/indexer/src/db.js (checkReviewQuorum, upsertPeerreviewResultDb) |
| Check-in, Status, Listen | services/appview/src/routes/participation/reviews.py |
| Prüfung der Review | SQL app_response_gate in db/db-setup.sql |
| Lexicons | lexicons/ch/atvoto/peerreview/ (request, invitation, response, result) |
Im Client
Abschnitt betitelt „Im Client“Der Client zeigt Einladungen (pending), checkt beim Öffnen des Formulars ein,
meldet Tippen (activity, gedrosselt), zeigt während provisional_closed einen
Countdown aus graceUntil und sichert den Entwurf lokal. Ein review_closed nach
dem Abgeben muss er verständlich anzeigen. Die Umsetzung im Client
(ReviewForm, PeerReviewDetail) dient als Vorlage für weitere Clients.
Geschichte
Abschnitt betitelt „Geschichte“| Datum | Änderung |
|---|---|
| vor 2026-06 | Hintergrund-Worker in der AppView, Abschluss bei früher Mehrheit |
| 2026-06 | Zuteilung durch Aktivität; Check-in und Nachfrist (app_peerreviews); Quorum pro Review; frühes Ende bei feststehendem Ergebnis wieder eingeführt |
| 2026-10 (atvoto) | Zuteilung und Annahme der Gutachten im Writer statt in der AppView; Auslöser peerreview.request statt Session-Middleware; Grenzen pro Dossier |
| 2026-10-26 | Ein/aus und Quorum pro Client (Registrar, Migration 006) statt APPVIEW_PEER_REVIEW_ENABLED und PEER_REVIEW_QUORUM |
| 2026-10-26 | Ergebnis als öffentlicher Record peerreview.result, Finaliser im Writer; Teilnahmeregeln pro Client bestimmen den Pool |