8.5 KiB
Plan: Inbound-Sync — Nostr-native Posts im Blog und im Repo
Status: Entwurf, noch nicht umgesetzt. Auslöser:
protocol-anthropology(naddr…70xst) erschien in der Übersicht, lieferte unter/protocol-anthropology/aber 404.
Problem
Zwei getrennte Befunde, die zusammen den 404 erzeugen:
A — Relay-Abdeckung. snapshot/src/core/relays.ts fragt die NIP-65-Liste
ab (loadReadRelays) und fällt nur dann auf FALLBACK_READ_RELAYS zurück,
wenn gar kein kind:10002 kommt. Gemessen am 2026-08-31:
| Relay | kind:30023 | protocol-anthropology |
|---|---|---|
| relay.primal.net | 1 | ja |
| nos.lol | 27 | nein |
| relay.tchncs.de | 27 | nein |
| relay.damus.io | 0 | nein |
| relay.edufeed.org | 0 | nein |
| relay.plebstr.com | 0 | nein |
Das Event liegt nur auf einem Relay, und dieses Relay liefert umgekehrt die anderen 27 Posts nicht. Je nachdem, welche Liste greift, fehlt entweder der neue Post oder fast alle alten.
Erschwerend: die NIP-65-Liste (2026-04-24) nennt wss://primal.net und
wss://relay-rpi.edufeed.org, der Code-Fallback dagegen wss://relay.primal.net
und wss://relay.edufeed.org — verschiedene Hosts, nicht nur Schreibweisen.
B — kein Rückweg Nostr → Repo. Der Snapshot baut PostJson allein aus dem
Event (buildPostJson), das Repo bleibt außen vor. Ein extern (Habla/Ditto)
verfasster Post existiert daher nie als .md und fehlt im Repo-Archiv.
Nicht das Problem: Die Annahme „der Code kennt nur Markdown aus dem Repo"
trifft für den Build-Pfad nicht zu. snapshot/src/cli.ts liest ausschließlich
von Relays. Läge das Event auf nos.lol, wäre die Seite ohne jedes .md
gebaut worden. Der Nostr-first-Pfad existiert bereits — er ist an der
Relay-Abdeckung gescheitert.
Ziel
- Nostr-native Longform-Posts erscheinen automatisch im Blog (Phase 1).
- Sie landen zusätzlich als
.mdim Repo — über einen PR, nicht per Direkt-Commit (Phase 2).
Phase 1 — Relay-Union (behebt den 404)
1.1 loadReadRelays auf Vereinigungsmenge umstellen
snapshot/src/core/relays.ts: statt „NIP-65 oder Fallback" künftig
„NIP-65 und Fallback", dedupliziert.
- Neue Funktion
normalizeRelayUrl(url): Trailing-Slash weg, lowercase, Schema erhalten. Verhindert, dasswss://nos.lol/undwss://nos.lolals zwei Relays zählen. loadReadRelaysgibt[...new Set([...nip65, ...fallback].map(normalize))]zurück.FALLBACK_READ_RELAYSum die Host-Varianten aus der echten NIP-65-Liste ergänzen:wss://primal.net,wss://relay-rpi.edufeed.org.
Damit wäre protocol-anthropology gefunden worden.
1.2 Quorum-Check an die größere Liste anpassen
runChecks verlangt 60 % Relay-Antworten. Bei größerer Liste mit mehreren
toten Relays (damus, edufeed und plebstr lieferten 0) kippt das in
False-Positive-Hard-Fails.
Wichtige Unterscheidung: „hat geantwortet" ≠ „hat Events geliefert". Der
aktuelle fetcher resolved auch bei Timeout mit leerem Array, zählt also
als ok. Das Quorum misst damit Erreichbarkeit, nicht Vollständigkeit.
- Quorum auf absolute Untergrenze umstellen: mindestens 2 Relays mit Events, statt 60 % Antwortende. Genauer am Schutzziel.
eventCount- und Drop-Check bleiben unverändert — die sind der eigentliche Datenverlust-Schutz und haben hier gut funktioniert.
1.3 Tests
snapshot/tests/relays.test.ts:
- Union enthält NIP-65- und Fallback-Einträge.
- Normalisierung:
wss://nos.lol/undwss://nos.lol→ ein Eintrag. - Leere NIP-65-Antwort → reine Fallback-Liste (Regression).
snapshot/tests/checks.test.ts:
- Viele tote Relays + 2 mit Events → kein Fail.
- 1 Relay mit Events → Fail.
1.4 Verifikation
deno task snapshot lokal; erwartet ≥ 28 Posts inklusive
protocol-anthropology.json und warum-dein-ki-gedaechtnis-luegen-muss.json.
Phase 2 — Rückschreibung als PR
2.1 Neuer Subcommand sync-inbound
Neu: publish/src/subcommands/sync-inbound.ts. Bewusst in publish/, nicht
in snapshot/ — dort liegen frontmatter.ts, markdown.ts und das
Frontmatter-Schema, das wir bedienen müssen.
Ablauf:
snapshot/output/index.jsonlesen (läuft nach dem Snapshot).- Pro Post prüfen, ob
content/posts/<lang>/<slug>/index.mdexistiert. Achtung: der Ordnername im Repo trägt ein Datums-Präfix (2025-09-09-banksy-high-court-prophet), der Nostr-d-Tag nicht (banksy-high-court-prophet). Matching muss über denslug:-Wert im Frontmatter laufen, nicht über den Ordnernamen — sonst wird jeder bestehende Post als „fehlend" erkannt. - Für fehlende:
index.mderzeugen ausPostJson. - Liste der neu erzeugten Pfade als JSON auf stdout (für die Action).
2.2 Frontmatter-Rückabbildung
Aus PostJson → Frontmatter (Gegenstück zu buildKind30023):
| Frontmatter | Quelle |
|---|---|
title |
title |
slug |
slug (der d-Tag — muss exakt erhalten bleiben) |
date |
published_at → YYYY-MM-DD |
description |
summary |
image |
cover_image.url |
tags |
tags |
lang |
lang |
a |
aus translations rekonstruiert |
Zusätzlich ein Marker, der die Herkunft festhält:
source: nostr
source_event_id: 8a16dea…
Der Marker ist nicht Kosmetik — er ist die Loop-Bremse (siehe 2.4).
Ordnername: <YYYY-MM-DD>-<slug> aus published_at, konsistent zum Bestand.
Bilder bleiben remote. Der Post referenziert Blossom-URLs
(blossom.ditto.pub/…). Kein Download, keine images:-Metadatenblöcke —
die Konvention aus 2026-04-16-image-metadata-convention.md verlangt
Lizenz- und Autor-Angaben, die im Event schlicht nicht stehen. Erfinden wäre
falsch. Stattdessen ein Kommentar im Frontmatter, dass die Metadaten für
extern verfasste Posts fehlen und bei Bedarf manuell zu ergänzen sind.
2.3 Workflow sync-inbound.yml
Trigger: schedule (täglich) + workflow_dispatch.
- Checkout, Deno.
- Snapshot laufen lassen (Phase 1 aktiv).
deno run … src/cli.ts sync-inbound.- Wenn nichts erzeugt → sauber beenden.
- Sonst: Branch
nostr-sync/<datum>, committen, PR gegenmainperpeter-evans/create-pull-requestodergh pr create.
PR-Body listet die importierten Posts mit naddr-Link.
2.4 Loop-Schutz — der kritische Punkt
publish.yml triggert auf push nach content/posts/**. Ein gemergter
Sync-PR feuert damit die Publish-Action, die das Event neu signiert und
publiziert — mit neuem created_at. Der nächste Snapshot sieht die neuere
Version, alles wandert eine Runde weiter. Kein Endlos-Loop (der Inhalt
konvergiert), aber jeder Merge überschreibt ein extern erstelltes Event mit
einer Neusignatur, und dedupByDtag bevorzugt das neuere — die
Original-Fassung aus dem Nostr-Editor verschwindet.
Absicherung, zwei Ebenen:
- In
publish.ts: Posts mitsource: nostrim Frontmatter werden übersprungen, außer--force-all. Der Marker aus 2.2 trägt diese Entscheidung. - Im Workflow:
paths-ignoreallein reicht nicht, da der Sync-PR zwangsläufig untercontent/posts/**landet. Ebene 1 ist die eigentliche Bremse; Ebene 2 wäre nur Redundanz.
Der Marker macht damit eine bewusste Aussage: dieser Post wird von Nostr verwaltet, das Repo ist Archiv. Wer ihn aus dem Frontmatter entfernt, übernimmt den Post ins Repo-Regime — ein sauberer, expliziter Übergabepunkt.
2.5 Tests
publish/tests/sync-inbound.test.ts:
PostJsonohne Repo-Datei → Frontmatter korrekt,source: nostrgesetzt.- Post mit vorhandenem
.md(Datums-Präfix im Ordner!) → übersprungen. - Round-Trip: erzeugtes Frontmatter durch
parseFrontmatter→buildKind30023ergibt dieselbend/title/published_at/t-Tags wie das Ursprungsevent. publish.tsüberspringtsource: nostrohne--force-all.
Reihenfolge
Phase 1 ist eigenständig wertvoll und behebt den 404 sofort. Phase 2 baut darauf auf, ist aber unabhängig testbar. Empfehlung: Phase 1 umsetzen und deployen, den Effekt live prüfen, dann Phase 2.
Offene Punkte
- Löschungen. Wird ein Nostr-Post per kind:5 gelöscht, verschwindet er aus
dem Snapshot, das
.mdbleibt. Vorschlag: zunächst bewusst so lassen (Repo = Archiv), im PR-Body vermerken. - Nachträgliche Edits. Ein extern editierter Post erzeugt beim nächsten
Sync keinen Diff, weil die Datei existiert. Ein
--update-Modus, dersource: nostr-Dateien neu schreibt, wäre die Erweiterung — bewusst nicht in Phase 2, um den ersten Durchstich klein zu halten. - Relay-Hygiene.
relay.plebstr.comsteht an erster Stelle der NIP-65-Liste, liefert aber nichts. Unabhängig von diesem Plan wäre die kind:10002-Liste eine Aktualisierung wert.