Ihr Name ist Auslieferungsstand: Wiederherstellen stellt ihn mit her, der
Neu-laden-Vergleich prüft ihn, und ein umbenanntes, aber unverändertes
Beispiel bekäme weiter still neue Fassungen nachgezogen (das Nachziehen
hängt an id + Text-Fingerabdruck, nicht am Namen). Kein Stift an
mitgelieferten Zeilen; renameDoc() prüft es selbst. Löschen bleibt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Das Zeilen-Symbol im Dokumenten-Menü konnte ein UNGEÖFFNETES mitgeliefertes
Dokument zurücksetzen — ohne zu sehen, was man verwirft (Nutzer-Einwand).
Umbenennen und Löschen bleiben je Zeile; restoreDoc() wirkt nur noch auf das
geöffnete Dokument und prüft das selbst.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Auf einer Instanz ohne eigenes Backend (GitHub Pages) endete Teilen mit
HTTP 405: Die Vorgabe "eigene Herkunft" (D76-Nachtrag 8) stimmt nur auf der
produktiven Installation. serverBaseOrAsk() prüft die Vorgabe jetzt mit
GET /api/v1/info (D77), bevor gePOSTet wird, und fragt sonst nach der
Server-Adresse; gemerkt wird nur eine Adresse, die die Probe besteht.
Von Pages aus trägt man werkbaum.javagil.de ein — CORS erlaubt es.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Der Dokument-Wähler steht jetzt als gerahmter Chip "Werkbaum › Name" im
App-Kopf und ist damit auch auf dem Telefon aus beiden Bereichen erreichbar;
sein Name übernimmt dort die Untertitel-Zeile. Das Menü gruppiert nach
Dokumentart (docKind, headless getestet), trägt Umbenennen/Löschen/
Wiederherstellen als Symbole je Zeile und scrollt als Ganzes, statt auf
kleinen Bildschirmen abgeschnitten zu werden. Die Editor-Titelzeile heißt
wieder "Text-Editor" und trägt die Stand-Funktionen des aktiven Dokuments:
Speichern (Strg+S), Stand sichern, frühere Stände, Neu laden (Original/URL/
Datei) und den neuen kurzen Teilen-Knopf (vorher "Auf den Server legen" im
Menü). subtitleShort ist ersatzlos ausgebaut.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wer bei offenem `?live=` umschaltet, behielt die alte Adresse — optisch
falsch, und ein Neuladen holte das falsche Dokument zurück. Der Parameter
gehört jetzt zum aktiven Dokument, für `?live=` wie für `?sourceUrl=`;
`?etherpad=` wird nur noch weggeräumt. Fremde Parameter (`?server=`)
bleiben wörtlich stehen, damit die URL lesbar bleibt.
Dabei gefunden: Die Live-Sitzung lief weiter, während ein anderes Dokument
vorn stand — `setLiveText()` schrieb fremde Änderungen in dessen Text. Die
Sitzung gehört jetzt dem sichtbaren Dokument: Umschalten beendet sie,
Umschalten auf ein Server-Dokument nimmt sie auf. Was noch im Debounce
steckt, geht vorher raus, und `pushLive()` hält seine Sitzung fest statt
anzunehmen, dass sich über ein `await` hinweg nichts ändert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gemeldet: ~3 s Verzoegerung zwischen zwei Browsern. Zerlegt statt geraten -
von 1,73 s gemessenem Weg A->B entfallen 1,67 s auf die Wartezeit vor dem
Senden. Alles andere sind zusammen ~70 ms.
Zwei Verdaechtige sind freigesprochen: Der Server weckt den wartenden Feed
39 ms nach dem PATCH (isoliert per curl, ohne Browser), und der Apache der
produktiven Instanz haelt den Long-Poll die vollen 25 s durch und schliesst
sauber mit 204 - kein Fenster ohne offenen Feed, kein 5-Sekunden-Fehlerpfad.
Produktiv kommen ~130 ms Rundlauf je Anfrage dazu.
Der Debounce bleibt ein Debounce (kein Takt): Wer durchtippt, erzeugt
weiterhin keine Version. Der Grund fuer die 1,5 s stammte aus der
Rate-Limit-Disziplin des Etherpad-Konzepts - und Etherpad ist ausgebaut (D78).
Die Aufbewahrung zahlt die haeufigeren Pushes: Jede Version speichert den
ganzen Text, und die Frist entscheidet einzig, ob ein zurueckgefallener Client
ein Diff oder den Volltext bekommt. Nutzersichtbar sind die Meilensteine, und
die werden nie verdichtet; zurueckfallen kann nur ein ruhender Feed
(Hintergrund-Tab). Zusammen sinkt die Spitze je aktiv getipptem Dokument von
115 MB auf 24 MB (49-kB-Plan, Dauertippen).
Dabei gefunden: Eine Schreibpause laenger als die Frist war mit einer Stunde
der Ausnahmefall und ist mit fuenf Minuten der Normalfall. Dass die letzte
Sync-Version davor nicht verlorengeht, haengt allein daran, dass
recordHistory() zuerst befoerdert und danach verdichtet - sonst loeschte die
Verdichtung genau den Stand, den die Befoerderung gleich zum Meilenstein
gemacht haette. Die Reihenfolge hat jetzt eine Zusicherung; vertauscht faellt
genau der danach benannte Test.
Werkzeuggrenze notiert: Der Automatisierungs-Browser zeigt seine Flaeche nicht
an, Chrome drosselt Timer verborgener Seiten auf 1 Hz (gemessen: ein blanker
setTimeout(600) feuert nach 999-1053 ms). Ein Sub-Sekunden-Debounce ist dort
grundsaetzlich nicht messbar.
501 Frontend-Tests, 139 Backend-Tests.
D31 lieh sich fuer die Echtzeit-Zusammenarbeit ein Etherpad: Das Pad war die
Schreibflaeche, Werkbaum die Ansicht. Richtig fuer den Zeitpunkt - die schwere
Arbeit war dort getan, und Werkbaum hatte kein Backend. Jetzt hat es eins
(D76), das dasselbe besser und im Editor selbst kann.
D31 bleibt stehen: Was dort gemessen wurde, ist der Grund, warum die Anbindung
nie gut wurde - Export-Drosselung (10 je 90 s), SameSite=Lax beim
Autoren-Cookie, abgelehnter Socket-Upgrade. Uebrig blieb ein Neu-laden-Knopf
und ein schreibgeschuetztes Textfeld.
Ein alter ?etherpad=-Link meldet sich, statt still nichts zu tun: Der Parameter
bleibt erkannt und ergibt die zeilenlose Warnung padGone, die auf ?live= zeigt
(neun Sprachen). Geholt wird nichts - nachgemessen geht keine Anfrage mehr an
den Pad-Host. Vorhandene Pad-Dokumente bleiben liegen und werden dabei besser:
ohne Schreibschutz sind sie bearbeitbar und sammeln fruehere Staende.
Mit ausgebaut, weil sie nur der Anbindung dienten: remote.js samt Tests, der
Ansichts-Waehler und sein Splitter, der Neu-laden-Knopf samt
Drosselungs-Zaehler, die Warnungen sourceTimeout/padRateLimit, elf i18n-
Schluessel x 9 Sprachen - und src.readOnly ganz, denn nur Pad-Dokumente haben
es gesetzt (mit ihm updateSnapBtn und sieben tote Waechter). Auch #srcArea
faellt weg: den Kasten gab es nur, damit Pad-Rahmen und Spiegel sich einen
Bereich teilen. ?sourceUrl= bleibt unangetastet - beide teilten sich einen
Fetch-Pfad, das war die Stelle, an der nichts verrutschen durfte.
Im Plan wird aus dem #col.pad-Zweig (14 Knoten samt der ganzen
"Update by itself"-Gruppe) ein verworfener Knoten mit Begruendung; der
gemeinsame Zeigefinger wandert als #col.point heraus, denn !!! gilt jedem
geteilten Dokument. 196 Knoten, 0 Warnungen.
Nebengewinn: Der README bekommt endlich den Abschnitt zu ?live= - den gab es
nie, die Einstiegs-Beschreibung stand weiter beim Pad.
Nachgemessen im Browser: ?etherpad= zeigt die Warnung ohne Netz-Anfrage,
?sourceUrl= laedt unveraendert und ist beschreibbar, Textfeld und
Zahlenstreifen liegen buendig, Legenden-Splitter teilt wie zuvor (954+10+300),
Falten und Staende funktionieren. 501 Tests, 946 Zeilen weniger.
Gemeldet: zwei Browser am selben Dokument, einen Knoten zuklappen, und es
kommt "Someone changed the same lines. Whose version should win?".
Der Feed beantwortet "was ist seit Version N geschehen" - und wer da
mitgeschrieben hat, steht nicht in der Frage. Er liefert also die EIGENE
Aenderung zurueck, und wacht er im Moment des eigenen Sendens auf, kommt sie
an, bevor die Antwort darauf da ist. Die Schattenkopie steht dann noch auf dem
Stand davor: Der Client haelt die eigene Aenderung fuer fremd, sieht sie sich
mit dem eigenen Text ueberschneiden und fragt. Die Erkennung hatte recht,
falsch war nur, wen sie fuer den anderen hielt. Der zweite Browser ist dafuer
gar nicht noetig; das Falten ist nur die kuerzeste Geste, die eine ganze Zeile
aendert.
Auf localhost liegen PATCH- und Feed-Antwort 7 ms auseinander und die
PATCH-Antwort gewinnt - der Fehler tritt dort nie auf. Reproduziert mit im
Client um 500 ms verzoegerter PATCH-Antwort (eine Reihenfolge, die uebers Netz
jederzeit auftritt): PATCH an 200 / FEED an 200 / KONFLIKT-BANNER.
Behoben in feedAction() (live.js) - dort steht ohnehin, wann eine Feed-Antwort
angewendet werden darf; Gegenprobe: Sperre entfernt => genau die zwei neuen
Zusicherungen fallen. Dieselbe Sperre gehoert in die runFeed-Schleife, sonst
fragt sie sofort wieder und dreht eine enge Runde uebers Netz.
Dabei gefunden: pushLive() las seine Basis erst NACH dem await und nahm damit
an, dass sich dazwischen nichts aendert - der Feed brach genau die Annahme und
haette die eigene Aenderung ein zweites Mal aufgerechnet. Jetzt vorher
festgehalten.
Nachgemessen: Falten erzeugt kein Banner mehr, fremde Aenderungen kommen
weiterhin an, und der ECHTE Konflikt wird weiterhin erkannt (A haelt
ungesendeten Text auf Zeile 1, B aendert dieselbe Zeile). 525 Tests.
Bis hierher kam ein Plan nur per curl auf den Server — der Menueintrag war im
Konzept vorgesehen und fehlte. Jetzt legt der Knopf das aktive Dokument an,
schaltet dorthin um und schreibt den Link in die Adresszeile und in die
Zwischenablage. Die Adresszeile IST der Link: dort sucht man ihn, und ein
Neuladen fuehrt ins selbe Dokument zurueck.
Die Basis-Adresse ist die eigene Herkunft — produktiv liegt das Backend hinter
derselben Domain, wer nichts konfiguriert bekommt also das Richtige. Darueber
liegen ?server= (Entwicklung) und die Adresse des offenen Server-Dokuments.
Traegt nichts, wird gefragt.
Das lokale Dokument bleibt: Wer sein einziges Exemplar einem Server
anvertraut, soll es nicht im selben Zug verlieren. Dafuer nennt der Waehler
jetzt den Host neben Server-Dokumenten — im Test standen sonst zwei Eintraege
"Nur lokal" da, unterscheidbar nur am Tooltip. Und der Knopf verschwindet bei
Dokumenten, die schon auf einem Server liegen.
Geprueft im Browser gegen ein echtes Backend: Knopf sichtbar bei einem lokalen
Dokument, nach dem Klick steht ?live=... in der Adresszeile, das lokale
Dokument ist noch da, der Knopf verborgen — und eine getippte Zeile erreicht
den Server als Version 2.
523 Frontend-Tests (5 neue fuer serverBase/documentsUrl).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Editor fuehrt ein Dokument des Backends: laden, nach 1,5 s Ruhe das Diff
schicken, ueber einen offenen Abruf fremde Aenderungen einspielen — ohne
Neuladen, mit mitwandernder Schreibmarke. Dazu CORS im Backend; ohne das
blockiert der Browser jeden Aufruf.
Zwei Fehler hat erst der Live-Test gegen das laufende Backend gefunden, beide
an der Naht zwischen Modul und Verdrahtung (D54-Nachtrag 3):
Der Konflikt entstand nie. Mit laufendem Feed zieht die Schattenkopie staendig
nach, die eigene Basis ist also nie veraltet — der Server haette nie 409
geantwortet, und die fremde Zeile waere stillschweigend ueberschrieben worden.
Der Client prueft die Ueberschneidung jetzt selbst gegen den ungesendeten
Text; den kennt der Server nicht.
Die Nummer begann nach jedem Neuladen wieder bei 1, waehrend die Kennung
blieb — der Server hielt die erste echte Aenderung fuer eine Wiederholung und
tat nichts. Beides liegt jetzt im sessionStorage: je Tab, ueberlebt Neuladen.
Je Tab ist zugleich die richtige Aussage, zwei Tabs sind zwei Schreiber.
Nachgemessen im Browser: fremde Aenderung erscheint ohne Neuladen, Getipptes
erreicht den Server, beide Konflikt-Knoepfe tun was sie sagen, und die
Schreibmarke steht nach zwei fremd eingefuegten Zeilen darueber unveraendert
bei Zeile+2, Spalte 8. 518 Frontend-Tests, 135 im Backend.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die entscheidbare Haelfte des Live-Editing-Clients (D76), headless und
geprueft: ?live=-Adressen normalisieren, Zeilen-Diff berechnen und anwenden,
die Cursor-Zeile durch fremde Aenderungen mitfuehren, und die Regel, wann eine
Feed-Antwort ueberhaupt angewendet werden darf.
Zerlegen und Hashen liegen hier und nicht verstreut in app.js: Beide Seiten
muessen Text gleich in Zeilen zerlegen, sonst zeigen die Indizes auseinander.
Das Diff-Modell ist dasselbe wie im Backend (de.werkbaum.diff.LineDiff).
Die Cursor-Rechnung ist der Teil, ohne den "kein Neuladen" nichts wert waere:
Ohne sie spraenge die Schreibmarke bei jeder fremden Aenderung weiter oben im
Dokument.
518 Tests (31 neu). Gegenprobe: Feed-Basis nicht geprueft, Zeile im Eingriff
wie darunter behandelt, Protokoll nicht geprueft -> es faellt jeweils genau
die danach benannte.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Abhängigkeits-Kanten enden am nächsten sichtbaren Vorfahren, für Quelle
wie Ziel; zusammengefallene Kanten entfallen bzw. werden dedupliziert
(data-sub-ids/data-sub-deps am eingeklappten Vertreter, SPEC §9)
- Umschalter für die Querverbindungen neben dem Günstigster-Pfad-Knopf,
Voreinstellung an, persistiert; Export und Druck folgen ihm
- Der Falt-Knopf schaltet durch: M und kleiner zu → alles abseits des
Pfads zu → alles zu → alles offen; Icon/Tooltip zeigen den nächsten
Schritt, Handfaltung setzt die Position zurück (presetFoldSet, model.js)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Nur in Brave (navigator.brave) bekommt das Datei-Banner eine zweite Zeile
mit brave://flags/#file-system-access-api als kopierbarem Code-Text —
verlinken lassen sich interne Schemata nicht. i18n in neun Sprachen, die
Adresse selbst steht einmal im Code.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Brave ist Chromium-basiert, schaltet File System Access aber bewusst ab —
die Begründung „Chromium-Browser schreiben direkt zurück" widersprach dort
dem eigenen Erleben. Alle 18 UI-Strings (fsNotice + hint_save, 9 Sprachen)
nennen jetzt die Schnittstelle und Beispiel-Browser (Chrome/Edge).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Firefox/Safari laden Dateien als Kopie und speichern als Download — beim
ersten Öffnen/Speichern erklärt ein Banner die Grenze (i18n in neun
Sprachen, „Verstanden" merkt der localStorage, Reset räumt mit).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mit bekanntem, aber nicht beschreibbarem Handle bekommt showSaveFilePicker
startIn + den exakten Dateinamen; ohne Handle teilen sich Öffnen und
Speichern eine Picker-id, damit der Dialog im Plan-Ordner aufgeht statt in
Downloads. Der Abbruch-Kreislauf (Dialog schlägt Falsches vor -> Abbruch ->
kein Handle gemerkt -> wieder Dialog) ist damit durchbrochen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Die Geste, die jeder zuerst versucht, tat bisher das Falsche (Browser-Dialog
„Seite speichern"). Jetzt ruft sie saveLocalFile(): mit gemerktem Handle
dialogfrei in dieselbe Datei, sichtbar bestätigt durch einen Petrol-Haken am
Dokumentnamen; ohne Handle wie der Menü-Eintrag. Legenden-Zeile hint_save in
neun Sprachen. Nebenbefund behoben: adoptFile() wartet jetzt auf die aus
IndexedDB zurückgeholten Handles — ein Doppelklick unmittelbar nach dem
App-Start legte sonst ein Duplikat an statt dasselbe Dokument zu
aktualisieren.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Manifest mit Icons aus der Marke und Standalone-Fenster; ein bewusst dummer
Service Worker beantwortet nur die App-Navigation network-first und hält die
zuletzt gesehene Fassung für den Offline-Fall — die Update-Prüfung (D45)
bleibt dadurch unverändert wahr, der skipWaiting-Lebenszyklus entfällt. Die
installierte App registriert sich für .werkbaum-Dateien (file_handlers +
launchQueue -> adoptFile). Beide Deploy-Wege kopieren die App-Hülle mit;
.htaccess liefert .webmanifest mit MIME-Typ aus.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
File System Access API, wo vorhanden (Chromium): Öffnen merkt sich das
FileSystemFileHandle (IndexedDB, überlebt den Neustart), Speichern schreibt
in dieselbe Datei; dieselbe Datei öffnet per isSameEntry wieder in dasselbe
Dokument. Firefox/Safari behalten den Stufe-1-Download.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Zwei Einträge im Dokumenten-Menü: „Datei öffnen…" (Datei-Input, neues
Dokument, Dateiname als Name) und „Als Datei speichern" (Blob-Download,
Dateiname aus dem Dokumentnamen via localfile.js). Läuft in jedem Browser;
Stufe 2 (File System Access, Chromium) folgt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Trägt ein @name mehr als die Hälfte der offenen Arbeit des günstigsten
Pfads (Marginalkosten-Maß, D69) und tragen mindestens zwei Personen Last
auf der offenen Front, meldet die zeilenlose Warnung assigneeOverload die
mögliche Engstelle; die Pillen der Person wechseln an offenen Pfad-Knoten
auf die Warnfarbe. Knoten ohne Tags erben für die Rechnung vom nächsten
getaggten Vorfahren, mehrere Tags teilen sich den Beitrag.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Die D66-Schätzung war status-blind: Ein größenloses Paket mit [x] (L) und
[ ] (S) wurde als L geschätzt, obwohl nur S offen ist. Erledigte Kinder
fallen jetzt heraus, eine erledigte realisierte Alternative stellt ihre
Gruppe fertig, und sind alle benannten Kinder erledigt, gilt XS (der eigene
Rest — M-Rückfall erhöhte den Preis beim Fertigwerden). Angegebene Größen
bleiben unangetastet (D69).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Die fast transparente Füllung mit mattgrauer Glyphe verschwand auf den
acht Status-Pastelltönen. Nur Farben geändert; line-height und Padding
bleiben (23-px-Geometrie).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Der Preis eines Knotens ist seine angegebene (oder D66-geschätzte) Größe;
die frühere Rekursion (eigene Größe plus Summe/Minimum der Kinder) zählte
doppelt und bestrafte sorgfältig zerlegte Pakete — eine zerlegte
S-Alternative verlor gegen ein grobes L. Das Vereinigungs-Maß der
Closure-Suche (D42) zählt je Knoten nur noch die Marge über seine nötigen
Teilpakete hinaus. Mitgelieferter Plan wählt identisch (per Vergleich
gemessen); SPEC §9 und llms.md nachgezogen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Erkannt nur alleinstehend angesetzt (wie #id und :#…) — damit greifen die
Zitier-Konventionen "(L)" und ((L)) von selbst, und Backend(L) bleibt
Label. Das letzte Token gewinnt statt des ersten: Die Größe steht hinter
dem Titel, frühere Vorkommen sind Text. SPEC §1/§5 und llms.md nachgezogen;
mitgelieferte Beispiele unverändert (Parse-Vergleich alt/neu).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Vor dem `:#…`-Token prüfte depIdAt nur Space/Tab, der Parser jedes \s —
vor einem geschützten Leerzeichen war das Token damit eine Abhängigkeit,
auf die Strg+Klick nichts tat. Test mit echtem NBSP; Gegenprobe: die alte
Prüfung lässt genau ihn fallen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Strg/Cmd+Klick auf eine Abhängigkeits-ID im Textfeld — auch jede ID der
Liste und die Kopf-Form #auth:#db — markiert die Zeile, die die ID vergibt;
Tastatur-Pendant Strg+Enter. Erste Vergabe gewinnt (D36/D39), unbekannte IDs
tun still nichts. Headless als depIdAt/idLine in autocomplete.js, 16 Tests;
Legenden-Zeile hint_jump in allen neun Sprachen erweitert.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mindestens die größte Größe der zählenden Kinder, ab drei Kindern dieser
Größe eine Stufe mehr (Deckel XXL); disjunktiv die kleinste Alternative,
rekursiv über größenlose Kinder, Blatt bleibt M. Badge, Tooltip und
aria-label zeigen die geschätzte Größe („mindestens {size} angenommen").
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lange Titel zogen das Diagramm zu breit. Ein Parameter in wrapLabel(),
der 32ch-Rückhalt im CSS und SPEC §9 ziehen mit; Balance-Regel und
Erste-Zeile-Verankerung unverändert.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Zwei Geometrie-Fehler bei Gruppen mit wenigen Kindern: :only-child schaltete
die Leiste ab, obwohl Stub (50 %) und Abzweig (23 px) seit dem 20-px-
Zusatzabstand nicht mehr zusammenfallen (6,6–15,8 px Lücke); und bei einem
großen Teilbaum im letzten Kind endete die Leiste über dem Stub (bis 98 px).
Fix: CSS-Verbinder 23px→50 % am Einzelkind, alignVRails() + --vrail-ext für
das letzte Kind. Export und Kompakt-Modus waren nie betroffen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Der High-Risk-Status fehlte seit Juli in der Chip-Reihe der Legende;
hint_break nennt jetzt die Größen-Prüfung (D62), in allen neun Sprachen.
llms.md: Alignment-Marker an beiden Tabellen, die Größen-Bereiche als eigene
bündige Tabelle statt Prosa — inhaltlich war die Agenten-Fassung auf Stand
(Fortsetzungszeilen, ID-als-Titel, Größen-Konflikt, angefangene Zugaben).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lange Knotentitel brechen bei ~40 Zeichen um, gleichmäßig auf die Zeilen
verteilt — vom Renderer (wrapLabel, headless getestet) statt text-wrap:balance,
damit der Kasten auf die längste balancierte Zeile schrumpft. Der
23-px-Abzweig ankert an der ersten Zeile; der Optional-Kreis sitzt fest auf
der Abzweighöhe. Der Grafikexport misst die gerenderten Zeilen (labelLines)
und gibt je Zeile ein <text> aus. Dazu: die eingeblendete ID (D56) steht in
einer eigenen Zeile über dem Titel, und das Falt-Zeichen ist ein gerahmter
19×16-px-Chip mit gedeckelter Höhe. Plan: #ed.render.wrap, #ed.render M→L
(D62-Prüfung schlug korrekt an).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wer :# tippt, bekommt die vergebenen IDs als Liste an der Schreibmarke:
Präfix- vor Teilstring-Treffern, Knotentitel als Kontext, schon gelistete
und die eigene ID ausgenommen. ↑/↓ wählt, Enter/Tab übernimmt (undo-fähig),
Esc schließt; Live-Region für Screenreader. Eingabehilfe wie die Kurzform
(D55) — der Parser sieht nie etwas davon. Regeln headless in autocomplete.js
(20 Tests), app.js verdrahtet Popup, Tasten und Einfügen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Liegt der Knoten der Cursor-Zeile in einem eingeklappten Teilbaum, hebt
nodeOfLine() den nächsten sichtbaren Vorfahren hervor — Cursor-Mitlaufen,
Alt+Klick, Alt+Enter und der Mobil-Umschalter erben es über die eine
Auflösungsstelle. Ausgeblendete verworfene Elemente heben weiterhin nichts
hervor (SPEC §9). Zeilen-Map als lineTargets() headless in model.js, 5 neue
Tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Größen zählen für diese eine Prüfung als Bereiche (Untergrenzen verdoppeln
sich, XXL nach oben offen); Konflikt erst, wenn selbst die günstigste Lesart
der Kinder die großzügigste des Elternknotens erreicht. Warnung sizeConflict
an der Elternzeile, Badge in --warn, Grund in Tooltip und aria-label. Nichts
wird automatisch korrigiert. Nebenbefund behoben: der Grafikexport zeichnete
das Größen-Badge mit festen Farben — das invertierte implizite M stand seit
jeher gefüllt im Bild. Die acht berechtigten Konflikte im eigenen Plan sind
nachgezogen. 372 Tests, 16 neue.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Die Basis-Kanten lagen schon richtig; „drüber gemalt" wurde allein die
Hervorhebung des ausgewählten Knotens (`svg.dep-front`, z-index 4). Eine
durchgezogene Linie quer über einen fremden Knoten durchstreicht dessen
Titel — und beim Tippen wechselt die Hervorhebung mit jeder Zeile.
`dep-front` entfällt; hervorgehobene Kanten hängen zuletzt in derselben
hinteren Ebene, liegen damit über den übrigen Kanten und unter jedem
Knoten. Export und Druck waren nie betroffen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`#.kc` wird jetzt aufgelöst, sobald ein Doppelpunkt die ID abschließt —
beim Tippen des Doppelpunkts ebenso wie beim nächsten Tastendruck in
einer Zeile, die ihn schon trägt. Ohne Doppelpunkt bleibt es beim
Verlassen der Zeile.
Der Cursor allein löst weiterhin nichts aus: Angefasst wird nur die
Zeile, in der auch getippt wurde (D55/D27).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Stiel aus dem Elternknoten sitzt bei 50 % der Gruppe, der Stiel zum
Kind bei `--stem-x` (der gemessenen Knotenmitte, Nachtrag 2). Für
`li:only-child` ist die waagerechte Sammelleiste abgeschaltet — richtig,
solange beide zusammenfallen, und das tun sie nur, solange der Knoten in
seiner Zelle zentriert steht. Bei einer Treppe darunter sind es gemessen
8,5 px Versatz, bei einem `li.has-or` als einzigem Kind 41,8 px.
Jetzt verbindet ein Leiterstück genau zwischen 50 % und `--stem-x` die
beiden — dieselbe L-Form wie bei einem äußeren Kind einer größeren
Gruppe; ohne `--stem-x` ist es 0 px breit und unsichtbar.
Dabei aufgefallen: `:only-child::after{border-left}` stand vor
`:last-child::after{border:0 none}` und war bei gleicher Spezifität seit
jeher wirkungslos. Der ganze Block steht nun dahinter.
Kein Rückschritt von D61 — mit ausgeschaltetem Pfad derselbe Versatz;
die Zugabe steht seither nur in voller Farbe statt blass da. Der
Grafikexport war nie betroffen (die Leiste schließt dort die Elternmitte
ein).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eine Zugabe (`+`) fällt aus dem günstigsten Pfad — außer sie ist
realisiert und noch nicht erledigt (`[~]`/`[/]`). Angefangene Arbeit ist
die offene Front, und seit D46 zeigt der Pfad genau die; bisher führte
der Von-Station-zu-Station-Knopf nie dorthin, wo gearbeitet wird.
Erledigte Zugaben bleiben draußen: dort ist nichts mehr zu tun, und was
darunter offen blieb, ist mit ihnen zusammen entbehrlich (§3).
Dabei die fehlende Hälfte von D46 gefunden: SPEC §9 sagt seit jeher
„eine bereits realisierte Alternative gewinnt", umgesetzt war das aber
nur über die Kosten — und die sind allein bei `[x]`/`[^]` null. Ein
`[~]`-Zweig verlor damit gegen eine unangetastete billigere, obwohl die
XOR-Regel gerade ihn als die realisierte Alternative ausweist.
`chosenPool()` schränkt die Wahlmenge auf die realisierten ein; unter
mehreren entscheiden wieder die Kosten. Nebengewinn: eine entschiedene
Gruppe ist keine freie Variable mehr und verkleinert die Suche (D42).
10 neue Tests; Gegenprobe geführt (jede Regel einzeln herausgenommen
lässt genau die nach ihr benannten Zusicherungen fallen). Der
mitgelieferte Plan ändert sich nicht — er hat keine angefangene Zugabe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`- #US-123` ergab bisher nichts — leeres Label, Zeile ignoriert, ID nicht
vergeben. Wo die Kennung schon der Name ist (Ticket-Referenzen, §11), war das
verkehrt herum: Wer den Titel nicht danebenschrieb, verlor den ganzen Knoten.
Jetzt wird `#id` das Label, mit Doppelpunkt geschrieben wie ohne.
Das `#` bleibt im Label — es sagt „hier steht die Adresse, weil es keinen
Titel gibt". Der `#`-Umschalter setzt bei so einem Knoten nichts davor
(sonst `#US-123: #US-123`), und Tooltip wie aria-label lassen die ID weg, die
schon im Titel steht.
Verhaltensänderung: Die ID ist damit vergeben — `- #auth` gefolgt von
`- [ ] Echt #auth` gibt jetzt zwei Knoten und eine duplicateId-Warnung.
SPEC §1, llms.md, D60; 6 neue Tests, der alte auf die neue Regel umgeschrieben.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eine Zeile trägt alles auf einmal — im mitgelieferten Plan ist die längste
122 Zeichen lang, und seit das Textfeld nicht mehr umbricht (D49) muss man
dafür schieben. Ein `\` am Zeilenende verteilt sie jetzt auf mehrere
Textzeilen, ohne dass ein neuer Knoten entsteht.
Leerraum davor ist Pflicht (`… \`, nicht `…\`) — bewusst gegen die
Shell-Gewohnheit: Ohne die Regel verschluckt ein Label wie `C:\temp\` stumm
den folgenden Knoten, mit ihr bekommt man nur keine Fortsetzung, was man
sofort sieht. Der laute Fehler ist der bessere.
Umgesetzt als Vor-Durchlauf `logicalLines()`: Die Folgezeile ist Teil der
Zeile, bevor irgendetwas aus ihr gelesen wird — die Extraktionsreihenfolge
aus §1 bleibt unangetastet. Alles gehört zur ersten Zeile: Einrückung,
Warnungs-Zeilennummer, Rückschreiber; der Cursor in der Fortsetzung wählt
ihren Knoten aus, wie bei Beschreibungszeilen.
SPEC §1/§9, llms.md, Legende in neun Sprachen, D59; 20 neue Tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wer die Oberfläche auf Japanisch stehen hat, sah einen japanischen Rahmen um
englische Sätze — das kann man für eine fehlende Übersetzung halten. Ein Satz
oben im Popup sagt jetzt, dass es Absicht ist, in allen neun Sprachen; klein
und grau, damit er hinter dem Inhalt zurücksteht, zu dem er gehört. Bei
englischer Oberfläche entfällt er.
SPEC §9 und D58-Nachtrag.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der „Was ist neu?"-Knopf stand im Diagramm-Kopf und war verborgen, solange es
nichts gab — er konnte also nur etwas über das offene Dokument sagen, und
meistens sagte er gar nichts. Jetzt steht er permanent in der oberen
Bedienleiste und öffnet ein Popup mit der Chronik der letzten Tage; je Tag
führt ein Link die betroffenen Knoten im Diagramm vor (dieselbe gelbe
Kranz-Ansicht wie D28, nur mit einer anderen Frage).
Zwei Quellen, jede in ihrer Rolle: docs/CHANGELOG.md sagt, WAS geschehen ist
(englisch — das Popup ist Oberfläche in neun Sprachen, die Commit-Betreffs
sind deutsch); die git-Historie des mitgelieferten Plans sagt, WELCHE Knoten
sich bewegt haben. Beides wird zur Bauzeit eingelesen (Vite-Plugin), zur
Laufzeit lädt Werkbaum weiterhin nichts nach.
Bernstein heißt ungesehen, Petrol heißt „wird gerade vorgeführt". Der
Besuchsvergleich (D28) steht als abgesetzter Abschnitt zuoberst im Popup und
trägt den „gesehen"-Knopf, den vorher der Knopf selbst war.
SPEC §9 (Neuigkeiten) und D58; 20 neue Tests, davon einer auf der
ausgelieferten CHANGELOG.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das `?` von `cursor:help` sagte nur „hier gibt es etwas zu lesen". Über der
orange gefärbten Zahl einer Warn-Zeile steht jetzt dasselbe ⚠, das im
Warnungsbereich vor jeder Meldung steht — der Zeiger sagt dieselbe Sache wie
das Ziel, auf das er zeigt.
Der Pfeil bleibt daneben stehen (sonst zeigte der Zeiger auf nichts), Spitze
auf dem Brennpunkt; als data:-URI im Stylesheet, `help` als Rückfall.
SPEC §9 (Zeilennummern) und D33-Nachtrag 2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das Fenster aus D52 war für Touch gebaut — damit standen zwei Darstellungen
desselben Inhalts nebeneinander, und ausgerechnet die schlechtere bekam der
Zeiger. Jetzt zeigt es überall: am Zeiger nach 350 ms beim Überfahren, bei
Tastaturfokus sofort, auf Touch unverändert beim Tipp.
Was ein `title` prinzipiell nicht kann:
- keine Absätze (die Beschreibungen im Plan brechen bei ~76 Zeichen, das sah
ausgefranst aus),
- keine Linie (der Trennstrich war aus 24 `─` GEMALT, D40-Nachtrag),
- nichts bei Tastaturfokus — wer mit Tab durch das Diagramm geht, sah bisher
keine Beschreibung. Nachgemessen mit ECHTEM Tastendruck: Fokus von
#not.line auf #not.status, Fenster folgt, 10 px unter dem Knoten, Ring am
fokussierten Knoten.
- Inhalt zieht von `title` nach `data-tip` (sonst zeigte der Browser seinen
eigenen zusätzlich) — dasselbe Attribut unter anderem Namen, keine
Verdopplung der 20 kB DOM-Text.
- Die letzten drei nativen Tooltips IN Knoten mussten weichen: Das
Warndreieck war redundant (Statusname steht in den Fakten), die Annahme
„Größe fehlt, gilt als M" wandert in die Faktenzeile. Nachgemessen: 0 von
161 Knoten und 0 Kinder tragen noch einen `title`.
- Absicht-Erkennung: 350 ms Verzögerung, beim Wechsel zwischen Knoten ohne
Warten, 120 ms Nachfrist beim Verlassen (der Weg ins Fenster führt über
einen Zwischenraum).
- `aria-hidden` am Fenster: Der Inhalt steht schon im aria-label, sonst würde
er doppelt vorgelesen; × und ↗ deshalb `tabindex="-1"`.
Werkzeuggrenze notiert: `element.focus()` feuert ohne Fensterfokus keine
Fokus-Ereignisse (`document.hasFocus()` false) — der erste Prüflauf zeigte
fälschlich „öffnet nichts".
SPEC §9 + D57; Plan: #ed.tipwindow [x] (161 Knoten, 0 Warnungen).
293 Tests grün, 5 Snapshots/Zusicherungen auf `data-tip` nachgezogen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die ID ist die Adresse eines Knotens — Ziel der Abhängigkeiten, Schlüssel der
Beschreibungsblöcke —, stand im Diagramm aber nur im Tooltip (D36). Jetzt
umschaltbar und geschrieben wie im Text: `#some.id: Titel`.
- Als RENDERER-Option, nicht per CSS versteckt: `diagramToSvg()` zieht den
Knotentext aus `textContent`, ein `display:none` hätte die IDs trotzdem in
jeden Export geschrieben. Nachgemessen im SVG: `>#not.line: Line format<`
mit Umschalter, `>Line format<` ohne.
- Zurückgenommen dargestellt (Mono wie im Textfeld, --muted), `aria-hidden` —
der Screenreader bekommt die ID über a11yId (D36). Zustand persistiert.
Das zehnte Bedienelement hat die Kopfzeile gekippt, zweimal dieselbe Rechnung
wie D17-Nachtrag 5:
- Schreibtisch: 738 nötig, 728 da -> 82 statt 49 px. Nicht die Knöpfe (566 px
für elf Elemente), sondern zehn Lücken à 14 px = 140 px. `gap:10` gibt
40 px zurück; danach 698 von 728 und wieder 49 px mit UND ohne Knopf.
- Telefon (375 px): alle Ziele schon auf 29 px, Lücken auf 6 — ein zehntes
passt arithmetisch nicht (386 gegen 359). D50s Umbruch-Schwelle wandert von
360 auf 440 px. Greift nur mit dem „Was ist neu?"-Knopf: mit allen zehn
78 px/zwei Reihen, ohne ihn 49 px/eine Reihe.
SPEC §9 + D56; Plan: #ed.showids [x] (160 Knoten, 0 Warnungen).
293 Tests grün (288+5); i18n `idsTooltip` in 9 Sprachen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gefragt war, ob `#.kc` unter `#prod-stage` als Notation `#prod-stage.kc`
bedeuten könnte. Als Notation wäre es falsch, und das zeigt der eigene Plan:
181 IDs im Baumteil, davon 50 (28 %), deren gepunktetes Präfix gar nicht die
Eltern-ID ist (`#ed.parser` hängt unter `#ed.live`) — die Punkte benennen den
BEREICH, nicht den Pfad, genau wie D48 es vorsieht. Dazu: der
Beschreibungsteil hinter `---` hat keinen Baum (zwei Schreibweisen für eine
ID), Einrücken änderte die Identität und bräche `:#…`-Verweise, und aus einer
Konvention würde Grammatik. Gewinn wären 802 Zeichen in 40 kB.
Also als Eingabehilfe: getippt wird `#.kc`, beim Verlassen der Zeile steht
`#prod-stage.kc` im Text — derselbe Griff, mit dem das Umklappen im Diagramm
seine Faltmarke zurückschreibt (D38-Nachtrag 2). Die Datei bleibt eindeutig,
durchsuchbar und umsortierbar; llms.md ist unberührt, weil sich an der
Notation nichts ändert.
- parser.js: `expandShortIds(text)` neben `setFoldMark`, 17 Tests.
- app.js: löst beim Zeilenwechsel auf, aber nur in der Zeile, in der auch
getippt wurde — ein bloß durchgeklicktes fremdes Dokument bleibt
zeichengenau stehen (sonst fiele es aus dem Nachziehen, D27).
Falle, zum zweiten Mal dieselbe: `execCommand` verweigert re-entrant im
`input`-Ereignis den Dienst, `replaceTextUndoable` fällt dann auf
`src.value =` zurück und löscht die Undo-Historie. Gemessen: erstes
Rückgängig ohne Wirkung, jedes weitere `false`. Jetzt einen Zug später
(`setTimeout 0`) und nur mit Fokus im Textfeld; danach nimmt ein Rückgängig
genau die Auflösung zurück.
SPEC §9 + D55; Plan: #ed.shortid [x] (159 Knoten, 0 Warnungen).
288 Tests grün (271+17).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
D54-Nachtrag 2 endete mit „nicht durch Tests gedeckt ... das ist der Preis
dafür, dass die Zustandslogik im UI-Modul sitzt". Der Preis war zu hoch: Der
Fehler kam bis in Produktion, und ein Test hätte ihn in einer Zeile
gefunden.
Geschnitten nach dem Vorbild von remote.js (D31) — die entscheidbare Hälfte
heraus, die I/O bleibt:
- snapshots.js: addSnapshot (wann entsteht ein Stand), dropOldestSnap /
persistSnaps (was fliegt bei Platzmangel), parseSnaps (was darf gelesen
werden), snapLabel. Speicher als {setItem, removeItem} und Uhr als Zahl
hereingereicht — genau die Abhängigkeiten, an denen die Prüfbarkeit
vorher scheiterte.
- app.js: -63/+14 Zeilen, snapshotNow() schrumpft auf sechs.
Die Gegenprobe zählt, nicht die Zahl der Tests: Baut man exakt den
ausgelieferten Fehler wieder ein (`manual ? null : base` -> `base`), fällt
GENAU die eine Zusicherung, die nach ihm benannt ist; die übrigen 27 bleiben
grün.
Zwei bewusste Verhaltensänderungen: parseSnaps wirft formfremde Einträge weg
(Liste kein Array, t keine Zahl, text kein String), und ein Array im
Speicher ergibt {} statt eines halb benutzbaren Zustands.
Verdrahtung bleibt Browser-Sache und wurde nachgemessen: Knopf ohne Eingabe
-> 1 Stand; Takt (2 s) bei bloß angesehenem Dokument -> 0, nach Änderung
-> 1, weitere Takte -> 1.
D54-Nachtrag 3 + Faustregel in frontend/CLAUDE.md. 271 Tests grün (243+28).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gemeldet: „I took a manual snapshot ... List is empty." Nachgestellt und
bestätigt — schlimmste Bauart: nichts gespeichert, Knopf bestätigte
trotzdem.
Ursache ist der Denkfehler aus D54-Nachtrag 1: `snapshotNow()` vergleicht
bei leerer Liste gegen `snapBase`, den Text beim Öffnen des Dokuments. Für
den Takt richtig (bloßes Ansehen sammelt nichts), für den Knopf falsch. Die
Begründung „bei unverändertem Text steht er schon oben in der Liste" gilt
eben nicht, wenn die Liste leer ist — und das ist genau der Anlass des
Knopfes: öffnen, sichern, DANN ändern. Die frühere Messung begann mit einer
Eingabe und prüfte damit die Wirkung, nicht den Anlass.
- `snapshotNow(manuell)`: Die snapBase-Sperre gilt nur noch für den Takt;
der Knopf vergleicht allein gegen den letzten Eintrag.
- `loadSnapshot()` sagt jetzt ebenfalls `true` — bewusstes Wegleg-Ereignis.
- `setInterval(() => snapshotNow(), …)`: Ein durchgereichtes Argument wäre
wahr und hebelte die Sperre aus (dieselbe Falle wie D17-Nachtrag 4).
Nachgemessen — Knopf: 10 Zeilen, Klick ohne jede Eingabe -> [10]; nach einer
Änderung -> [10,11]; ältesten laden -> zurück auf 10, kein Doppel-Eintrag.
Takt (2 s für die Prüfung): nur angesehen, 3 Takte -> 0; nach Änderung -> 1;
weitere Takte -> 1.
D54-Nachtrag 2. 243 Tests grün (die Stände selbst sind nicht test-gedeckt,
sie leben in app.js — Lücke benannt).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zehn Minuten sind der falsche Takt für den einen Moment, in dem man einen
Stand wirklich will: unmittelbar vor einer größeren Änderung. Dann ist der
letzte selbsttätige Stand im Zweifel neun Minuten alt.
- Kamera statt Plus: Ein `+` neben dem Dokument-Namen läse sich als „neues
Dokument“, ein Lesezeichen als „Favorit“. Gegen Uhr (Verlauf) und Caret
(Dokumentwahl) ist die Kamera eindeutig.
- Die Rückmeldung kommt IMMER, auch wenn nichts angelegt wurde: Der Knopf
verspricht „dein Stand ist gesichert“, nicht „ein Eintrag wurde erzeugt“.
Bei unverändertem Text steht er schon oben in der Liste — einen doppelten
Eintrag zu erzwingen kostete nur einen der 20 Plätze.
- Gezeigt per vorhandenem Haus-Idiom (`flashBtn`, 1,5 s Haken); dafür nur
`.copybtn.done .ic-copy` um `.ic-main` erweitert.
- Pad-Fall folgt von selbst: beide Knöpfe in EINER Zuweisung versteckt.
- i18n `snapAddTooltip` in 9 Sprachen; der Leer-Text des Menüs nennt jetzt
beide Wege.
Nachgemessen: 0 -> Klick -> 1 (Haken sichtbar, nach 1,7 s wieder Kamera);
zweiter Klick ohne Änderung -> weiterhin 1, Rückmeldung trotzdem; nach einer
Änderung -> 2. Im schmalen Layout kostet der Knopf nichts (Kopfhöhe 44 px
und Zeilenzahl mit und ohne ihn identisch).
D54-Nachtrag; Plan: #ed.snaps.manual [x] (158 Knoten, 0 Warnungen).
243 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>