Zum Inhalt springen

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.

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 peerreviewStatus bzw. [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.

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) → rejected

Zwei Zustände: app_peerreviews.state (open → provisional_closed → closed) und das Ergebnis app_arguments.peerreview_status (preliminary → approved / rejected).

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), nur createRecord.
  • 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) und OPEN_LIMIT (gleichzeitig offene), damit ein Dossier ein anderes nicht verdrängt und sich bei inaktiven Personen nichts aufstaut.

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)

Der Client schreibt ch.atvoto.peerreview.response (Argument, Kriterien, vote) ins Repo der Person. Der Writer prüft (_accept_response):

  1. vote ist APPROVE oder REJECT (invalid_vote)
  2. Argument bekannt (argument_not_found)
  3. Teilnahmeregeln des Dossiers (participation_check, Aktion peerreview)
  4. Phase des Dossiers (dossier_not_open; paused, closed, withdrawn = warten, die Review ruht bis zum Wieder öffnen)
  5. 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).

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.

services/writer/src/arguments/peer_review_finalize.py:

  1. provisional_closed mit abgelaufener Frist → closed. Ab dann antwortet app_response_gate mit review_closed, die Zahlen stehen fest.
  2. Für jede geschlossene Review ohne Ergebnis: Gutachten zählen und ch.atvoto.peerreview.result ins Community-Konto schreiben (rkey = rkey des Arguments; argument, outcome, approvals, rejections, quorum, createdAt), dann result_uri und 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.

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.

  • 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.
  • 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)

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.

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