Plainva 0.8.0: eine Notiz, die zurückschreibt
Eine Notiz war bisher etwas, das man allein schreibt. In diesem Release geht es darum, was passiert, wenn nicht.
Das Schwere war die Stelle, nicht der Kommentar
Ein Kommentar ist einfach. Ein Kommentar, der nächste Woche noch etwas bedeutet, ist es nicht.
Das Problem: zwei Arten von Bearbeitung arbeiten gegeneinander. In Plainva ist ein Marker im Markdown exakt — er überlebt jede Änderung, weil er sich mit dem Text bewegt. In Obsidian, in vim, in was auch immer denselben Ordner öffnet, kommt dieser Marker nicht zurück: ein Einfügen, ein Neuformatieren, ein Suchen-und-Ersetzen, und er ist weg.
Also reisen beide Anker zusammen. Der harte Anker ist ein HTML-Kommentar-Paar, <!--pv#7f3a--> — der eine Markdown-Mechanismus, den jeder konforme Renderer versteckt, und den Plainva schon für seinen Index-Marker benutzt. Der weiche Anker ist das Zitat plus vierzig Zeichen Kontext auf jeder Seite. Die Auflösung geht vier Stufen: Marker, Zitat-im-Kontext, nächstliegender von mehreren Kandidaten, verwaist.
Die vierte Stufe ist die, an der mir liegt. Ein Kommentar ohne Ort wird behalten, nie verworfen. Seine Karte sagt das, und sie sagt es ehrlich: Wo die Stelle sich verschoben hat, benennt die Karte die Verschiebung; wo sie noch passt, bleibt die Karte still — eine Statuszeile, die immer steht, sagt nämlich gar nichts.
Ein Detail, das ich erst spät gesehen habe: Die Suche nach dem Zitat läuft auf dem markerfreien Text. Ein Zitat stammt aus dem, was jemand markiert hat, enthält also nie einen Marker — der Rohtext dagegen kann sehr wohl einen im Bereich stehen haben, wenn sich zwei Kommentare überlappen. Und ein einzelner öffnender Marker, dessen Gegenstück ein fremder Editor gefressen hat, gilt als abwesend: eine halbe Klammer darf keinen Bereich erzeugen, der bis zum Ende der Notiz läuft.
Der Kommentartext selbst landet nie im Markdown. In einer verschlüsselten Workspace liegt die Notiz als Chiffrat in der Cloud, und was Menschen darüber sagen, ist oft die heiklere Hälfte.
Kommentare an Dingen, die kein Text sind
Alles, was ein Widget zeichnet — ein eingebettetes Bild, ein Mermaid-Diagramm, eine Tabelle — ersetzt sein Markdown durch ein Decoration.replace. Es bleibt kein Text zum Markieren, also gab es nichts zu verankern.
Die Lösung hat eine Form, die mir gefällt. Der Anker lernt einen optionalen Anzeige-Hinweis: worauf gezeigt wurde, und bei einer Tabelle auf welche Zeile und Spalte. Ein Hinweis, keine Identität — der Anker bleibt Markerpaar plus Zitat, das Protokoll ändert sich nicht, und ein älterer Client liest diese Kommentare anstandslos.
Dann der Rahmen. Eine Markierung über einem ersetzten Bereich rendert nichts, also wird der gerahmte Anker beim Bauen der Markierungen übersprungen und seine Nutzlast reist in einem zweiten Feld, das durch Änderungen mitwandert. Die Widgets fragen, wo ihr eigener Bereich liegt, und zeichnen ein outline — outline und nicht border, weil outline außerhalb des Layouts sitzt und die Höhenmessungen in Ruhe lässt. Ein Rahmen hätte jede Tabelle um zwei Pixel verschoben, sobald jemand sie kommentiert.
Die Tabellenzelle war die letzte, die saß, und sie brauchte einen zweiten Durchgang, nachdem der Maintainer sie benutzt hatte. Die Karte hatte das Markdown der ganzen Tabelle zitiert. Jetzt zitiert ein Zellanker die Zelle, die Koordinaten bleiben im Hinweis, und die Auflösung findet die Zelle an ihren Koordinaten oder ersatzweise über ihren Inhalt — eine oben eingefügte Zeile verschiebt den Kommentar also mit, und die Karte nennt die neue Zeile.
Ein Vorschlag wird getippt, nicht beschrieben
Die erste Fassung hatte einen Schalter im Schreibfeld: Anmerkung schreiben, „das ist eine Änderung” ankreuzen, ein Feld mit dem Ersatztext füllen. Es funktionierte, und es war falsch. Eine Änderung neben dem Text zu beschreiben ist nicht, wie irgendjemand Text bearbeitet.
Vorschlagen ist deshalb jetzt ein Modus, neben Lesen, Live und Quelltext. Du tippst in einer Kopie der Notiz, die Datei bleibt die Grundlage. @codemirror/merge zeichnet Verschwundenes durchgestrichen und Neues markiert, in den zwei Tönen der App, und nichts wird gespeichert oder ins Journal geschrieben, solange der Modus läuft. Ein Band unter der Leiste zählt die Änderungsblöcke und nimmt einen optionalen Satz zur Runde entgegen.
Senden macht aus jedem Block zwischen Grundlage und Kopie einen Vorschlag. Der Grundtext wird zum Zitat des Ankers — oder, wo ein Block nur hinzufügt und nichts entfernt, zu einem Einfügepunkt: ein Anker mit leerem Zitat, der eine Stelle allein über ihren Kontext benennt. Dieser Fall hatte vorher keinen Platz zum Stehen. Ein Vorschlag, der kein Zeichen abdeckt, wird im Editor als Widget an seiner Stelle gezeichnet und in der Leseansicht als ins zwischen zwei Zeichen.
Und die Blöcke bleiben zusammen. Ein Senden ist eine Runde: sie zeigt sich als Gruppe und bietet, solange mehr als ein Block offen ist, „Alle übernehmen” und „Alle ablehnen”. Übernehmen löst jeden Block gegen den Text auf, wie er gerade steht, verweigert, wenn einer seine Stelle verloren hat, und schreibt die Notiz in einer Transaktion — von hinten nach vorn. Von hinten, weil das Übernehmen eines früheren Blocks jeden späteren verschiebt; die Alternative wäre, Offsets viermal neu zu berechnen und einmal danebenzuliegen.
Eine Publikation ist eine eigene Workspace
Einen Ausschnitt des Vaults mit jemandem zu teilen, der nicht im Vault ist, ist kein Rechte-Problem. Es ist ein Schlüssel-Problem, und die einzige ehrliche Antwort, die ich gefunden habe, ist Trennung: eine Publikation bekommt ihre eigene Genesis, ihre eigene Policy, ihre eigenen Gruppenschlüssel. Nichts wandert hinüber. Die einzige Verbindung ist eine abgeleitete Id.
Genau das macht das Zurückziehen ehrlich. Eine Publikation fallenzulassen verschlüsselt nichts neu, woran das Team gerade arbeitet — das Team war nie in diesen Schlüsseln.
Zwei Dinge haben mich unterwegs überrascht.
Erstens: Die Verbotsliste hatte die falsche Form. Der Sanitizer entschied, was den Vault verlässt, indem er apiKey, password, private, secret, token blockte — und zwei Dinge, die Plainva selbst schreibt, laufen an dieser Liste vorbei, ohne eines dieser Wörter zu enthalten. Der plainva-Namespace trägt einen Aufgaben-Anker, dessen Identität das verifizierte Google- oder Microsoft-Konto des Veröffentlichenden ist. OKF-sources trägt die RFC-Message-ID der privaten Mail, aus der eine Notiz erfasst wurde. Frontmatter passiert jetzt eine gemeinsame Erlaubnisliste. Eine fehlende Liste bedeutet diese Regel, nicht „alles veröffentlichen”.
Die Link-Projektion hatte dasselbe Loch: Wiki-Links und Inline-Links waren abgedeckt, Referenz-Links nicht — und die Definitionszeilen am Dateiende mit ihren Zielen auch nicht. Ein Label zu neutralisieren und den Pfad stehen zu lassen, veröffentlicht den Pfad.
Zweitens: Eine rote Probe hat einen Wächter als tragend erwiesen, den ich für defensiv gehalten hätte. Einen Empfänger zu widerrufen benutzt denselben Code wie das Widerrufen eines Mitglieds. Ohne Wächter wirft das Widerrufen des Publikations-Eigentümers gar nichts — der Veröffentlichende wird still aus seiner eigenen Publikation geworfen, die genau einen Owner hat und keinen zweiten, der ihn zurückholen könnte.
Ein Stück gefällt mir besonders. Die Id eines veröffentlichten Objekts ist ein Einweg-Hash aus (Publikation, Quelle). Ein Empfänger, der zwei Publikationen von mir hält, kann nicht erkennen, welche Notizen sich überschneiden. Ich habe die Eingaben, und das Manifest listet ohnehin jedes veröffentlichte Objekt — die Abbildung wird also vorwärts gebaut und rückwärts gelesen, ohne einen zweiten gespeicherten Zustand, der vom ersten abdriften könnte.
Namen sind Behauptungen, keine Identitäten
Ein @ im Kommentar bietet die Mitglieder der Workspace an und schreibt den gewählten Namen als gewöhnlichen Text in den Text. Das ist das ganze Speicherformat. Erwähnungen sind nie ein Feld.
Eine gespeicherte Id-Liste wäre eine zweite Wahrheit, der der sichtbare Text in dem Moment widersprechen kann, in dem sich jemand umbenennt. Stattdessen abgeleitet: benennt sich ein Mitglied um, folgen alte Kommentare dem neuen Namen; wird es entfernt, fällt die Hervorhebung weg, während der getippte Text genau so stehen bleibt, wie er geschrieben wurde.
Bleibt der Fall, dass zwei Mitglieder einen Namen teilen. Die Darstellung muss sich für einen entscheiden und tut das deterministisch. Der Benachrichtigungspfad liefert jedes Mitglied, das der Name meinen könnte — einen von zweien zu erreichen und dabei auszusehen, als hätte man den richtigen erreicht, ist der schlimmere der beiden Fehler.
Eine Löschung, die gelöscht bleibt
Gehört nicht zum Gesprächs-Thema, war aber der Bericht, der mich am meisten geärgert hat, also kommt er mit.
Eine bestätigte Löschung erreichte die anderen Geräte nie als Absicht. Sie sahen nur Dateien, die in der entfernten Auflistung fehlten — und genau so sieht eine kaputte Auflistung aus. Also tat der Wächter auf der Pull-Seite seine Arbeit: er hielt sie, behielt sie lokal und lud sie im nächsten Zyklus wieder hoch. Die Löschung kam auf jedem Gerät zurück, und der Wächter hatte keinen Ausgang.
Das Seitenband trägt jetzt ein Lösch-Journal, das jedes Gerät vor dem Abgleich einliest. Was das Journal erklärt, wird nachvollzogen, egal wie viele Dateien es sind. Was es nicht erklärt, bleibt bewacht wie zuvor — aber der Wächter fragt jetzt, mit zwei Antworten: hier auch löschen, oder die lokalen Kopien behalten und wieder hochladen. Ein Wächter ohne Ausweg ist eine Vermutung, die zur Regel befördert wurde.
Was nicht drin ist
Die Kryptografie hinter den verschlüsselten Workspaces hat weiterhin kein unabhängiges Review. Das galt vorher schon, und es wiegt jetzt schwerer: mit Publikationen verlässt Inhalt Dein Gerät in Richtung anderer Menschen. Die Funktion ist in der App als experimentell gekennzeichnet, und das ist ernst gemeint — sie ist eine Vorschau, kein Ort für etwas Vertrauliches. Das Review ist ein Tor, das ich aufgeschrieben habe und um das ich mich nicht herumreden werde.
Ebenfalls offen: die Signatur-Gegenprobe mit einem absichtlich manipulierten Update-Artefakt und der SMB-Kill-Drill am Netzlaufwerk.
Die Geräte-Abnahme des externen Vault-Ordners läuft, während ich das veröffentliche. Das ist ein benanntes Risiko, kein Versehen — aber benannt ist es.