+Anforderungskatalog für die Dokumentation von Forschungssoftware (Digital Humanities) +
+Ein Überblick und Best Practices für die Dokumantation von Forschungssoftware.
+commit ce0c52a66a27f8d484068dc26455d0ac9d0e4819
Author: Nicole Dresselhaus ',
+ ' ',
+ ' Die Dokumentation von Forschungssoftware ist entscheidend, um wissenschaftliche Ergebnisse nachvollziehbar und Software für andere nutzbar zu machen. Insbesondere in den Digital Humanities (etwa in der Geschichtswissenschaft) entwickeln Forschende neben Forschung und Lehre oft eigene Software – meist unter hohem Zeitdruck und ohne formale Ausbildung in Softwareentwicklung. Häufig bleibt die Dokumentation deshalb minimal oder unvollständig, was dazu führt, dass andere (und sogar die Autor*innen selbst) viel Zeit aufwenden müssen, um den Code zu verstehen und anzuwenden. Dabei gilt gute Dokumentation als zentrale Voraussetzung, um Forschungssoftware auffindbar, nachvollziehbar und wiederverwendbar zu machen. Dieser Anforderungskatalog richtet sich an Forschende, die keine Vollzeit-Programmierer sind, und soll wissenschaftlich fundierte Richtlinien für die Dokumentation von Forschungssoftware liefern. Die Empfehlungen berücksichtigen Best Practices des Research Software Engineering (RSE) und insbesondere die Prinzipien des Endings-Projekts für digitale Langlebigkeit (Endings Project 2020). Ziel ist es, ein praxistaugliches Gerüst bereitzustellen, das – trotz Zeitknappheit – die wesentlichen Dokumentationsaspekte abdeckt, um sowohl die Nachvollziehbarkeit der Ergebnisse als auch eine Weiterverwendung der Software zu ermöglichen. Im Folgenden werden die Anforderungen an Inhalt, Format und Umfang der Dokumentation definiert, geeignete (teil-)automatisierte Dokumentationswerkzeuge diskutiert und Best Practices in Form von Vorlagen und Checklisten vorgestellt. Ein zentrales Problem in der Dokumentation wissenschaftlicher Software ist oft das fehlende Big Picture, also eine klare Darstellung des Was und Warum. Die Dokumentation sollte daher alle Informationen abdecken, die zum Verstehen, Nutzen und Weiterentwickeln der Software nötig sind. Insbesondere sind folgende Inhalte essenziell: Beschreiben Sie was die Software tut und warum sie entwickelt wurde. Nennen Sie den wissenschaftlichen Zweck, das Forschungsproblem oder die Fragestellung, die mit der Software adressiert wird, sowie die Zielgruppe (wer soll sie nutzen?). Dieser Kontext hilft anderen, den Nutzen der Software einzuschätzen. Beispiel: “Dieses Tool extrahiert Personen-Netzwerke aus historischen Briefkorpora, um sozialwissenschaftliche Analysen zu ermöglichen.” Eine klare Problem- und Zielbeschreibung richtet sich auch nach dem Umfeld ähnlicher Lösungen – falls es bereits etablierte Tools gibt, sollte die Dokumentation die eigene Herangehensweise einordnen (z. B. was die Software anders oder besser macht). Dokumentieren Sie alle Eingabeformate, Ausgabedaten und verwendeten Datensätze. Nutzer*innen müssen wissen, welche Daten die Software erwartet (Dateiformate, Schnittstellen, Parameter) und welche Ergebnisse sie produziert. Idealerweise werden Beispiele angegeben: z. B. Beispiel-Dateien oder -Parameter und die korrespondierende Ausgabe. Falls die Software mit bestimmten Forschungsdaten arbeitet, beschreiben Sie diese Daten und ihre Struktur. Dies umfasst die Datenmodelle (etwa wichtige Felder, deren Bedeutung und kontrollierte Vokabulare) und Annahmen über die Daten. Gemäß den ENDINGS-Prinzipien sollte die Datenstruktur in einem statischen Dokument festgehalten und der Software beigelegt sein – so bleibt nachvollziehbar, wie die Software die Daten interpretiert. Eine Tabelle oder Auflistung der Eingabefelder und Ausgabegrößen mit kurzen Beschreibungen erhöht die Klarheit. Listen Sie alle Abhängigkeiten (Dependencies) der Software auf. Dazu gehören verwendete Programmiersprachen/Versionen, erforderliche Bibliotheken oder Frameworks, und sonstige Systemvoraussetzungen (z. B. Betriebssystem, Mindesthardware, Datenbank-Versionen). Wichtig ist, wie diese Abhängigkeiten installiert werden können. Optimal ist eine automatisierte Installationsroutine (z. B. ein Da es sich um Forschungssoftware handelt, sollten Sie den wissenschaftlichen Kontext offenlegen. Das heißt, erklären Sie die grundlegenden Methoden, Algorithmen oder Modelle, die in der Software umgesetzt sind, zumindest in Überblicksform. Verweisen Sie auf relevante Publikationen oder Theorien, damit andere die wissenschaftliche Grundlage nachvollziehen können. Beispielsweise: “Die Implementierung folgt dem Algorithmus von Müller et al. (2019) zur Netzwerkanalyse historischer Korrespondenz.” Halten Sie diesen Abschnitt aber prägnant – Details gehören in die Forschungsarbeit selbst. Wichtig ist, dass die Dokumentation den Brückenschlag zwischen Code und Forschung herstellt. Da viele Wissenschaftler*innen zentrale Aspekte lieber in ihren Artikeln dokumentieren, sollte in der Software-Dokumentation zumindest eine Zusammenfassung mit Querverweis erfolgen. So wissen Nutzer*innen, unter welchen Annahmen oder Theorien das Tool funktioniert. Geben Sie ehrlich Auskunft über die Grenzen der Software. Welche Fälle werden nicht abgedeckt? Welche Annahmen über die Daten oder Anwendungsszenarien werden getroffen? Dokumentieren Sie bekannte Probleme oder Einschränkungen (z. B. “funktioniert nur für Deutschsprachige Texte”, “maximale Datenmenge 1 Mio. Datensätze, da Speicherbegrenzung”). Solche Hinweise verhindern Fehlanwendungen und sparen Nutzern Zeit. Falls es bekannte Bugs oder Workarounds gibt, sollten diese ebenfalls (etwa in einer FAQ oder einem Abschnitt “Bekannte Probleme”) erwähnt werden. Eine transparente Auflistung von Limitationen erhöht die Vertrauenswürdigkeit und hilft anderen, die Ergebnisse richtig einzuordnen. Auch aussagekräftige Fehlermeldungen im Programm selbst sind eine Form von Dokumentation: Sie sollten nicht nur kryptisch abbrechen, sondern dem/der Anwender*in idealerweise mitteilen, was schiefging und wie es behoben werden kann (z. B. “Fehler: Ungültiges Datum im Feld XY – bitte Format TT/MM/JJJJ verwenden.”). Solche in den Code integrierten Hinweise ergänzen die schriftliche Dokumentation und tragen zur besseren Nutzbarkeit bei. Obwohl viele Digital-Humanities-Tools primär von Einzelpersonen genutzt werden, sollte dennoch angegeben werden, wie andere ggf. zur Software beitragen oder Support erhalten können. Ein kurzer Hinweis auf den Issue-Tracker (z. B. “Fehler bitte über GitHub-Issues melden”) oder auf die Kontaktmöglichkeit zum Autor (E-Mail) gehört dazu. Ebenso können Community Guidelines skizziert werden: etwa Codierstandards oder ein Verhaltenskodex, falls Beiträge erwartet werden. Für kleinere Projekte reicht oft ein Satz wie “Beiträge durch Pull Requests sind willkommen; bei Fragen wenden Sie sich an…”. Teil der Dokumentation sind auch formale Informationen, die im Repository leicht zugänglich sein sollten. Lizenzinformationen klären die rechtlichen Bedingungen der Nutzung und Weiterverbreitung. Es ist Best Practice, eine LICENSE-Datei beizulegen, aber auch in der README kurz zu erwähnen, unter welcher Lizenz die Software steht. Für Forschungssoftware empfiehlt sich eine offene Lizenz (z. B. MIT, BSD oder Apache 2.0 für Code, CC-BY für Daten), um Nachnutzung nicht zu behindern. Zudem sollte angegeben werden, wie die Software zitiert werden kann (z. B. DOI, Paper-Referenz). Ein eigener Abschnitt “Zitation” oder eine CITATION-Datei beschreibt, welche Publikation oder welcher DOI bei Verwendung der Software in wissenschaftlichen Arbeiten anzugeben ist. Dies erhöht die akademische Sichtbarkeit und stellt sicher, dass Autor*innen Credits für ihre Software bekommen(Smith u. a. 2016). Schließlich ist es sinnvoll, eine Versionsnummer der Software zu nennen (idealerweise in README und im Tool selbst), damit Nutzer wissen, auf welche Ausgabe sich die Dokumentation bezieht – insbesondere, wenn es im Laufe der Zeit Aktualisierungen gibt. Diese Praxis entspricht auch den ENDINGS-Prinzipien, die verlangen, dass jede veröffentlichte Version eindeutig erkennbar ist und zitiert werden kann. Zusammengefasst sollte die Dokumentation alle W-Fragen beantworten: Was tut die Software, warum wurde sie geschrieben (wissenschaftlicher Zweck), wer soll sie nutzen, wie wird sie benutzt (Inputs, Outputs, Abläufe), womit läuft sie (Umgebung/Abhängigkeiten), unter welchen Bedingungen (Annahmen/Limitationen) und wohin können sich Nutzer wenden (Support/Zitation). All diese Punkte sorgen für Nachvollziehbarkeit (im Sinne von Reproduzierbarkeit der Ergebnisse) und Weiterverwendbarkeit (im Sinne von Adaptierbarkeit der Software für neue Kontexte). Für Forschende ohne viel Ressourcen muss die Dokumentation einfach zugänglich, leicht pflegbar und ohne Spezialsoftware erstellbar sein. Daher empfiehlt es sich, auf leichte Formate und eine klare Struktur zu setzen: Die Hauptdokumentation sollte als README in Markdown-Format im Hauptverzeichnis des Code-Repositoriums liegen. Dieses README fungiert als “Startseite” des Projekts und enthält idealerweise eine komprimierte Übersicht aller wichtigen Punkte: Zweck der Software, Kurzbeschreibung, Installation, kurzer Nutzungsbeispiel, Kontakt/Lizenz. Auf Plattformen wie GitHub, GitLab etc. wird die README automatisch angezeigt, was die Sichtbarkeit erhöht. Die Vorteile von Markdown sind die einfache Lesbarkeit in Rohform, die breite Unterstützung (auch in Renderern wie GitHub-Webansicht) und die Eignung für Versionierung (Textdatei im Git). So bleibt die Dokumentation eng mit dem Code verzahnt und unter Versionskontrolle – ein Prinzip, das auch von ENDINGS propagiert wird (Dokumentation soll statisch und zusammen mit den Daten/Code abgelegt werden). Beispielhafter Struktur eines Code-Repositories Sollte die Dokumentation umfangreicher sein, ist es sinnvoll, sie in logisch getrennte Abschnitte aufzuteilen. Dies kann innerhalb der README durch Überschriften geschehen oder durch zusätzliche Markdown-Dateien im Repository (z. B. eine Diese Dateien sollten konsistent formatiert und benannt sein, damit sie leicht auffindbar sind. Sie kommen ohne spezielle Tools aus – ein einfacher Texteditor genügt zum Bearbeiten. Auch Wiki-Seiten (etwa in GitHub) können genutzt werden, sind aber weniger dauerhaft versioniert im Vergleich zu Dateien im Code-Repository selbst. Die Dokumentation sollte möglichst im Repository selbst liegen, um sicherzustellen, dass sie gemeinsam mit dem Code versioniert, verteilt und archiviert wird. Externe Dokumentationswebsites sind für kleine Projekte oft Overkill und können im schlimmsten Fall verwaisen. Um Hürden für die Erstellung und Nutzung der Dokumentation gering zu halten, sollte auf gängige, offene Formate gesetzt werden (Plaintext, Markdown, reStructuredText). Vermeiden Sie nach Möglichkeit Formate wie Word-Dokumente oder PDF als primäre Dokumentationsquelle – solche Formate sind nicht diff-freundlich, erschweren Zusammenarbeits-Workflows und sind meist nicht Teil des Versionskontrollsystems. Ein Markdown-Dokument hingegen kann gemeinsam mit dem Code gepflegt werden, und Änderungen sind transparent nachvollziehbar. Zudem erlauben offene Formate eine leichtere Langzeitarchivierung: Gemäß Endings-Prinzip sollten Informationsressourcen in langfristig lesbaren Formaten vorliegen. Markdown/Plaintext erfüllt diese Bedingung (im Gegensatz etwa zu einer Datenbank-gestützten Wissensbasis oder einem proprietären Wiki, das in 10 Jahren evtl. nicht mehr läuft). Im Sinne der Digital Longevity ist eine statische HTML- oder PDF-Version der Dokumentation (automatisch generiert aus Markdown) als Teil der Release-Artefakte sinnvoll – so kann z. B. in jeder veröffentlichten Version ein PDF-Handbuch beigelegt werden, das später zitiert oder referenziert werden kann. Wichtig ist aber, dass die Quelle der Wahrheit immer die im Repository gepflegte Doku bleibt. Nutzen Sie die Möglichkeiten von Markdown, um die Dokumentation lebendig zu gestalten. Zeigen Sie Code-Beispiele als formatierte Codeblöcke, fügen Sie Links zu weiterführenden Ressourcen ein, oder binden Sie bei Bedarf Abbildungen ein (etwa ein Diagramm der Datenpipeline, ein Screenshot der Benutzeroberfläche, etc.). Achten Sie dabei auf Dateigrößen und Formate (Bilder als PNG/JPG, Diagramme wenn möglich als SVG für Langlebigkeit). Falls Diagramme der Architektur oder Workflow-Abbildungen hilfreich sind, können diese mit simplen Mitteln erstellt werden (zur Not handgezeichnet und abfotografiert, besser jedoch mit Tools wie mermaid.js Diagrammen in Markdown oder Graphviz). Diese Visualisierungen sind jedoch nur dann einzusetzen, wenn sie echten Mehrwert bieten und ohne komplexe Build-Prozesse eingebunden werden können. Im Zweifel hat textuelle Beschreibung Vorrang, um nicht vom prinzip “keep it simple” abzuweichen. Insgesamt gilt: Die Dokumentation sollte im gleichen Repository leben wie der Code, klar strukturiert und in einem einfach handhabbaren Format vorliegen. Sie soll ohne spezielle Umgebung lesbar sein – ein Nutzer, der das Repository klont oder herunterlädt, muss sofort Zugang zur Dokumentation haben. Dieses Prinzip entspricht auch den FAIR- und RSE-Richtlinien, die fordern, Software (und deren Doku) auffindbar und zugänglich zu machen, ohne Hürden. Eine gut gepflegte README in Markdown erfüllt diese Anforderungen in den meisten Fällen optimal. Gerade weil Forschende wenig Zeit haben, muss die Dokumentation effizient gestaltet sein – sie soll alle wichtigen Informationen enthalten, aber auch nicht unnötig ausschweifen. Für typische Forschungssoftware-Projekte in den Geisteswissenschaften wird ein Umfang von maximal ca. 10 Seiten (bei Bedarf verteilt auf mehrere Dateien) als ausreichend erachtet. Dieser Richtwert verhindert, dass die Doku zu einer unüberschaubaren Abhandlung wird, und zwingt zur Fokussierung auf das Wesentliche. Wichtig ist der Inhalt, nicht die Länge: eine kürzere, aber inhaltsreiche Dokumentation ist besser als eine lange, die nichts aussagt. Ein effizienter Umfang lässt sich erreichen, indem man sich auf die oben genannten Kernpunkte konzentriert und Ablenkendes weglässt. Dokumentieren Sie alles, was für Nachvollziehbarkeit und Wiederverwendung nötig ist, und skippen Sie alles andere. Zum Beispiel muss nicht jeder interne Programmiertrick erläutert werden – Quellcode-Kommentare richten sich an Entwickler, während die Nutzerdokumentation sich auf Nutzung und Kontext beschränkt. Verzichten Sie auf seitenlange Theorieableitungen (verweisen Sie stattdessen auf Papers) und auf generische Erklärungen bekannter Technologien (man muss Git oder Python nicht in der Doku erklären, sondern kann referenzieren). Halten Sie auch die Sprache prägnant: kurze Absätze, Listen und einfache Sätze erhöhen die Lesbarkeit. Fachtermini aus dem jeweiligen wissenschaftlichen Bereich dürfen verwendet werden, aber erklären Sie sie, falls die Zielnutzer sie evtl. nicht kennen. Priorisierung: Beginnen Sie mit einer Minimaldokumentation, die alle Schlüsselaspekte abdeckt (“keine Dokumentation” ist keine Option). Good Enough Practices empfehlen, als ersten Schritt zumindest einen kurzen erklärenden Kommentar am Anfang jedes Scripts oder eine README mit ein paar Sätzen zu erstellen. Diese Hürde ist niedrig und bringt bereits Nutzen – selbst wenn (noch) keine ausführliche Handbuch-Doku existiert. Später kann die Dokumentation erweitert werden, insbesondere wenn die Software in Kooperation entsteht oder mehr Nutzer gewinnt. Es hat sich gezeigt, dass ausführliche Dokumentation oft erst entsteht, wenn ein echter Bedarf (z. B. durch externe Nutzer) vorhanden ist. Daher: zögern Sie nicht, zunächst klein anzufangen, aber stellen Sie sicher, dass zumindest die kritischen Informationen sofort verfügbar sind (lieber ein 2-seitiges README heute, als das perfekte 30-seitige Handbuch in zwei Jahren, das evtl. nie geschrieben wird). Die Obergrenze von ~10 Seiten ist ein Richtwert. Umfangreiche Projekte könnten etwas mehr benötigen, sehr kleine Tools kommen mit einer Seite aus. Das Ziel ist, dass ein interessierter Nutzer die Dokumentation in überschaubarer Zeit durchsehen kann. Ein guter Test ist: Kann eine neue Person in < 1 Stunde mit Hilfe der Doku das Tool zum Laufen bringen und ein einfaches Beispiel ausführen? Wenn ja, ist der Detailgrad angemessen. Wenn die Person hingegen nach 10 Seiten immer noch nicht weiß, wie sie loslegen soll, muss die Doku fokussierter werden. Fügen Sie zur Not eine kurze Übersicht/Zusammenfassung am Anfang ein, die das Wichtigste in Kürze nennt – viele Leser entscheiden in wenigen Minuten, ob sie eine Software weiter betrachten oder nicht, und hier zählt der erste Eindruck. Ein weiterer Tipp zur Effizienz: Nutzen Sie Verweise und vorhandene Ressourcen. Wenn z. B. Ihr Tool auf einem komplizierten Setup (Datenbank, Webserver) aufbaut, brauchen Sie nicht jede Installationsoption im Detail in Ihrer Doku zu reproduzieren – verlinken Sie auf offizielle Installationsanleitungen dieser Abhängigkeiten, und nennen Sie nur Ihre spezifischen Konfigurationen. Ebenso können Tutorials oder Papers, die schon existieren, als weiterführende Links angegeben werden, anstatt Inhalte redundant zu erklären. Das entlastet Ihre Dokumentation und hält sie schlank. Zum Fokus gehört auch, zwischen Nutzerdokumentation und Entwicklerdokumentation zu unterscheiden. Dieser Katalog adressiert primär die Nutzerdokumentation (für Endnutzer und für die Autoren selbst, wenn sie das Tool später wieder anfassen). Entwicklerdokumentation (z. B. detaillierte API-Dokumente, Code-Kommentare, technische Architektur) kann separat gehalten werden, sofern nötig, um den Hauptnutzerfluss nicht zu überfrachten. Für viele kleine Forschungssoftware-Projekte sind ausführliche Entwicklerdokus nicht nötig – hier reicht es, den Code gut zu kommentieren und eventuell eine grobe Architekturübersicht bereitzustellen. Konzentrieren Sie die Hauptdokumentation darauf, das Nutzen und Verstehen der Software von außen zu ermöglichen. Abschließend sei betont: Ein kompakter, zielgerichteter Dokumentsatz, der genau die relevanten Infos liefert, erhöht die Wahrscheinlichkeit, dass er aktualisiert und genutzt wird. Umfangmonster schrecken ab und veralten schneller. Halten Sie die Dokumentation deshalb so knapp wie möglich, aber so ausführlich wie nötig – ganz im Sinne von Einsteins Prinzip, Dinge so einfach wie möglich zu machen, aber nicht einfacher. Die Dokumentationslast lässt sich durch den Einsatz geeigneter Werkzeuge erheblich senken. Gerade Forschende, die alleine programmieren, können von (teil-)automatisierter Dokumentation profitieren, um konsistente und aktuelle Unterlagen zu erhalten, ohne alles von Hand schreiben zu müssen. Im Folgenden werden einige Tools und Möglichkeiten vorgestellt – samt Empfehlungen, wann ihr Einsatz sinnvoll oder notwendig ist: Nutzen Sie die Möglichkeit, Dokumentation direkt im Quellcode unterzubringen, z. B. in Form von Docstrings (mehrzeilige Strings in Funktionen/Klassen bei Python, Roxygen-Kommentare in R, Javadoc-Kommentare in Java, etc.). Diese dienen doppelt: Zum einen erleichtern sie es Ihnen und Kollegen, den Code beim Lesen zu verstehen, zum anderen können sie von Tools ausgelesen und zu hübschen API-Dokumentationen verarbeitet werden. Idealerweise dokumentieren Sie jede wichtige Funktion, Klasse oder Modul mit einem kurzen Docstring, der Zweck, Parameter, Rückgaben und ggf. Beispiele enthält. Für kleine Scripte genügen ggf. Modul- oder Abschnittskommentare. Wichtig ist Konsistenz im Stil – halten Sie sich an Konventionen Ihres Ökosystems (z. B. Google Style Guide für Python Docstrings oder entsprechende Formatvorgaben für andere Sprachen). Mit Tools wie Sphinx (für Python, aber grundsätzlich sprachunabhängig) können aus Docstrings automatisiert Webseiten oder PDF-Handbücher generiert werden. Sphinx liest z. B. die Python-Docstrings und erzeugt daraus strukturiert eine Dokumentation; Erweiterungen wie napoleon erlauben es, Google- oder Numpy-Style-Dokumentation direkt zu verarbeiten. Ähnliche Generatoren gibt es für nahezu alle Sprachen: Javadoc für Java, Doxygen für C/C++ (und viele andere Sprachen), MkDocs oder pdoc für Python, etc. Der Einsatz solcher Tools ist besonders dann sinnvoll, wenn Ihre Forschungssoftware über eine Programmierschnittstelle (API) verfügt, die von anderen genutzt werden soll, oder wenn das Projekt größer wird und die interne Struktur komplexer ist. In solchen Fällen kann eine API-Referenz (automatisch aus dem Code erzeugt) eine erhebliche Hilfe sein. Verpflichtend wird dieser Ansatz etwa, wenn Sie ein Bibliothekspaket veröffentlichen (z. B. ein R-Package in CRAN oder Python-Package auf PyPI) – dort sind Docstrings und generierte Dokumentation quasi Standard. Für ein einmaliges Analyse-Skript in den Digital Humanities ist eine voll ausgebaute API-Doku vielleicht nicht nötig; hier reicht möglicherweise ein inline kommentierter Code. Doch sobald Funktionen von anderen aufgerufen oder das Projekt von mehreren entwickelt wird, sollte ein Dokumentationstool in Betracht gezogen werden, um den Aufwand gering zu halten und Einheitlichkeit zu gewährleisten. Ein mächtiges Werkzeug – gerade in datengetriebenen Geisteswissenschaften – sind Jupyter Notebooks bzw. R Markdown Notebooks (Kluyver u. a. 2016). Diese erlauben es, ausführbaren Code mit erklärendem Text und Visualisierungen in einem Dokument zu vereinen. Für Dokumentationszwecke können Notebooks zweierlei leisten: (1) als Tutorials/Beispiel-Workflows, die Nutzer interaktiv nachvollziehen können, und (2) als Reproduzierbarkeits-Dokumentation für analytische Prozesse. Wenn Ihre Forschungssoftware z. B. eine Bibliothek ist, könnten Sie ein Notebook bereitstellen, das einen typischen Anwendungsfall durchspielt (inklusive Daten-Loading, Aufruf der Funktionen, Darstellung der Ergebnisse). Notebooks senken die Hürde, weil Nutzer direkt experimentieren können, und fördern transparente Forschung, da Code, Ergebnisse und Beschreibung zusammenfließen. Sie sind daher sinnvoll, wenn der Hauptanwendungsfall die Durchführung von Analysen oder Datenverarbeitungen ist, die man Schritt für Schritt demonstrieren kann. Notebooks erfordern allerdings eine lauffähige Umgebung – das heißt, Sie müssen darauf achten, dass alle Abhängigkeiten im Notebook deklariert sind und die Daten zugänglich sind. Es hat sich gezeigt, dass Notebooks aus Publikationen oft nicht ohne Weiteres laufen, weil Pfade, Datenquellen oder spezielle Umgebungen fehlen. Deshalb: Wenn Sie Notebooks als Doku nutzen, stellen Sie sicher, dass sie leicht ausführbar sind (z. B. durch Bereitstellen von Umgebungsdateien wie Wann sind Notebooks verpflichtend? – Nie im strengen Sinne, aber sie sind quasi Goldstandard, um wissenschaftliche Analysen nachvollziehbar zu machen. In Projekten, wo es um Data Science Workflows oder interaktive Exploration geht, sollten Notebooks stark erwogen werden, während für ein reines Tool/Script eine gut geschriebene README mit Beispielausgabe ausreichend sein kann. Für umfangreichere Projekte oder solche mit eigener Website kann es sinnvoll sein, eine Dokumentationswebsite zu generieren. Tools wie Sphinx (zusammen mit ReadTheDocs für Hosting) oder MkDocs erlauben es, aus Markdown/reStructuredText-Dateien einen ansprechend formatierten HTML-Dokumentationssatz zu bauen. Der Vorteil ist, dass man eine durchsuchbare, verlinkte Doku bekommt, oft mit schönem Layout und zusätzlicher Navigation. Mit Continuous Integration lassen sich diese Seiten bei jedem Git-Push automatisch aktualisieren. Für die Nachhaltigkeit (ENDINGS-Prinzip) ist wichtig, dass diese Webseiten statisch sind – d.h. sie funktionieren ohne Server-Backends und bleiben auch offline nutzbar. Sphinx erfüllt dies, indem es reine HTML-Seiten erzeugt. Solche Tools sind sinnvoll, wenn die Dokumentation sehr groß oder öffentlich weit verbreitet ist – z. B. wenn Ihre Software von vielen genutzt wird und Sie ein professionelles Auftreten wünschen, oder wenn Sie die Doku als PDF veröffentlichen möchten. Verpflichtend ist so ein Tool selten, höchstens wenn Förderprogramme oder Journals ein dokumentationsseitiges HTML-Manual verlangen. Wenn Sie jedoch planen, Ihre Software z. B. über Jahre zu pflegen und ggf. einem Journal wie JOSS vorzustellen, dann erwartet die Community meist, dass zumindest eine Sphinx/Doxygen-Doku für die API existiert. Als Daumenregel: ab einer Codebasis > einige tausend Zeilen oder > 5 Module lohnt es sich, eine generierte Dokumentation bereitzustellen, um den Überblick zu behalten. Falls Ihre Software ein Command-Line Interface (CLI) hat, stellen Sie sicher, dass eine eingebaute Hilfe vorhanden ist (z. B. Ausgabe bei Für GUI-Anwendungen sollten Tooltips, Hilfetexte in der Oberfläche oder zumindest ein kleiner Help-Abschnitt im Handbuch vorhanden sein. Diese eingebetteten Hilfen ersetzen keine ausführliche Dokumentation, aber sie senken die Schwelle für alltägliche Fragen. Eine Form der Teil-Automatisierung ist es, die Dokumentation an den Entwicklungs-Workflow zu koppeln. So sollte die Dokumentation im selben Versionskontrollsystem (Git) liegen wie der Code, damit Änderungen synchron nachverfolgt werden. Es empfiehlt sich, bei jedem größeren Code-Update zu prüfen, ob die Doku noch stimmt (das kann man sich z. B. als Punkt in Pull-Request-Reviews notieren oder per Issue-Template abfragen). Für Projekte mit Continuous Integration (CI) kann man sogar automatisierte Checks einrichten, die z. B. prüfen, ob die Doku gebaut werden kann oder ob Docstrings fehlen. Einige CI-Skripte generieren bei jedem Commit eine frische Doku (z. B. mittels Sphinx) und veröffentlichen sie – so ist garantiert, dass die aktuelle Codeversion immer eine aktuelle Doku hat. In bestimmten Fällen gibt es weitere Werkzeuge: z. B. Doxygen für automatisierte Code-Diagramme und Querverweise (gerne in C++-Projekten genutzt), oder Swagger/OpenAPI für automatische Dokumentation von Web-APIs. Wenn Ihre Forschungssoftware z. B. einen Webservice anbietet, kann Swagger eine interaktive API-Doku erzeugen. Ebenso können Literatur-Manager wie Manubot oder RMarkdown Bücher helfen, Code und Text zu integrieren (aber das geht über das hinaus, was die meisten DH-Projekte benötigen). Erwähnenswert ist noch Jupyter Book oder R Bookdown, womit man umfangreiche narrative Dokumentationen (inkl. Code) als Website/Book erstellen kann – nützlich, falls Ihre Dokumentation eher ein ausführlicher Lehrtext werden soll (z. B. wenn die Software einen ganzen methodischen Ansatz dokumentiert). Für den hier anvisierten Zweck (knackiger Doku-Katalog) sind solche Tools meist zu schwergewichtig. Es gibt kein universelles Muss, außer: Irgendeine Form der Doku ist Pflicht. Ob Sie nun per Hand Markdown schreiben oder Sphinx einsetzen, hängt von Kontext und Projektgröße ab. Allgemein gilt: Verwenden Sie Automatisierung wo immer möglich, um sich zu entlasten, aber vermeiden Sie Overhead durch Tools, die Sie nicht brauchen. Ein einzelnes historisches Analyse-Skript braucht kein Doxygen; ein komplexes DH-Toolkit mit API sollte hingegen Doxygen oder Sphinx nutzen, damit die Nutzer nicht den Code lesen müssen, um Funktionen zu verstehen. Denken Sie daran: “Die beste Dokumentation ist die, die sich selbst schreibt.” – dieses Motto aus der Literatur spielt darauf an, dass wir Tools nutzen sollen, die uns Schreibarbeit abnehmen. Perfekt autonom schreibt sich die Dokumentation zwar nie, aber moderne Werkzeuge können Routineaufgaben (z. B. Inhaltsverzeichnisse, Funktionsreferenzen, Formatierung) automatisieren. Dadurch bleibt Ihnen mehr Zeit für das inhaltliche Fine-Tuning der Texte. Um zu entscheiden, was dokumentiert wird (und was nicht), helfen etablierte Best Practices sowie Vorlagen aus der Community. Im Folgenden sind einige bewährte Richtlinien zusammengefasst, untermauert von Quellen, die bei der Priorisierung der Dokumentationsinhalte helfen: Stellen Sie sich beim Schreiben der Doku die verschiedenen Nutzerrollen vor: “Zukünftiges Ich”, Kolleg*innen, Fachforscher anderer Disziplin und ggf. Software-Entwickler, die den Code erweitern. Jede dieser Gruppen möchte bestimmte Dinge wissen. Forscher*innen fragen: Was kann das Tool? Wie benutze ich es? In welchem Kontext steht es?. Entwickler*innen fragen: Wie kann ich beitragen? Wie funktioniert es unter der Haube?. Priorisieren Sie zunächst die erstgenannten (Anwender) – deshalb Fokus auf Zweck, Nutzung und Ergebnisse in der Hauptdoku. Detailinfos für Entwickler (z. B. Code-Struktur, To-do-Liste) können separat oder später ergänzt werden. Halten Sie sich stets vor Augen: Dokumentation ist primär für Menschen (nicht für Maschinen), daher schreiben Sie klar und vermeiden Sie unnötigen Jargon. Selbst wenn der Code “für sich spricht”, denken Sie daran, dass klare Erläuterungen später viel Zeit sparen. Die folgenden Punkte fassen zusammen, was eine gute Dokumentation mindestens enthalten sollte. Sie können auch als Qualitäts-Checkliste dienen, um Ihre Dokumentation zu überprüfen: Diese Checkliste kann vor einem “Release” der Software durchgegangen werden, ähnlich einem Review-Prozess (vgl. JOSS Review-Kriterien, die viele dieser Punkte abdecken). Sie hilft zu entscheiden, was noch dokumentiert werden muss und was eventuell weggelassen werden kann. Alles, was für die obigen Punkte nicht relevant ist, kann man tendenziell aus der Hauptdokumentation herauslassen. Beispielsweise interne Code-Refaktorierungsdetails oder historische Anekdoten zur Entwicklung gehören eher ins interne Changelog oder in Blog-Posts, nicht in die Nutzerdokumentation. Ein guter Weg, die eigene Dokumentation zu verbessern, ist ein Blick auf Projekte mit exzellenter Doku. In der Journal of Open Source Software (JOSS) oder Journal of Open Research Software (JORS) werden oft Softwareartikel veröffentlicht, bei denen die zugehörigen Repositorien vorbildliche READMEs und Wikis haben. Diese können als Vorlage dienen. Achten Sie darauf, wie diese Projekte ihre README strukturieren, welche Abschnitte vorhanden sind und welche nicht. Viele erfolgreiche Projekte haben z. B. eine ähnliche Reihenfolge: Introduction, Installation, Usage, Contributing, License, Citation – ein Muster, das sich bewährt hat. Ebenso gibt es von Initiativen wie der Software Sustainability Institute Blogposts mit Best Practices und sogar Vorlagen (Templates) für Dokumentation. Nutzen Sie solche Ressourcen; sie ersparen einem das Rad neu zu erfinden. Allerdings: Adaptieren Sie sie auf Ihre Bedürfnisse – nicht jede Vorlage passt 1:1. Beachten Sie, dass dieser Anforderungskatalog in Einklang mit den Prinzipien des Research Software Engineering und den ENDINGS-Prinzipien steht. Gutes Research Software Engineering fördert u.a. Nachhaltigkeit, Offenheit und Reproduzierbarkeit in der Softwareentwicklung. Dementsprechend legt unsere Dokumentations-Checkliste Wert auf Reproduzierbarkeit (Installation, Daten, Beispiele), Offenheit (Lizenz, offene Formate) und Nachhaltigkeit (Versionierung, Langlebigkeit der Doku). Die ENDINGS-Prinzipien für digitale Projekte betonen insbesondere die Bedeutung von Dokumentation für Datenstrukturen, offenen Lizenzen, statischen Outputs und Zitierbarkeit. Unsere Empfehlungen, etwa ein statisches Markdown-README beizulegen, die Datenmodell-Doku nicht auszulagern oder Zitationsangaben zu machen, setzen genau diese Vorgaben um. Indem Sie also diesem Anforderungskatalog folgen, berücksichtigen Sie automatisch wichtige anerkannte Prinzipien für gute wissenschaftliche Softwarepraxis. Dokumentation ist kein einmaliges Ereignis, sondern ein fortlaufender Prozess. Best Practice ist, früh Feedback von Testnutzer*innen oder Kolleg*innen einzuholen: Lassen Sie jemanden die Anleitung befolgen und hören Sie auf Stolpersteine. Oft zeigen sich Lücken erst im Praxistest (“Ich wusste nicht, was ich nach Schritt X tun soll” etc.). Planen Sie Zeiten ein, die Dokumentation nachzuführen, insbesondere wenn sich die Software ändert. Ein lebendiges Projekt wird vielleicht Release für Release die Dokumentation erweitern (evtl. neue Tutorials, neue Module dokumentieren). Nutzen Sie auch Issues für Dokumentation: Wenn Nutzer Fragen stellen, überlegen Sie, ob die Antwort in die offizielle Doku übernommen werden sollte. So wächst die Dokumentation organisch entlang der tatsächlichen Bedürfnisse. Zusammenfassend helfen die genannten Best Practices dabei, die Dokumentation zielgerichtet zu gestalten: Dokumentiert wird, was dem Verständnis und der Nutzung dient; weggelassen wird, was überflüssig oder selbstverständlich ist. Eine gute Dokumentation erzählt eine klare Geschichte über die Software, anstatt den Leser mit irrelevanten Details zu verlieren. Mit den richtigen Werkzeugen und Prinzipien an der Hand kann selbst unter Zeitdruck eine qualitativ hochwertige Dokumentation entstehen – zur Freude aller, die mit der Forschungssoftware arbeiten möchten. Die hier präsentierten Anforderungen und Empfehlungen bieten einen Leitfaden für die Dokumentation von Forschungssoftware in den Digital Humanities. Sie sind darauf ausgerichtet, mit überschaubarem Aufwand maximale Nachvollziehbarkeit, Langlebigkeit und Wiederverwendbarkeit zu erreichen. Indem zentrale Inhalte (Ziele, Inputs/Outputs, Hintergrund, etc.) klar dokumentiert, ein nutzerfreundliches Format (README im Repo) gewählt, der Umfang fokussiert gehalten und hilfreiche Tools eingesetzt werden, kann die Dokumentation zur Stärke eines Projekts werden statt einem lästigen Anhängsel. Wissenschaftlich fundierte Best Practices – von Ten Simple Rules for Documenting Scientific Software bis zu den ENDINGS-Principles – untermauern diesen Katalog. Die Umsetzung dieser Richtlinien wird dazu beitragen, dass Forschungssoftware aus den Geisteswissenschaften nicht nur kurzfristig von ihren Autor*innen genutzt wird, sondern langfristig von Dritten verstanden, validiert und weiterentwickelt werden kann. So schließt sich der Kreis zwischen guter Softwareentwicklung und guter Wissenschaft: Dokumentation ist das Bindeglied, das Code und Erkenntnis transparent verbindet. In der Praxis bedeutet dies zwar zusätzliche Arbeitsschritte, doch wie die Erfahrung zeigt, zahlen sich diese in Form von Zeiteinsparung bei Nutzern, höherer Zitierbarkeit und größerer Wirkung der Software aus. Mit diesem Anforderungskatalog sind Forschende gut gerüstet, um ihre Softwareprojekte dokumentationstechnisch auf ein solides Fundament zu stellen – trotz knapper Zeit und ohne Informatikabschluss. Denn am Ende gilt: Gut dokumentierte Forschungscode ist nachhaltige Forschung. Mit einer solchen Struktur und Herangehensweise lässt sich auch in einem kleinen Forschungsteam eine professionelle Dokumentation erzielen, die den Prinzipien von Open Science und nachhaltiger Softwareentwicklung gerecht wird. Die investierte Mühe wird durch Zeitgewinn bei Wiederverwendung und Erweiterung der Software mehr als aufgewogen. So wird die Forschungssoftware nicht zum einmaligen “Nebenprodukt”, sondern zu einem robusten, teilbaren Ergebnis wissenschaftlicher Arbeit. Erstellt wurde der initial draft mittels Websuche und “Deep-Research” von The referenced study introduced a prompt-driven approach to NER, reframing it “from a purely linguistic task into a humanities-focused task”. Instead of training a specialized NER model for each corpus, the method leverages the fact that large pretrained LLMs already contain vast world knowledge and language understanding. The key idea is to provide the model with contextual definitions and instructions so it can recognize entities in context. Notably, the authors found that with proper prompts, a commercial LLM (ChatGPT-4) could achieve precision and recall on par with or better than state-of-the-art NER tools on a 1921 historical travel guide. This was achieved zero-shot, i.e. without any fine-tuning or additional training data beyond the prompt itself. Prompt Strategy: The success of this approach hinges on careful prompt engineering. The final prompt used in the paper had multiple components: Why Local LLMs? The original experiments used a proprietary API (ChatGPT-4). To make the method accessible to all (and avoid data governance issues of cloud APIs), we implement it with open-source LLMs running locally. Recent openly licensed models are rapidly improving and can handle such extraction tasks given the right prompt. Running everything locally also aligns with the paper’s goal of “democratizing access” to NER for diverse, low-resource texts – there are no API costs or internet needed, and data stays on local hardware for privacy. Our solution consists of a workflow in n8n that orchestrates the NER process, and a local Ollama server that hosts the LLM for text analysis. The high-level workflow is as follows: Workflow Structure: In n8n’s interface, the workflow might look like a sequence of connected nodes: Webhook → Function (build prompt) → AI Model (Ollama) → Webhook Response. If using n8n’s new AI Agent feature, some steps (like prompt templating) can be configured within the AI nodes themselves. The key is that the Ollama model node is configured to use the local server (usually at To reproduce this solution, you will need a machine with an NVIDIA GPU and the following software components installed: n8n (v1.x** or later)** – the workflow automation tool. You can install n8n via npm, Docker, or use the desktop app. For a server environment, Docker is convenient. For example, to run n8n with Docker: This exposes n8n on Ollama (v0.x*) – an LLM runtime that serves models via an HTTP API. Installing Ollama is straightforward: download the installer for your OS from the official site (Linux users can run the one-line script This will launch the service listening on port 11434. You can verify it’s running by opening LLM Model (14B class): Finally, download at least one large language model to use for NER. You have a few options here, and you can “pull” them via Ollama’s CLI: DeepSeek-R1 14B: A 14.8B-parameter model distilled from larger reasoning models (based on Qwen architecture). It’s optimized for reasoning tasks and compares to OpenAI’s models in quality. Pull it with: This downloads ~9 GB of data (the quantized weights). If you have a very strong GPU (e.g. A100 80GB), you could even try Cogito 14B: A 14B “hybrid reasoning” model by Deep Cogito, known for excellent instruction-following and multilingual capability. Pull it with: Cogito-14B is also ~9 GB (quantized) and supports an extended context window up to 128k tokens – which is extremely useful if you plan to analyze very long documents without chunking. It’s trained in 30+ languages and tuned to follow complex instructions, which can help in structured output tasks like ours. Others: Ollama offers many models (LLaMA 2 variants, Mistral, etc.). For instance, Hardware Requirements: Our case assumes an NVIDIA A100 GPU (40 GB), which comfortably hosts a 14B model in memory and accelerates inference. In practice, any modern GPU with ≥10 GB memory can run a 13–14B model in 4-bit quantization. For example, an RTX 3090 or 4090 (24 GB) could handle it, and even smaller GPUs (or Apple Silicon with 16+ GB RAM) can run 7B models. Ensure you have sufficient system RAM as well (at least as much as the model size, plus overhead for n8n – 16 GB RAM is a safe minimum for 14B). Disk space of ~10 GB per model is needed. If using Docker for n8n, allocate CPU and memory generously to avoid bottlenecks when the LLM node processes large text. With the environment ready, we now construct the n8n workflow that ties everything together. We outline each component with instructions: Start by creating a Webhook trigger node in n8n. This will provide a URL (endpoint) that you can send a request to. Configure it to accept a POST request containing the necessary inputs. For example, we expect the request JSON to look like: Here, Next, add a node to build the prompt that will be fed to the LLM. You can use a Function node (JavaScript code) or the “Set” node to template a prompt string. We will create two pieces of prompt content: a system instruction (the role played by the system prompt in chat models) and the user message (which will contain the text to be processed). According to the method, our system prompt should incorporate the following: Once this system prompt is assembled as a single string, it will be sent as the system role content to the LLM. Now, for the user prompt, we simply supply the text to be analyzed. In many chat-based LLMs, the user message would contain the text on which the assistant should perform the task. We might prefix it with something like “Text to analyze:” for clarity, or just include the raw text. (Including a prefix is slightly safer to distinguish it from any instructions, but since the system prompt already set the task, the user message can be just the document text.) In n8n, if using the Basic LLM Chain node, you can configure it to use a custom system prompt. For example, connect the Function/Set node output into the LLM node, and in the LLM node’s settings choose “Mode: Complete” or similar, then under System Instructions put an expression that references the constructed prompt text (e.g., Now configure the LLM node to use the Ollama backend and your downloaded model. n8n provides an “Ollama Chat Model” integration, which is a sub-node of the AI Agent system. In the n8n editor, add or open the LLM node (if using the AI Agent, this might be inside a larger agent node), and look for model selection. Select Ollama as the provider. You’ll need to set up a credential for Ollama API access – use Double-check the parameters for generation. By default, Ollama models have their own preset for max tokens and temperature. For an extraction task, we want the model to stay focused and deterministic. It’s wise to set a relatively low temperature (e.g. 0.2) to reduce randomness, and a high max tokens so it can output the entire text with tags (set max tokens to at least the length of your input in tokens plus 10-20% for tags). If using Cogito with its 128k context, you can safely feed very long text; with other models (often ~4k context), ensure your text isn’t longer than the model’s context limit or use a model variant with extended context. If the model supports “tools” or functions, you won’t need those here – this is a single-shot prompt, not a multi-step agent requiring tool usage, so just the chat completion mode is sufficient. At this point, when the workflow runs to this node, n8n will send the system and user messages to Ollama and wait for the response. The heavy lifting is done by the LLM on the GPU, which will generate the tagged text. On an A100, a 14B model can process a few thousand tokens of input and output in just a handful of seconds (exact time depends on the model and input size). After the LLM node, add a node to handle the output. If you want to present the tagged text directly, you can pass the LLM’s output to the final Webhook Response node (or if using the built-in n8n chat UI, you would see the answer in the chat). The tagged text will look something like: This format highlights each identified entity. It is immediately human-readable with the tags, and trivial to post-process if needed. For example, one could use a regex like Finally, use an HTTP Response node (if the workflow was triggered by a Webhook) to send back the results. If the workflow was triggered via n8n’s chat trigger (in the case of interactive usage), you would instead rely on the chat UI output. For a pure API workflow, the HTTP response will contain either the tagged text or a JSON of extracted entities, which the user’s script or application can then use. Note: If you plan to run multiple analyses or have an ongoing service, you might want to persist the Ollama server (don’t shut it down between runs) and perhaps keep the model loaded in VRAM for performance. Ollama will cache the model in memory after the first request, so subsequent requests are faster. On an A100, you could even load two models (if you plan to experiment with which gives better results) but be mindful of VRAM usage if doing so concurrently. We provided two example 14B models (DeepSeek-R1 and Cogito) to use with this pipeline. Both are good choices, but here are some considerations and alternatives: In our implementation, we encourage experimenting with both DeepSeek-R1 and Cogito models. Both are open-source and free for commercial use (Cogito uses an Apache 2.0 license, DeepSeek MIT). They represent some of the best 14B-class models as of early 2025. You can cite these models in any academic context if needed, or even switch to another model with minimal changes to the n8n workflow (just pull the model and change the model name in the Ollama node). Let’s run through a hypothetical example to illustrate the output. Suppose a historian supplies the following via the webhook: When the workflow executes, the LLM receives instructions to tag people (PER), organizations (ORG), and locations (LOC). With the prompt techniques described, the model’s output might look like: All person names (Baron Münchhausen, Empress Anna) are enclosed in While this pipeline makes NER easier to reproduce, it’s important to be aware of its limitations and how to mitigate them: Model Misclassifications: A local 14B model may not match GPT-4’s level of understanding. It might occasionally tag something incorrectly or miss a subtle entity. For instance, in historical texts, titles or honorifics (e.g. “Dr. John Smith”) might confuse it, or a ship name might be tagged as ORG when it’s not in our categories. Solution: Refine the prompt with additional guidance. You can add a “Note” section in the instructions to handle known ambiguities (the paper did this with notes about Greek gods being persons, etc.). Also, a quick manual review or spot-check is recommended for important outputs. Since the output format is simple, a human or a simple script can catch obvious mistakes (e.g., if “Russian” was tagged LOC, a post-process could remove it knowing it’s likely wrong). Over time, if you notice a pattern of mistakes, update the prompt instructions accordingly. Text Reproduction Issues: We instruct the model to output the original text verbatim with tags, but LLMs sometimes can’t resist minor changes. They may “correct” spelling or punctuation, or alter spacing. The paper noted this tendency and used fuzzy matching when evaluating. In our pipeline, minor format changes usually don’t harm the extraction, but if preserving text exactly is important (say for downstream alignment), this is a concern. Solution: Emphasize fidelity in the prompt (we already do). If needed, do a diff between the original text and tagged text and flag differences. Usually differences will be small (e.g., changing an old spelling to modern). You can then either accept them or attempt a more rigid approach (like asking for a JSON list of entity offsets – though that introduces other complexities and was intentionally avoided by the authors). In practice, we found the tag insertion approach with strong instructions yields nearly identical text apart from the tags. Long Inputs and Memory: Very large documents may exceed the model’s input capacity or make the process slow. The A100 GPU can handle a lot, but n8n itself might have default timeouts for a single workflow execution. Solution: For long texts, break the input into smaller chunks (maybe one chapter or section at a time). n8n can loop through chunks using the Split In Batches node or simply by splitting the text in the Function node and feeding the LLM node multiple times. You’d then concatenate the outputs. If chunking, ensure that if an entity spans a chunk boundary, it might be missed – usually rare in well-chosen chunk boundaries (paragraph or sentence). Alternatively, use Cogito for its extended context to avoid chunking. Make sure to increase n8n’s execution timeout if needed (via environment variable Concurrent Usage: If multiple users or processes hit the webhook simultaneously, they would be sharing the single LLM instance. Ollama can queue requests, but the GPU will handle them one at a time (unless running separate instances with multiple GPUs). For a research setting with one user at a time, this is fine. If offering this as a service to others, consider queuing requests or scaling out (multiple replicas of this workflow on different GPU machines). The stateless design of the prompt makes each run independent. n8n Learning Curve: For historians new to n8n, setting up the workflow might be unfamiliar. However, n8n’s no-code interface is fairly intuitive with a bit of guidance. This case study provides the logic; one can also import pre-built workflows. In fact, the n8n community has template workflows (for example, a template for chatting with local LLMs) that could be adapted. We assume the base pipeline from the paper’s authors is available on GitHub – using that as a starting point, one mostly needs to adjust nodes as described. If needed, one can refer to n8n’s official docs or community forum for help on creating a webhook or using function nodes. Once set up, running the workflow is as easy as sending an HTTP request or clicking “Execute Workflow” in n8n. Output Verification: Since we prioritize correctness, you may want to evaluate how well the model did, especially if you have ground truth annotations. While benchmarking is out of scope here, note that you can integrate evaluation into the pipeline too. For instance, if you had a small test set with known entities, you could compare the model output tags with expected tags using a Python script (n8n has an Execute Python node) or use an NER evaluation library like nervaluate for precision/recall. This is exactly what the authors did to report performance, and you could mimic that to gauge your chosen model’s accuracy. By following this guide, we implemented the NER4All paper’s methodology with a local, reproducible setup. We used n8n to handle automation and prompt assembly, and a local LLM (via Ollama) to perform the heavy-duty language understanding. The result is a flexible NER pipeline that requires no training data or API access – just a well-crafted prompt and a powerful pretrained model. We demonstrated how a user can specify custom entity types and get their text annotated in one click or API call. The approach leverages the strengths of LLMs (vast knowledge and language proficiency) to adapt to historical or niche texts, aligning with the paper’s finding that a bit of context and expert prompt design can unlock high NER performance. Importantly, this setup is easy to reproduce: all components are either open-source or freely available (n8n, Ollama, and the models). A research engineer or historian can run it on a single machine with sufficient resources, and it can be shared as a workflow file for others to import. By removing the need for extensive data preparation or model training, this lowers the barrier to extracting structured information from large text archives. Moving forward, users can extend this case study in various ways: adding more entity types (just update the definitions input), switching to other LLMs as they become available (perhaps a future 20B model with even better understanding), or integrating the output with databases or search indexes for further analysis. With the rapid advancements in local AI models, we anticipate that such pipelines will become even more accurate and faster over time, continually democratizing access to advanced NLP for all domains. Sources: This implementation draws on insights from Ahmed et al. (2025) for the prompt-based NER method, and uses tools like n8n and Ollama as documented in their official guides. The chosen models (DeepSeek-R1 and Cogito) are described in their respective releases. All software and models are utilized in accordance with their licenses for a fully local deployment. Erstellt wurde der initial draft mittels Websuche und “Deep-Research” von Der Nutzer verfügt über eine Obsidian-Wissensdatenbank, in der Markdown-Dateien mit typisierten Inhalten (FileClasses wie Ziel ist es, mithilfe eines Language Models (LLM) wiederkehrende Aufgaben zu erleichtern, zum Beispiel: automatisch YAML-Felder ausfüllen (etwa fehlende ORCID iDs bei Personen ergänzen), neue Entitätsseiten anhand von Templates befüllen oder sinnvolle Verlinkungen zwischen Notizen vorschlagen. Dabei reicht ein tägliches Neu-Einlesen der Obsidian-Daten (via Cronjob o.Ä.) aus – eine Echtzeit-Synchronisation ist optional. Die Obsidian-internen Wikilinks ( Um diese Funktionen umzusetzen, bieten sich verschiedene technische Ansätze an. Im Folgenden werden fünf Optionen untersucht: (1) Nutzung eines Vektorspeichers für semantische Suche, (2) Aufbau eines Knowledge Graph aus den Notizen, (3) eine Hybrid-Lösung aus Graph und Vektor, (4) Extraktion & Normalisierung der YAML-Metadaten und (5) existierende Tools/Workflows zur Automatisierung. Jede Option wird mit Funktionsweise, Vorund Nachteilen, Aufwand, Integrationsmöglichkeiten (insb. mit lokalen LLMs wie LLaMA, Deepseek, Cogito etc.) sowie konkreten Tool-Empfehlungen dargestellt. Alle Markdown-Notizen (bzw. deren Inhalt) werden in kleinere Chunks zerlegt und durch einen Embedding-Modell in hochdimensionale Vektoren umgewandelt. Diese Vektoren werden in einem Vektorstore (wie ChromaDB oder Weaviate) gespeichert. Bei Anfragen des LLM (z.B. “Welche Projekte hat Person X?” oder “Erstelle eine neue Organisation XYZ basierend auf ähnlichen Einträgen”), können mittels ähnlichkeitssuche semantisch passende Notiz-Abschnitte abgerufen und dem LLM als Kontext mitgegeben werden (Retrieval-Augmented Generation). In der Praxis ließe sich z.B. ein Workflow mit Ollama + Nomic Embeddings + Chroma1 aufbauen. Ollama stellt ein lokales LLM-Serving bereit und bietet auch eine API für Embeddins [1]. Man könnte ein spezialisiertes Embeddin-Modell wie 1 Alle diese Teile laufen bereits individuell in der Arbeitsgruppe bzw. werden schon genutzt. Ein Vorteil dieses Ansatzes ist, dass er schon heute mit lokalen Open-Source-LLMs funktioniert. Beispielsweise ließ sich in Smart Connections ein lokal gehostetes LLaMA-Model (3B Instruct) via text-generation-webui einbinden [2]. Alternativ kann man auch LLM-as-a-service Tools wie Ollama nutzen, um ein Modell wie Llama 2 bereitzustellen. Die Open-Source-Tools LangChain oder LlamaIndex bieten Module, um Vektorstores anzubinden und mit LLM-Abfragen zu kombinieren – dies kann man auch mit lokal eingebundenen Modellen (z.B. über LlamaCpp oder GPT4All) verwenden. Zahlreiche fertige Projekte demonstrieren dieses Vorgehen: z.B. privateGPT kombiniert LangChain, GPT4All (lokales LLM) und Chroma, um komplett offline Fragen über lokale Dateien zu beantworten [3]. Auch Khoj verfolgt einen ähnlichen Pfad: Es indexiert den Vault und erlaubt semantische Natürliche Sprache Suche über Markdown-Inhalte sowie “ähnliche Notizen finden” [4]. Dank moderner Embedding-Modelle können semantisch ähnliche Inhalte gefunden werden, selbst wenn die Schlagwörter nicht exakt übereinstimmen. Das löst das in Obsidian bekannte Problem, dass die eingebaute Suche nur exakte Worttreffer findet [5]. Der Ansatz skaliert auch auf größere Wissensbasen; Vektordatenbanken wie Weaviate oder Chroma sind für zehntausende Einträge ausgelegt. Eine tägliche Aktualisierung ist machbar, da nur neue/geänderte Notizen re-embedded werden müssen. Die Einrichtung erfordert mehrere Komponenten. Man benötigt Pipeline-Schritte für das Chunking, Embedding und das Handling des Vektorstores – dies bedeutet anfängliche Komplexität und Rechenaufwand [5]. Insbesondere das Generieren der Embeddings kann bei großen Vaults zeitund speicherintensiv sein (je nach Modell und Hardware) [5]. Laufende Kosten sind bei rein lokaler Verarbeitung allerdings kein Thema außer CPU/GPU-Last. Ein potenzieller Nachteil ist, dass rein embeddings-basierte Suche keine strukturierte Abfrage erlaubt – das Modell findet zwar thematisch passende Textpassagen, aber um z.B. eine bestimmte Eigenschaft (wie eine fehlende ORCID) gezielt abzufragen, müsste man dennoch im Text suchen oder zusätzliche Logik anwenden. Das LLM kann aus den gefundenen Texten zwar implizit Fakten entnehmen, hat aber kein explizites Wissen über die Datenstruktur. Zudem können irrelevante Kontextstücke eingebunden werden, wenn das semantische Matching fehlerhaft ist (dies erfordert ggf. Feintuning der Chunk-Größe oder Filtern per Dateityp/-klasse)2. 2 Und diese Nachteile machen dies zu einem Deal-Breaker. Gerade in Tabellen oder Auflistungen kann der Attention-Mechanismus der LLM schnell zu einem Mischen oder Verwechseln von präsentierten Informationen führen. Besonders kleine Netze (meist bis ~7b) sind hier anfällig. Statt (oder zusätzlich zu) freiem Text wird der Informationsgehalt des Vaults als Graph modelliert. Jede Notiz entspricht einem Knoten im Graphen (mit Typ-Label gemäß FileClass, z.B. Eine Möglichkeit ist die Obsidian-Daten nach RDF zu exportieren. So beschreibt Pavlyshyn (2023) ein Verfahren, einen Vault ins RDF-Format zu überführen, um “komplexe Abfragen mit klassischen Semantic-Tools” zu ermöglichen [6]. Alternativ kann man direkt in einer Graphdatenbank wie Neo4j modellieren. Ein Community-Plugin (obsidian-neo4j-stream) hat beispielsweise versucht, den Obsidian-Linkgraph in Neo4j importierbar zu machen [7]. Konkret würde man pro Markdown-Datei einen Node mit dessen YAML-Feldern als Properties anlegen. Bestehende Wiki-Links zwischen Dateien werden als ungerichtete oder gerichtete Edges abgebildet (hier kann man, sofern man mehr Semantik will, Link-Typen einführen – z.B. im Text Ein solcher Graph erlaubt strukturierte Abfragen und Schlussfolgerungen. Für wiederkehrende Aufgaben kann man den Graph gezielt auswerten. Beispielsweise ließen sich “alle Personen ohne ORCID” mittels einer einfachen Graph-Query ermitteln. Das LLM könnte diese Liste als Input erhalten und dann (ggf. mittels Tools oder Wissensbasis) die fehlenden IDs ergänzen. Auch Link-Vorschläge können aus dem Graph gezogen werden: Durch Graph-Analysen wie das Finden von gemeinsamen Nachbarn oder kürzesten Pfaden entdeckt man Verbindungen, die im Vault noch nicht als direkte Links existieren. So könnte man z.B. feststellen, dass zwei Personen an vielen gleichen Meetings teilgenommen haben und dem Nutzer vorschlagen, diese Personen direkt miteinander zu verknüpfen. Oder man erkennt durch link prediction Algorithmen neue mögliche Beziehungen. Forschung und Community sehen hier großes Potential: Eine AI-gestützte Graphanalyse kann helfen, verborgene Zusammenhänge im eigenen Zettelkasten zu finden [9]. Mit Graph-basiertem Reasoning ließe sich sogar neues Wissen entdecken oder logisch konsistente Antworten generieren [8] – etwas, das rein embeddings-basierte Ansätze so nicht leisten. Die Integration eines Graphen erfordert meist eine Zwischenschicht. Ein LLM kann nicht direkt “in” einer Neo4j-Datenbank suchen, aber man kann ihm eine Schnittstelle anbieten. Zwei Strategien sind denkbar: 3 Via ‘Tool Use’ in Modernen LLM könnte das LLM selbst eine Suche auslösen und so den Teilgraphen wählen. Aber alleine die formulierung der Suche führt dann direkt zu dem hybriden Ansatz unten. Eine andere interessante Möglichkeit ist die Nutzung graphbasierter KI-Modelle (Graph Neural Networks o.ä.), die aber in unserem Kontext (persönlicher Vault) noch experimentell sind. Erwähnenswert ist z.B. MyKin.ai, ein Projekt, das einen privaten KI-Assistenten baut, der gemeinsam mit dem Nutzer einen persönlichen Wissensgraphen aufbaut und nutzt [8]. Hier übernimmt die KI das “heavy lifting” der Graph-Pflege, während der Nutzer chattet – ein hybrider Ansatz aus Conversation und Graphaufbau. Für unseren Anwendungsfall wäre jedoch eher ein statischer Graph sinnvoll, den wir periodisch aktualisieren. Ein Wissensgraph spielt seine Stärken vor allem bei strukturbezogenen Aufgaben aus. Für das automatische Ausfüllen von YAML-Feldern oder das Prüfen von Verlinkungen ist er ideal, da solche Fragen direkte Graphabfragen ermöglichen. Auch für neuartige Verknüpfungen (Link-Vorschläge) lässt sich ein Graph analytisch nutzen (z.B. “Link Prediction” auf Basis von Graph-Nachbarschaft). Allerdings ist die Umsetzung deutlich komplexer als beim Vektorstore, und viele RAG-Anwendungsfälle (Zusammenfassungen, inhaltliche Q&A) erfordern trotzdem den Rückgriff auf die eigentlichen Texte – was wiederum den Vektoransatz benötigt. Daher bietet sich oft eine Kombination beider Methoden an. Dieser Ansatz versucht, semantische Textsuche und strukturierte Graph-Abfragen zu vereinen, um die Vorteile beider Welten auszuschöpfen. In der Praxis gibt es mehrere Möglichkeiten, wie ein hybrides System ausgestaltet sein kann: Die Hybridlösung verspricht maximale Abdeckung der Anwendungsfälle. Strukturierte Fakten und unstrukturierte Inhalte können gemeinsam dem LLM präsentiert werden, was sowohl präzise Faktenkenntnis als auch reichhaltigen Kontext ermöglicht. Gerade für komplexe Aufgaben – etwa das automatisierte Erstellen einer neuen Entitätenseite – wären wohl beide Aspekte wichtig: das LLM müsste sich an vorhandenen ähnlichen Seiten inhaltlich orientieren (Vektorsuche nach ähnlichen Organisations-Beschreibungen) und zugleich korrekte Verknüpfungen setzen (Graph checken, ob z.B. die neue Organisation bereits Personen im Vault hat, die als Mitarbeiter verknüpft werden sollten). Ein solches System könnte also dem Nutzer sehr viel Arbeit abnehmen und dabei konsistente, vernetzte Notizen erzeugen. Dem steht jedoch ein hoher Architekturund Wartungsaufwand gegenüber. Man muss im Grunde zwei Systeme aufbauen und aktuell halten. Zudem ist die Logik, wie die Ergebnisse zusammenfließen, nicht trivial. Ohne gutes Design kann es passieren, dass der Graph-Teil und der Vektor-Teil widersprüchliche oder redundante Informationen liefern. Auch muss man Performance beachten – doppelte Abfragen kosten mehr Zeit. In vielen Fällen mag auch ein einzelner Ansatz ausreichen, sodass die Zusatzkomplexität nicht immer gerechtfertigt ist. Auf technischer Seite ist so ein hybrides System durchaus machbar. Beispielsweise ließe sich LlamaIndex verwenden, um unterschiedliche Indexe (VectorIndex, KnowledgeGraphIndex) zu kombinieren – es gibt Konzepte wie “Composable Indices”, mit denen man hierarchische Abfragen bauen kann. So könnte man erst den Graph nach relevanten Knoten filtern und dann nur die zugehörigen Dokumente vektor-suchen (oder umgekehrt). Weaviate als All-in-one-Lösung wurde bereits erwähnt. In kleineren Umgebungen kann man auch pragmatisch vorgehen: Ein Python-Skript, das bei bestimmten Fragen zuerst einen Neo4j-Query absetzt und dessen Ergebnis dem LLM als Teil des Prompts voranstellt, während es parallel eine Chroma-Query macht, wäre eine einfache implementierbare Variante. Die Hybrid-Lösung ist die ambitionierteste, aber auch zukunftsträchtigste Option. Sie empfiehlt sich, wenn sowohl inhaltliche Assistenz (Texte zusammenfassen, beantworten) als auch datenbankartige Operationen (Felder validieren, Beziehungen auswerten) gefragt sind – was hier der Fall ist. Oft kann man auch schrittweise vorgehen: zunächst mit einem Vektor-RAG starten (geringerer Aufwand) und dann gezielt Graph-Features ergänzen, sobald z.B. Link-Empfehlungen oder Konsistenzprüfungen wichtiger werden. Unabhängig vom gewählten Retrieval-Ansatz ist es essenziell, die in YAML front matter steckenden strukturierten Informationen effektiv zu nutzen. Die Obsidian-Plugins Metadata Menu und Templater stellen sicher, dass viele wichtige Daten bereits sauber in den Notizen vorliegen (z.B. hat eine Personenseite Felder wie Ein möglicher Schritt im täglichen Refresh ist ein Skript, das alle Dateien durchläuft und die YAML-Blöcke parst (z.B. mit einem YAML-Parser in Python oder JavaScript). Die extrahierten Felder können dann in eine normale Datenbank (SQLite/CSV/JSON) oder direkt als Knoten/Properties in den Knowledge Graph überführt werden. Damit erhält man z.B. eine Tabelle aller Personen mit ihren ORCID-IDs, eine Liste aller Projekte mit Start-/Enddatum etc. Oft müssen die Rohwerte etwas vereinheitlicht werden. Beispielsweise sollten Datumsangaben ein konsistentes Format haben, Personennamen evtl. in Vor-/Nachname zerlegt, und fehlende Felder explizit als Der aufbereitete YAML-Datensatz kann auf zwei Weisen eingebunden werden: 4 oder Plugins wie Dataview-Publisher benutzen, die die Ergebnisse als Markdown-Tabell in ein Dokument schreiben Die Vorteile der strukturierten Extraktion liegen auf der Hand – Performance und Präzision. Man muss nicht jedes Mal den gesamten Markdown-Text durchsuchen, um z.B. den Wert eines bestimmten Feldes zu finden; man hat ihn direkt. Außerdem reduziert es die Abhängigkeit vom LLM für einfache Aufgaben (Daten finden, vergleichen). Für die meisten Menschen ist es auch leichter zu verstehen und zu prüfen, wenn man z.B. eine CSV mit allen ORCIDs hat, als wenn man dem LLM blind glauben muss. In jedem Fall sollte man eine Pipeline vorsehen, die die YAML-Metadaten extrahiert und in eine strukturierte Form bringt. Diese bildet das Rückgrat für den Knowledge-Graph-Ansatz (ohne diese wären die Knoten nackte Titel ohne Attribute) und ist auch für Vektor-RAG nützlich (z.B. als Filter oder zur post-processing der LLM-Antworten). Insbesondere dank der FileClass-Typisierung im Vault kann man hier sehr zielgerichtet vorgehen – etwa nur definierte Entitätstypen verarbeiten. In Community-Diskussionen wurde vorgeschlagen, YAML-Metadaten zu nutzen, um AI-Aufgaben einzuschränken: z.B. NER-Modelle nur auf bestimmten Notizen laufen zu lassen, die laut YAML einen bestimmten Typ haben [9]. Solche Optimierungen werden durch saubere strukturelle Aufbereitung erst möglich. Für die Umsetzung der oben beschriebenen Ansätze gibt es bereits einige Tools, Projekte und Best Practices, die man nutzen oder von denen man lernen kann. Hier eine strukturierte Übersicht samt Empfehlungen: Für einen ersten Prototypen empfiehlt es sich, mit dem Vektorstore-Ansatz (1) zu beginnen, da dieser am schnellsten sichtbare Erfolge bringt. Man kann z.B. mit ChromaDB + einem lokalen LLM experimentieren, oder direkt das Smart-Connections-Plugin ausprobieren, um ein Gefühl für semantische Suche im Vault zu bekommen. Die YAML-Daten sollte man von Anfang an mit-extrahieren (4), da sie die Grundlage für weitere Strukturierungsmaßnahmen bilden. Anschließend kann man gezielt Graph-Features (2) ergänzen: etwa den exportierten Vault in Neo4j laden und ein paar Abfragen formulieren, um Missing Links oder fehlende Felder aufzuspüren. Mittelfristig dürfte eine Kombination (3) notwendig sein, um sowohl Inhalt als auch Struktur abzudecken – dies kann man Schritt für Schritt angehen (z.B. zunächst Vector-RAG für inhaltliche Fragen, und separate Tools/Reports für strukturierte Checks; später dann Integration zu einem einheitlichen KI-Assistenten). Unterstützend sollte man vorhandene Tools (5) nutzen, wo möglich – z.B. Khoj für ad-hoc Fragen, oder LlamaIndex für schnelle Implementierung von Prototypen. Generell gilt: lokale LLMs sind inzwischen leistungsfähig genug für solche Aufgaben, wie die genannten Beispiele zeigen (Chat mit Vault über LLaMA etc.). Wichtig ist es, die Vault-Organisation konsequent weiterzuführen (FileClasses, Templates), da ein sauber strukturiertes Wissen die Grundlage für jede erfolgreiche RAG-Lösung ist – egal ob Vektor, Graph oder hybrid. Die Analyse basiert auf aktuellen Erkenntnissen aus der Obsidian-Community und KI-Fachwelt, u.a. Erfahrungen mit semantischer Suche [2], Diskussionen zu Knowledge Graphs in PKM [9] und Berichten über lokale RAG-Implementierungen [2]. Erstellt wurde der initial draft mittels Websuche und “Deep-Research” von ' .. summary .. '
',
+ '' .. text .. '
',
+ '' .. summary .. '
',
+ ''
+ }
+ return table.concat(details, "")
+ end
+ local summary = (#kwargs["summary"] > 0) and kwargs["summary"] or "Details"
+ local open = ""
+ if table.concat(args, " "):find("open") then
+ open = " open"
+ end
+ local output = buildDetails(summary, open)
+ if quarto.doc.isFormat("html:js") then
+ return pandoc.RawInline('html', output)
+ else
+ return pandoc.Null()
+ end
+end
+
+-- dstop shortcode
+function dstop(args, kwargs, meta)
+ local output = table.concat({'
', 'Einleitung
+Inhaltliche Anforderungen an die Dokumentation
+Ziel und Zweck der Software (Statement of Need)
+Input-/Output-Spezifikation und Datenbeschreibung
+Autor
, Empfänger
, …; Ausgabe: JSON-Datei mit Netzwerk-Metriken pro Briefwechsel.”Code-Abhängigkeiten und technische Voraussetzungen
+requirements.txt
für Python oder ein Paketmanager-Befehl). In jedem Fall sollte die Dokumentation mindestens Schritt-für-Schritt-Installationsanleitungen enthalten (inklusive evtl. benötigter Vorkenntnisse, z. B. “Python 3 erforderlich”). pip install -r requirements.txt
.” Falls spezielle technische Voraussetzungen bestehen – etwa Zugriff auf bestimmte Hardware, ein Hochleistungsrechner oder große Speicherkapazitäten – sind diese zu nennen.
+
+--help
dokumentiert). Wissenschaftlicher Hintergrund und theoretischer Kontext
+Bekannte Limitationen, Annahmen und Fehlermeldungen
+Weiterentwicklung und Beitragsmöglichkeiten
+Projekt-Metadaten (Lizenz, Zitation, Version)
+Zusammenfassung der inhaltlichen Anforderungen
+Format und Struktur der Dokumentation
+
+README.md
als zentrales DokumentStrukturierte Unterteilung in weitere Dateien/Abschnitte
+
+
+example-project/
+├── README.md
+├── CONTRIBUTING.md (optional)
+├── CHANGELOG.md (optional)
+├── CITATION.md (oder CITATION.cff)
+├── LICENSE
+├── data/ (optional)
+│ └── sample_data.csv
+├── docs/ (optional)
+│ ├── INSTALL.md
+│ └── USAGE.md
+├── examples/ (optional)
+│ └── example_workflow.ipynb
+└── src/
+ ├── script.py
+ └── module/
+ └── helper.py
INSTALL.md
für ausführliche Installationshinweise, eine USAGE.md
oder TUTORIAL.md
für detaillierte Benutzeranleitungen, eine CHANGELOG.md
für Changelog etc.). Eine gängige Struktur ist z. B.:
+
+README.md
– Überblick (Ziel, Installation, kurzes Beispiel, Lizenz/Zitation)docs/
Verzeichnis mit weiteren .md-Dateien für tiefergehende Dokumentation (optional)CONTRIBUTING.md
– Hinweise für Beiträger (falls relevant)LICENSE
– LizenztextCITATION.cff
oder CITATION.md
– wie zu zitieren.Keine proprietären Formate oder Abhängigkeit von Werkzeugen
+Beispiele, Codeblöcke und ggf. Abbildungen einbinden
+Fazit Format und Struktur
+Umfang und Fokus der Dokumentation
+(Teil-)automatisierte Dokumentationswerkzeuge
+Docstrings und API-Dokumentationsgeneratoren
+Jupyter Notebooks und literate programming
+environment.yml
oder Dockerfiles, kleinen Beispieldatensätzen und klaren Anweisungen im Notebook). Ggf. kann man Notebooks auch in reine Markdown/HTML exportieren und dem Repo beilegen, damit zumindest statisch die Inhalte einsehbar sind.Sphinx/MkDocs/Doxygen (statische Dokumentationswebseiten)
+In-Code Hilfefunktionen und CL-Interface Doku
+--help
). Viele Nutzer greifen zunächst darauf zurück. Dieses Hilfemenü sollte kurz erläutern, welche Subkommandos oder Optionen existieren. Moderne CLI-Frameworks generieren solche Hilfen oft automatisch aus Ihrem Code (z. B. Click oder argparse in Python erzeugen --help
-Texte). Nutzen Sie das, um konsistente Infos zu garantieren.Versionskontrolle und kontinuierliche Dokumentationspflege
+Spezialfälle
+Wann ist was verpflichtend
+Best Practices, Vorlagen und Checklisten
+Orientierung an Nutzerbedürfnissen
+Checkliste für die Mindest-Dokumentation
+
+
+Positiv- und Negativbeispiele studieren
+Prinzipien: FAIR und ENDINGS
+Kontinuierliche Verbesserung und Feedback
+Zusammenfassung Best Practices
+Fazit
+Tabellarische Übersicht der Dokumentations-Bestandteile
+
+
+
+
+
+
+Dokuelement
+Inhalt/Purpose
+Format/Ort
+Umfang
+
+
+README (Hauptdoku)
+Zweck der Software; Kurzbeschreibung; Installationsanleitung; einfaches Nutzungsbeispiel; Lizenz- und Kontaktinfo
+Markdown im Root des Repos (statisch versioniert)
+1–2 Seiten
+
+
+Eingabe/Ausgabe-Guide
+Beschreibung der erwarteten Inputs (Datenformat, Parameter) und generierten Outputs (Dateien, Berichte) inkl. Beispielen
+Teil der README oder separate Datei (z.B. USAGE.md)
+1 Seite (mit Beispielen)
+
+
+Wissenschaftlicher Hintergrund
+Erläuterung der Methode, Theorie, Algorithmen; Verweise auf Literatur
+README-Abschnitt “Hintergrund” oder separate Doku (BACKGROUND.md)
+0.5–1 Seite (plus Referenzen)
+
+
+Bekannte Limitationen
+Auflistung von Einschränkungen, Annahmen, bekannten Problemen; ggf. Workarounds
+README-Abschnitt “Limitations” oder FAQ.md
+0.5 Seite
+
+
+Beispiel-Workflow (Tutorial)
+Schritt-für-Schritt Anleitung mit einem realistischen Anwendungsfall (ggf. mit Code und Screenshot)
+Jupyter Notebook (
+.ipynb
) im Repo examples/
Ordner oder Markdown in docs/1–3 Seiten / entsprechend Zellen
+
+
+API-Referenz
+Technische Dokumentation von Funktionen/Klassen für Entwickler*innen
+Automatisch generiert aus Docstrings (z.B. Sphinx in
+docs/
Ordner, HTML/PDF Ausgabe)Je nach Codegröße (ggf. umfangreich)
+
+
+CONTRIBUTING
+Anleitung für Beitragswillige: Code Style, Workflow, Tests, Kontakt
+CONTRIBUTING.md im Repo
+0.5–1 Seite
+
+
+LICENSE / CITATION
+Rechtliche Infos (Lizenztext); Zitationsleitfaden (Bevorzugte Zitierweise, DOI)
+Jeweils eigene Datei im Repo (Plain Text/Markdown)
+Kurz (Standardtext bzw. Referenz)
+
+
+
+Release-Information
+Versionshinweise, Änderungsprotokoll (Changelog)
+CHANGELOG.md oder Releases auf GitHub
+fortlaufend pro Version (Stichpunkte)
+Schlusswort
+Methodik / LLMs als ‘Autoren’
gpt-4.5 (preview)
. Abschließendes Korrekturlesen/inhaltliche Prüfung/Layouting durch Nicole Dresselhaus.Literatur
Zitat
@online{dresselhaus2025,
+ author = {Dresselhaus, Nicole and , GPT-4.5},
+ title = {Anforderungskatalog für die Dokumentation von
+ Forschungssoftware (Digital Humanities)},
+ date = {2025-05-08},
+ url = {https://nicole.dresselhaus.cloud/Writing/documentation.html},
+ langid = {de},
+ abstract = {Diese Dokumentation fasst zusammen, welche
+ wissenschaftlichen Konzepte, Algorithmen und Theorien hinter der
+ Software stehen. Sie dient dazu, den Nutzer*innen zu helfen, die
+ theoretischen Grundlagen nachvollziehbar zu machen.}
+}
+
Background: LLM-Based NER Method Overview
+
+
+<<PER ... /PER>>
to tag people, <<LOC ... /LOC>>
for locations, etc., covering each full entity span. This inline tagging format leveraged the model’s familiarity with XML/HTML syntax (from its training data) and largely eliminated problems like unclosed tags or extra spaces. By instructing the model not to alter any other text, they ensured the output could be easily compared to the input and parsed for entities.Solution Architecture
+
+
+"PER, ORG, LOC"
), and (b) the text to analyze (either included in the request or accessible via a provided file URL). This trigger node captures the input and starts the automation.<<PER John Doe /PER>> went to <<LOC Berlin /LOC>> ...
). This step harnesses the A100 GPU to generate results quickly, using the chosen model’s weights locally.{"entity": "John Doe", "type": "PER"}
objects). This parsing can be done with a Regex or code node, but given our focus on correctness, we often trust the model’s tagging format to be consistent (the paper reported the format was reliably followed when instructed clearly). Finally, an HTTP Response node sends the results back to the user (or stores them), completing the workflow.http://127.0.0.1:11434
by default) and the specific model name. We assume the base pipeline (available on GitHub) already includes most of this structure – our task is to slot in the custom prompt and model configuration for the NER use case.Setup and Infrastructure Requirements
+
+
+docker run -it --rm \
+ -p 5678:5678 \
+ -v ~/.n8n:/home/node/.n8n \
+ n8nio/n8n:latest
http://localhost:5678
for the web interface. (If you use Docker and plan to connect to a host-running Ollama, start the container with --network=host
to allow access to the Ollama API on localhost.)curl -sSL https://ollama.com/install.sh | sh
). After installation, start the Ollama server (daemon) by running:ollama serve
http://localhost:11434
in a browser – it should respond with “Ollama is running”. Note: Ensure your system has recent NVIDIA drivers and CUDA support if using GPU. Ollama supports NVIDIA GPUs with compute capability ≥5.0 (the A100 is well above this). Use nvidia-smi
to confirm your GPU is recognized. If everything is set up, Ollama will automatically use the GPU for model inference (falling back to CPU if none available).
+
ollama pull deepseek-r1:14b
deepseek-r1:70b
(~43 GB), but 14B is a good balance for our use-case. DeepSeek-R1 is licensed MIT and designed to run locally with no restrictions.ollama pull cogito:14b
ollama pull llama2:13b
would get a LLaMA-2 13B model. These can work, but for best results in NER with no fine-tuning, we suggest using one of the above well-instructed models. If your hardware is limited, you could try a 7-8B model (e.g., deepseek-r1:7b
or cogito:8b
), which download faster and use ~4–5 GB VRAM, at the cost of some accuracy. In CPU-only scenarios, even a 1.5B model is available – it will run very slowly and likely miss more entities, but it proves the pipeline can work on minimal hardware.Building the n8n Workflow
+1. Webhook Input for Entities and Text
+{
+ "entities": "PER, ORG, LOC",
+ "text": "John Doe visited Berlin in 1921 and met with the Board of Acme Corp."
+}
"entities"
is a simple comma-separated string of entity types (you could also accept an array or a more detailed schema; for simplicity we use the format used in the paper: PER for person, LOC for location, ORG for organization). The "text"
field contains the content to analyze. In a real scenario, the text could be much longer or might be sent as a file. If it’s a file, one approach is to send it as form-data and use n8n’s Read Binary File + Move Binary Data nodes to get it into text form. Alternatively, send a URL in the JSON and use an HTTP Request node in the workflow to fetch the content. The key is that by the end of this step, we have the raw text and the list of entity labels available in the n8n workflow as variables.2. Constructing the LLM Prompt
+
+
+<<TYPE ... /TYPE>>
. So instruct: “Enclose each entity in double angle brackets with its type label. For example: <<PER John Doe /PER>> for a person named John Doe. Do not alter any other text – only insert tags. Ensure every opening tag has a closing tag.” Also mention that tags can nest or overlap if necessary (though that’s rare).{ $json["prompt"] }
if the prompt was output to that field). The User Message can similarly be fed from the input text field (e.g., { $json["text"] }
). Essentially, we map our crafted instruction into the system role, and the actual content into the user role.3. Configuring the Local LLM (Ollama Model Node)
+http://127.0.0.1:11434
as the host (instead of the default localhost, to avoid any IPv6 binding issues). No API key is needed since it’s local. Once connected, you should see a dropdown of available models (all the ones you pulled). Choose the 14B model you downloaded, e.g. deepseek-r1:14b
or cogito:14b
.4. Returning the Results
+
+<<PER John Doe /PER>> visited <<LOC Berlin /LOC>> in 1921 and met with the Board
+of <<ORG Acme Corp /ORG>>.
<<(\w+) (.*?) /\1>>
to extract all type
and entity
pairs from the text. In n8n, a quick approach is to use a Function node to find all matches of that pattern in item.json["data"]
(assuming the LLM output is in data
). Then one could return a JSON array of entities. However, since our focus is on correctness and ease, you might simply return the marked-up text and perhaps document how to parse it externally if the user wants structured data.Model Selection Considerations
+
+
+<<PER Herr Schmidt /PER>>
. Always test on a small sample if in doubt.Example Run
+
+
+PER, ORG, LOC
+<<PER Baron Münchhausen /PER>> was born in <<LOC Bodenwerder /LOC>> and served
+in the Russian military under <<PER Empress Anna /PER>>. Today, the <<ORG
+Münchhausen Museum /ORG>> in <<LOC Bodenwerder /LOC>> is operated by the town
+council.
<<PER>>
tags, the museum is marked as an organization, and the town Bodenwerder is marked as a location (twice). The rest of the sentence remains unchanged. This output can be returned as-is to the user. They can visually verify it or programmatically parse out the tagged entities. The correctness of outputs is high: each tag corresponds to a real entity mention in the text, and there are no hallucinated tags. If the model were to make an error (say, tagging “Russian” as LOC erroneously), the user could adjust the prompt (for example, clarify that national adjectives are not entities) and re-run.Limitations and Solutions
+
+
+N8N_DEFAULT_TIMEOUT
or in the workflow settings).Conclusion
+Methodik / LLMs als ‘Autoren’
gpt-4.5 (preview)
. Abschließendes Korrekturlesen/inhaltliche Prüfung/Layouting durch Nicole Dresselhaus.References
Citation
@online{2025,
+ author = {, GPT-4.5 and , cogito-v1-preview and Dresselhaus, Nicole},
+ title = {Case {Study:} {Local} {LLM-Based} {NER} with N8n and
+ {Ollama}},
+ date = {2025-05-05},
+ url = {https://nicole.dresselhaus.cloud/Writing/ner4all-case-study.html},
+ langid = {en},
+ abstract = {Named Entity Recognition (NER) is a foundational task in
+ text analysis, traditionally addressed by training NLP models on
+ annotated data. However, a recent study – \_“NER4All or Context is
+ All You Need”\_ – showed that out-of-the-box Large Language Models
+ (LLMs) can **significantly outperform** classical NER pipelines
+ (e.g. spaCy, Flair) on historical texts by using clever prompting,
+ without any model retraining. This case study demonstrates how to
+ implement the paper’s method using entirely local infrastructure: an
+ **n8n** automation workflow (for orchestration) and a **Ollama**
+ server running a 14B-parameter LLM on an NVIDIA A100 GPU. The goal
+ is to enable research engineers and tech-savvy historians to
+ **reproduce and apply this method easily** on their own data, with a
+ focus on usability and correct outputs rather than raw performance.
+ We will walk through the end-to-end solution – from accepting a
+ webhook input that defines entity types (e.g. Person, Organization,
+ Location) to prompting a local LLM to extract those entities from a
+ text. The solution covers setup instructions, required
+ infrastructure (GPU, memory, software), model configuration, and
+ workflow design in n8n. We also discuss potential limitations (like
+ model accuracy and context length) and how to address them. By the
+ end, you will have a clear blueprint for a **self-hosted NER
+ pipeline** that leverages the knowledge encoded in LLMs (as
+ advocated by the paper) while maintaining data privacy and
+ reproducibility.}
+}
+
Hintergrund und Zielsetzung
+Person
, Projekt
, Deliverable
etc.) verwaltet werden. Die Notizen enthalten strukturierte YAML-Metadaten (unterstützt durch Plugins wie Metadata Menu) und sind durch viele Wiki-Links miteinander vernetzt. Standardisierte Templates (via Templater) sorgen dafür, dass z.B. Personenseiten immer ähnliche Felder (Name, ORCID, etc.) aufweisen.[[...]]
) müssen im LLM-Ausgabeformat nicht unbedingt klickbar sein (es genügt, wenn sie referenziert werden).1. Vektorbasierter Ansatz: Semantic Search mit Embeddings
+Prinzip
+Implementierung
+nomic-embed-text
verwenden, welches kompakte 1024-dimensionale Textvektoren liefert [1]. Die Notizen des Obsidian Vault würden per Skript täglich eingelesen, in Sinnabschnitte (Chunks) aufgeteilt (z.B. nach Überschriften oder einer festen Token-Länge) und über Ollamas Embedding-API in Vektoren umgewandelt [1]. Diese Vektoren speichert man in einer lokalen DB wie Chroma. Anfragen an das LLM werden dann zunächst an den Vektorstore gestellt, um die relevantesten Notiz-Abschnitte zu finden, welche dann zusammen mit der eigentlichen Frage an das LLM gegeben werden (klassischer RAG-Pipeline). Dieses Verfahren ist vergleichbar mit dem Smart Connections Obsidian-Plugin: Dort wird ebenfalls ein “Text Embedding Model” auf den Vault angewendet, um zu einer Nutzerfrage automatisch thematisch passende Notizen zu finden und dem LLM bereitzustellen [2]. So konnte im Beispiel ein lokales LLaMA-basiertes Modell Fragen zum eigenen Vault korrekt beantworten, indem es zuvor den passenden Ausschnitt (hier: eine Styleguide-Notiz) über Embeddings gefunden hatte [2].Integration mit lokalen LLMs
+Leistung
+Nachteile und Aufwand
+Zusammenfassung – Ansatz 1: Vektordatenbank (Embeddings)
+
+
+
+
+
+
+
+ Details
+
+
+Vorgehen
+Inhalte aller Markdown-Dateien in semantische Vektoren kodieren (z.B. mit
+nomic-embed-text
([1])) und in einer Vektor-DB speichern. LLM-Anfragen per Similarity Search mit relevantem Kontext anreichern.
+
+Stärken
+Semantische Suche (findet thematisch passende Infos, nicht nur exakte Worttreffer) [5]. Skaliert auf große Textmengen. Bereits heute mit lokalen LLMs erprobt (z.B. Smart Connections Plugin) [2]. Gut geeignet für Q&A, Textzusammenfassungen und Link-Vorschläge basierend auf Ähnlichkeit.
+
+
+Schwächen
+Komplexeres Setup (Embedding-Model + DB + Pipeline) [5]. Hoher Rechenaufwand für Embeddings bei großen Vaults. Kein explizites Modell von Beziehungen/Metadaten – strukturierte Abfragen (z.B. “zeige alle Personen ohne ORCID”) nur mit Zusatzlogik. Kontexttreffer können ungenau sein (erfordert ggf. Feinjustierung).
+
+
+
+Integrations-Optionen
+Lokale LLM-Einbindung möglich (z.B. LLaMA 2 über Ollama-API). Tools: ChromaDB, Weaviate oder FAISS als Vektorstore; LangChain/LlamaIndex für Pipeline-Management. Obsidian-Plugins: Smart Connections (komplett integriert mit lokalem Embedder+LLM) [2]; Khoj (separater Suchassistent mit Embeddings) [4].
+graph LR
+ A[Obsidian Vault] --> B[Chunking]
+ B --> C[Embedding Model]
+ C --> D[(Vector Database)]
+ E[User Query] --> F[Query Embedding]
+ F --> G[Similarity Search]
+ D --> G
+ G --> H[Relevant Chunks]
+ H --> I[LLM]
+ E --> I
+ I --> J[Response]
+
+2. Knowledge-Graph-Ansatz: Strukturierte Graphdatenbank
+Prinzip
+Person
, Projekt
etc.). Relationen zwischen Notizen – implizit durch Obsidian-Wikilinks gegeben – werden zu expliziten Kanten im Graph (z.B. eine Person “arbeitet in” Organisation, ein Projekt “liefert” ein Deliverable). Auch Metadaten aus YAML können als Knoten oder Properties modelliert werden (z.B. ORCID als Attribut eines Person-Knotens, Tags als Relationen “hat Schlagwort” usw.). Das Ergebnis ist ein Wissensgraph, der ähnlich einem klassischen RDF-Triple-Store oder Neo4j-Property-Graph komplexe Abfragen und Analysen ermöglicht.Erstellung des Graphen
+[[Albert Einstein|Autor]]
könnte der Alias “Autor” als Kanten-Label genutzt werden). Da Obsidian standardmäßig keine typisierten Kanten unterstützt, bleiben Relationstypen begrenzt – Plugins wie Juggl oder Graph-Link-Types erlauben allerdings das Hinzufügen von Link-Metadaten, was für eine genauere Graph-Modellierung hilfreich sein könnte [8]. YAML-Inhalte, die auf andere Notizen referenzieren, können ebenfalls als Kanten kodiert werden (Beispiel: In einer Projekt-Notiz listet das YAML-Feld team:
mehrere Personen – diese Verweise werden im Graph als Kanten Projekt —hatTeam→ Person umgesetzt). Nicht referenzielle Metadaten (etwa ein ORCID-Wert) bleiben einfach als Datenfeld am Knoten.Nutzung für LLM-Aufgaben
+Integration mit LLMs
+
+
+Zusammenfassung - Ansatz 2: Graphdatenbank
+
+
+
+
+
+
+
+ Details
+
+
+Vorgehen
+Konvertiere Vault-Inhalte in einen strukturierten Graphen (Knoten = Notizen/Entitäten; Kanten = Obsidian-Links oder abgeleitete Relationen). Nutzen von Graph-DB (Neo4j, RDF-Store) für Abfragen und Analysen.
+
+
+Stärken
+Explizite Struktur: ermöglicht genaue Abfragen, z.B. Finde fehlende Werte oder alle Verknüpfungen eines Knotens auf einen Blick. Logische Inferenzen möglich (Graph Reasoning) – unterstützt Link-Empfehlungen und Konsistenzprüfungen [8]. Gute Ergänzung zu YAML-Typisierung: FileClass-Struktur wird vollständig nutzbar. Persistenz: Graph kann unabhängig von Obsidian analysiert, versioniert, mit anderen Daten gemappt werden (z.B. ORCID-Abgleich via externen Datensatz).
+
+
+Schwächen
+Erheblicher Initialaufwand: Datenmodell entwerfen, Export-Skripte schreiben oder Tools einrichten [7]. Keine fertige Out-of-the-box-Lösung für Obsidian↔︎Graph (bislang nur Ansätze in der Community). Laufende Synchronisation nötig (Vault-Änderungen -> Graph-Update). Die LLM-Integration ist komplexer – erfordert Query-Tool oder das Einbetten von Graph-Daten ins Prompt. Für offene Fragen (Freitext) allein nicht ausreichend, da der Graph primär Fakten repräsentiert, nicht Fließtext.
+
+
+
+Integrations-Optionen
+Neo4j (mit APOC/Neosemantics für RDF-Unterstützung) eignet sich für Property-Graph-Modell; Apache Jena oder GraphDB für RDF-Triple-Store. LangChain bietet Memory/Agent, um Wissensgraphen abzufragen (z.B. ConversationKGMemory). LlamaIndex hat einen KnowledgeGraphIndex, der Tripel extrahieren und durchs LLM traversieren kann. Diese Lösungen sind aber noch experimentell. Evtl. Kombination mit Obsidian-Plugin: Ein früher Plugin-Prototyp streamte Obsidian-Daten nach Neo4j [7] – dieser könnte als Ausgangspunkt dienen.
+graph LR
+ A[Obsidian Vault] --> B[Entity Extraction]
+ A --> C[Relationship Extraction]
+ B --> D[Graph Construction]
+ C --> D
+ D --> E[(Graph Database)]
+ F[User Query] --> G[Query Parser]
+ G --> H[Graph Traversal]
+ E --> H
+ H --> I[Structured Facts]
+ I --> J[LLM]
+ F --> J
+ J --> K[Response]
+
+Fazit Graphdatenbank
+3. Hybrid-Ansatz: Kombination aus Graph und Vektor-RAG
+
+
+[Text]
, die vom Typ Projekt sind und nach 2020 erstellt wurden.” – Weaviate würde erst nach Ähnlichkeit filtern, dann die Metadaten-Bedingungen anwenden. So eine hybride Suche könnte man nutzen, um etwa bei Template-Befüllung nur vergleichbare Objekte zum Prompt hinzuzufügen (z.B. nur andere Organisationen, keine Meeting-Notizen). Auch ChromaDB arbeitet an Feature-Filterfunktionen, die so etwas erlauben würden. Alternativ kann man den Graphen selbst mit Vektor-Embeddings anreichern: Man könnte jedem Knotentyp einen eigenen Vektor zuordnen, der den gesamten Inhalt der zugehörigen Notiz(en) repräsentiert. Diese Vektoren ließen sich im Graphen als Attribut halten und für Ähnlichkeitssuchen zwischen Knoten verwenden (knowledge graph embeddings). Allerdings ist das experimentell – man müsste z.B. bei Kanten-Traversierung dynamisch Nachbarschaftsvektoren kombinieren, was nicht trivial ist.Vor-/Nachteile
+Integrationsmöglichkeiten
+Zusammenfassung – Ansatz 3: Hybrid-Lösung
+
+
+
+
+
+
+
+ Details
+
+
+Vorgehen
+Kombination beider Ansätze: Pflege einer Graph-Struktur und eines Vektorindex. Nutzung je nach Bedarf – entweder separat oder durch orchestrierte Abfragen, um sowohl strukturiertes Wissen als auch relevante Texte bereitzustellen.
+
+
+Stärken
+Sehr leistungsfähig – deckt sowohl faktische als auch kontextuelle Fragen ab. Kann die höchste Antwortqualität liefern (Konsistenz durch Graph-Fakten, Detail durch Textauszüge). Hilft, sowohl “Known-item” Suchen (explizite Werte) als auch “Open-ended” Suchen (Texte) zu bedienen. Für Link-Vorschläge ideal: Kombination aus semantischer Ähnlichkeit und Graph-Nachbarschaft erhöht Trefferquote sinnvoll.
+
+
+Schwächen
+Sehr komplex in Umsetzung und Wartung. Erfordert doppelte Infrastruktur. Koordination zwischen Graph und Vectorstore nötig – potenziell fehleranfällig. Höhere Latenz durch Mehrfach-Abfragen. Nur lohnend, wenn wirklich vielfältige Aufgaben automatisiert werden sollen; für rein textliche Q&A overkill.
+
+
+
+Integrations-Optionen
+Weaviate (Vectors + strukturierte Class-Properties in einem System), oder Kombination aus Neo4j + Chroma. LangChain Agents könnten Graphund Vektor-Tools parallel nutzen. LlamaIndex bietet experimentell kombinierbare Indizes. Workflows müssen sorgfältig entworfen werden (z.B. zuerst Graph-Query, dann Vector-Query auf Untermenge).
+graph LR
+ A[Obsidian Vault] --> B[Chunking]
+ A --> C[Entity Extraction]
+ B --> D[Embedding Model]
+ C --> E[Graph Construction]
+ D --> F[(Vector Database)]
+ E --> G[(Graph Database)]
+ H[User Query] --> I[Query Embedding]
+ H --> J[Graph Query]
+ I --> K[Vector Search]
+ J --> L[Graph Traversal]
+ F --> K
+ G --> L
+ K --> M[Text Context]
+ L --> N[Structured Facts]
+ M --> O[Combined Context]
+ N --> O
+ H --> P[LLM]
+ O --> P
+ P --> Q[Enriched Response]
+
+Fazit Hybrid-Lösung
+4. Datenaufbereitung: YAML-Metadaten extrahieren und normalisieren
+fullname
, birthdate
, ORCID
usw.). Ein LLM könnte zwar theoretisch auch direkt im Markdown nach diesen Mustern suchen, aber es ist deutlich effizienter, die Daten einmalig zu extrahieren und in einer leichter nutzbaren Form vorzuhalten.Extraktion
+Normalisierung
+null
markiert werden. Außerdem kann man hier foreign-key-Bezüge auflösen: Wenn z.B. im YAML einer Publikation author: "[[Doe, John]]"
steht, könnte das Skript erkennen, dass dies die Person mit UID XYZ ist, und entsprechend in der extrahierten Struktur statt des Link-Codes einen eindeutigen Verweis (auf die Person John Doe) speichern. Diese Normalisierung erleichtert nachfolgende Analysen enorm – insbesondere kann man einfache Regeln ableiten, die dann vom LLM geprüft oder genutzt werden. Zum Beispiel: “Wenn person.ORCID
leer ist, schlage vor, ihn zu ergänzen” – das kann das LLM dann direkt als Aufforderung bekommen. Oder: “Beim Erstellen einer neuen Person fülle Felder X,Y nach Vorlage aus” – hier weiß man aus der YAML-Definition bereits, welche Felder existieren müssen.Nutzung durch LLM
+
+
+Person[42]
der ORCID. Hier ist eine Liste aller Personennamen mit ORCID, finde anhand des Namens den passenden ORCID und trage ihn ein.” – Falls die Person woanders erwähnt wurde, könnte das Modell es herausfinden. (Eher unwahrscheinlich ohne Internetzugriff – ORCID erfordert eher einen API-Call, aber zumindest könnte das LLM erkennen, dass es fehlt und ggf. den Nutzer nach der ID fragen). Für Link-Empfehlungen könnte man dem LLM eine Liste aller Titel geben – oder besser direkt die Graph-Info wie “Person A und Person B haben 3 gemeinsame Projekte” – siehe Hybrid-Ansatz.[[Link]]
markiert sind; wenn nicht, die Stelle hervorheben und dem LLM als “Kandidat für Verlinkung” präsentieren. Oder bei einer neuen Organisation könnten automatisch Felder aus externen APIs gezogen und ins Template eingetragen werden (sofern erlaubt). Das LLM hätte dann eher die Rolle, die zusammengestellten Infos in schönen Prosa-Text zu gießen, anstatt die Fakten selbst zu suchen.Beispiel-Workflows
+
+
+os
und pyyaml
kann alle .md
Files scannen.Vor- & Nachteile
+
+Als Nachteil kann gesehen werden, dass es zusätzlicher Implementierungsaufwand ist und eine gewisse Duplizierung der Daten (die YAML-Inhalte leben dann in zwei Formen: im Markdown und in der extrahierten Sammlung). Die Synchronisation muss bei Änderungen immer gewährleistet sein (Cronjob). Allerdings ist das, verglichen mit dem Aufwand der LLM-Integration, relativ gering und gut automatisierbar.graph LR
+ A[Obsidian Vault] --> B[FileClass Detection]
+ B --> C[Type-Specific Extraction]
+ C --> D[YAML Parser]
+ D --> E[Data Validation]
+ E --> F[Type Normalization]
+ F --> G[(Typed Collections)]
+ H[Task Request] --> I[Schema Lookup]
+ I --> J[Targeted Data Fetch]
+ G --> J
+ J --> K[Context Assembly]
+ H --> K
+ K --> L[LLM Processing]
+ L --> M[Schema-Aware Output]
+
+Zusammenfassung – Ansatz 4: Extraktion
+5. Automatisierungstools und Workflows
+Obsidian-Plugins (In-App KI-Features)
+
+
+Externe Anwendungen / Skripte
+
+
+
+Für bestimmte Felder wie ORCID wird ein rein lokales LLM kaum die Werte erraten können, sofern sie nicht schon irgendwo im Vault stehen. Falls Internetzugriff eine Option ist, könnte man ein Plugin oder einen Workflow integrieren, der ORCID API Abfragen durchführt (z.B. über den Namen der Person) und die ID zurückliefert. Ein LLM-Agent könnte auch so einen API-Call ausführen (via Tools in LangChain). Alternativ: Alle bekannten ORCID-IDs der eigenen Personen könnte man in einer Datei sammeln; wenn das LLM eine Lücke findet, bittet es den Nutzer um Input. Hier muss man die Limitierungen eines LLM realistisch sehen und ggf. klassische Automatisierung (API-Skripte) kombinieren.graph LR
+ subgraph Obsidian
+ A[Vault] --> B[Plugins]
+ B --> C[Templater]
+ B --> D[Metadata Menu]
+ B --> E[AI Assistant]
+ end
+
+ subgraph External Processing
+ A --> F[Daily Export]
+ F --> G[Data Processing]
+ G --> H[LLM Analysis]
+ H --> I[Automation Scripts]
+ end
+
+ subgraph Integration
+ I --> J[Change Proposals]
+ J --> K[User Review]
+ K --> L[Accepted Changes]
+ L --> M[Vault Updates]
+ M --> A
+ end
+
+Zusammenfassende Empfehlung
+Quellen
+Methodik / LLMs als ‘Autoren’
gpt-4.5 (preview)
. Systematische Überarbeitungen (Extraktion Bibliographie, Überarbeitung Metadaten) mittels cogito-v0.1
im Editor. Übernahme nach manueller Prüfung. Erstellung der Mermaid-Diagramme mittels Claude 3.7 Sonnet
. Abschließendes Korrekturlesen/inhaltliche Prüfung/Layouting durch Nicole Dresselhaus.Literatur
{-# OPTIONS_GHC -Wno-name-shadowing #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+module MyService where
+
+-- generische imports aus den dependencies/base, nicht in der prelude
+import Codec.MIME.Type
+import Configuration.Dotenv as Dotenv
+import Control.Concurrent (forkIO, threadDelay)
+import Control.Concurrent.Async
+import Control.Concurrent.STM
+import Control.Monad
+import Control.Monad.Catch
+import Control.Monad.Except
+import Conversion
+import Conversion.Text ()
+import Data.Binary.Builder
+import Data.String (IsString (..))
+import Data.Time
+import Data.Time.Clock
+import Data.Time.Format
+import Data.Default
+import Network.HostName
+import Network.HTTP.Client as HTTP hiding
+ (withConnection)
+import Network.HTTP.Types (Status, statusCode)
+import Network.Mom.Stompl.Client.Queue
+import Network.Wai (Middleware)
+import Network.Wai.Logger
+import Network.Wai.Middleware.Cors
+import Network.Wai.Middleware.RequestLogger (OutputFormat (..),
+ logStdout,
+ mkRequestLogger,
+ outputFormat)
+import Servant.Client (mkClientEnv,
+ parseBaseUrl)
+import System.Directory
+import System.Envy
+import System.IO
+import System.Log.FastLogger
+import Text.PrettyPrint.GenericPretty
+
+-- generische imports, aber qualified, weil es sonst zu name-clashes kommt
+
+import qualified Data.ByteString as BS
+-- import qualified Data.ByteString.Char8 as BS8
+import qualified Data.ByteString.Lazy as LBS
+import qualified Network.HTTP.Client.TLS as UseDefaultHTTPSSettings (tlsManagerSettings)
+import qualified Network.Mom.Stompl.Client.Queue as AMQ
+import qualified Network.Wai as WAI
+
+-- Handler für den MyServiceBackend-Typen und Imports aus den Libraries
+import MyService.Handler as H -- handler der H.myApiEndpointV1Post implementiert
+import MyService.Types -- weitere Type (s. nächste box)
+import MyServiceGen.API as MS -- aus der generierten library
+
+
+myServicemain :: IO ()
+myServicemain = do
+ -- .env-Datei ins Prozess-Environment laden, falls noch nicht von außen gesetzt
+ void $ loadFile $ Dotenv.Config [".env"] [] False
+ -- Config holen (defaults + overrides aus dem Environment)
+ sc@ServerConfig{..} <- decodeWithDefaults defConfig
+ -- Backend-Setup
+ -- legt sowas wie Proxy-Server fest und wo man wie dran kommt. Benötigt für das Sprechen mit anderen Microservices
+ let defaultHTTPSSettings = UseDefaultHTTPSSettings.tlsManagerSettings { managerResponseTimeout = responseTimeoutMicro $ 1000 * 1000 * myserviceMaxTimeout }
+ createBackend url proxy = do
+ manager <- newManager . managerSetProxy proxy
+ $ defaultHTTPSSettings
+ url' <- parseBaseUrl url
+ return (mkClientEnv manager url')
+ internalProxy = case myserviceInternalProxyUrl of
+ "" -> noProxy
+ url -> useProxy $ HTTP.Proxy (fromString url) myserviceInternalProxyPort
+ -- externalProxy = case myserviceExternalProxyUrl of
+ -- "" -> noProxy
+ -- url -> useProxy $ HTTP.Proxy (fromString url) myserviceExternalProxyPort
+
+ -- Definieren & Erzeugen der Funktionen um die anderen Services anzusprechen.
+ calls <- (,)
+ <$> createBackend myserviceAUri internalProxy
+ <*> createBackend myserviceBUri internalProxy
+
+ -- Logging-Setup
+ hSetBuffering stdout LineBuffering
+ hSetBuffering stderr LineBuffering
+
+
+ -- Infos holen, brauchen wir später
+ myName <- getHostName
+ today <- formatTime defaultTimeLocale "%F" . utctDay <$> getCurrentTime
+
+
+ -- activeMQ-Transaktional-Queue zum schreiben nachher vorbereiten
+ amqPost <- newTQueueIO
+
+
+ -- bracket a b c == erst a machen, ergebnis an c als variablen übergeben. Schmeisst c ne exception/wird gekillt/..., werden die variablen an b übergeben.
+ bracket
+ -- logfiles öffnen
+ (LogFiles <$> openFile ("/logs/myservice-"<>myName<>"-"<>today<>".info") AppendMode
+ <*> openFile (if myserviceDebug then "/logs/myservice-"<>myName<>"-"<>today<>".debug" else "/dev/null") AppendMode
+ <*> openFile ("/logs/myservice-"<>myName<>"-"<>today<>".error") AppendMode
+ <*> openFile ("/logs/myservice-"<>myName<>"-"<>today<>".timings") AppendMode
+ )
+ -- und bei exception/beendigung schlißen.h
+ (\(LogFiles a b c d) -> mapM_ hClose [a,b,c,d])
+ $ \logfiles -> do
+
+
+ -- logschreibe-funktionen aliasen; log ist hier abstrakt, iolog spezialisiert auf io.
+ let log = printLogFiles logfiles :: MonadIO m => [LogItem] -> m ()
+ iolog = printLogFilesIO logfiles :: [LogItem] -> IO ()
+
+
+ -- H.myApiEndpointV1Post ist ein Handler (alle Handler werden mit alias H importiert) und in einer eigenen Datei
+ -- Per Default bekommen Handler sowas wie die server-config, die Funktionen um mit anderen Services zu reden, die AMQ-Queue um ins Kibana zu loggen und eine Datei-Logging-Funktion
+ -- Man kann aber noch viel mehr machen - z.b. gecachte Daten übergeben, eine Talk-Instanz, etc. pp.
+ server = MyServiceBackend{ myApiEndpointV1Post = H.myApiEndpointV1Post sc calls amqPost log
+ }
+ config = MS.Config $ "http://" ++ myserviceHost ++ ":" ++ show myservicePort ++ "/"
+ iolog . pure . Info $ "Using Server configuration:"
+ iolog . pure . Info $ pretty sc { myserviceActivemqPassword = "******" -- Do NOT log the password ;)
+ , myserviceMongoPassword = "******"
+ }
+ -- alle Services starten (Hintergrund-Aktionen wie z.b. einen MongoDB-Dumper, einen Talk-Server oder wie hier die ActiveMQ
+ void $ forkIO $ keepActiveMQConnected sc iolog amqPost
+ -- logging-Framework erzeugen
+ loggingMW <- loggingMiddleware
+ -- server starten
+ if myserviceDebug
+ then runMyServiceMiddlewareServer config (cors (\_ -> Just (simpleCorsResourcePolicy {corsRequestHeaders = ["Content-Type"]})) . loggingMW . logStdout) server
+ else runMyServiceMiddlewareServer config (cors (\_ -> Just (simpleCorsResourcePolicy {corsRequestHeaders = ["Content-Type"]}))) server
+
+
+-- Sollte bald in die Library hs-stomp ausgelagert werden
+-- ist ein Beispiel für einen ActiveMQ-Dumper
+keepActiveMQConnected :: ServerConfig -> ([LogItem] -> IO ()) -> TQueue BS.ByteString -> IO ()
+keepActiveMQConnected sc@ServerConfig{..} printLog var = do
+ res <- handle (\(e :: SomeException) -> do
+ printLog . pure . Error $ "Exception in AMQ-Thread: "<>show e
+ return $ Right ()
+ ) $ AMQ.try $ do -- catches all AMQ-Exception that we can handle. All others bubble up.
+ printLog . pure . Info $ "AMQ: connecting..."
+ withConnection myserviceActivemqHost myserviceActivemqPort [ OAuth myserviceActivemqUsername myserviceActivemqPassword
+ , OTmo (30*1000) {- 30 sec timeout -}
+ ]
+ [] $ \c -> do
+ let oconv = return
+ printLog . pure . Info $ "AMQ: connected"
+ withWriter c "Chaos-Logger for Kibana" "chaos.logs" [] [] oconv $ \writer -> do
+ printLog . pure . Info $ "AMQ: queue created"
+ let postfun = writeQ writer (Type (Application "json") []) []
+ void $ race
+ (forever $ atomically (readTQueue var) >>= postfun)
+ (threadDelay (600*1000*1000)) -- wait 10 Minutes
+ -- close writer
+ -- close connection
+ -- get outside of all try/handle/...-constructions befor recursing.
+ case res of
+ Left ex -> do
+ printLog . pure . Error $ "AMQ: "<>show ex
+ keepActiveMQConnected sc printLog var
+ Right _ -> keepActiveMQConnected sc printLog var
+
+
+-- Beispiel für eine Custom-Logging-Middleware.
+-- Hier werden z.B. alle 4xx-Status-Codes inkl. Payload ins stdout-Log geschrieben.
+-- Nützlich, wenn die Kollegen ihre Requests nicht ordentlich schreiben können und der Server das Format zurecht mit einem BadRequest ablehnt ;)
+loggingMiddleware :: IO Middleware
+loggingMiddleware = liftIO $ mkRequestLogger $ def { outputFormat = CustomOutputFormatWithDetails out }
+ where
+ out :: ZonedDate -> WAI.Request -> Status -> Maybe Integer -> NominalDiffTime -> [BS.ByteString] -> Builder -> LogStr
+ out _ r status _ _ payload _
+ | statusCode status < 300 = ""
+ | statusCode status > 399 && statusCode status < 500 = "Error code "<>toLogStr (statusCode status) <>" sent. Request-Payload was: "<> mconcat (toLogStr <$> payload) <> "\n"
+ | otherwise = toLogStr (show r) <> "\n"
{-# OPTIONS_GHC -Wno-orphans #-}
+{-# OPTIONS_GHC -Wno-name-shadowing #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveFunctor #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingVia #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE RecordWildCards #-}
+module MyService.Types where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.Text
+import Data.Time.Clock
+import GHC.Generics
+import System.Envy
+import Text.PrettyPrint (text)
+import Text.PrettyPrint.GenericPretty
+
+-- Out hat hierfür keine Instanzen, daher kurz eine einfach Definition.
+instance Out Text where
+ doc = text . unpack
+ docPrec i a = text $ showsPrec i a ""
+
+instance Out UTCTime where
+ doc = text . show
+ docPrec i a = text $ showsPrec i a ""
+
+-- Der ServerConfig-Typ. Wird mit den defaults unten initialisiert, dann mit den Variablen aus der .env-Datei überschrieben und zum Schluss können Serveradmins diese via $MYSERVICE_FOO nochmal überschreiben.
+data ServerConfig = ServerConfig
+ { myserviceHost :: String -- ^ Environment: $MYSERVICE_HOST
+ , myservicePort :: Int -- ^ Environment: $MYSERVICE_PORT
+ , myserviceMaxTimeout :: Int -- ^ Environment: $MYSERVICE_MAX_TIMEOUT
+ , myserviceInternalProxyUrl :: String -- ^ Environment: $MYSERVICE_INTERNAL_PROXY_URL
+ , myserviceInternalProxyPort :: Int -- ^ Environment: $MYSERVICE_INTERNAL_PROXY_PORT
+ , myserviceExternalProxyUrl :: String -- ^ Environment: $MYSERVICE_EXTERNAL_PROXY_URL
+ , myserviceExternalProxyPort :: Int -- ^ Environment: $MYSERVICE_EXTERNAL_PROXY_PORT
+ , myserviceActivemqHost :: String -- ^ Environment: $MYSERVICE_ACTIVEMQ_HOST
+ , myserviceActivemqPort :: Int -- ^ Environment: $MYSERVICE_ACTIVEMQ_PORT
+ , myserviceActivemqUsername :: String -- ^ Environment: $MYSERVICE_ACTIVEMQ_USERNAME
+ , myserviceActivemqPassword :: String -- ^ Environment: $MYSERVICE_ACTIVEMQ_PASSWORD
+ , myserviceMongoUsername :: String -- ^ Environment: $MYSERVICE_MONGO_USERNAME
+ , myserviceMongoPassword :: String -- ^ Environment: $MYSERVICE_MONGO_PASSWORD
+ , myserviceDebug :: Bool -- ^ Environment: $MYSERVICE_DEBUG
+ } deriving (Show, Eq, Generic)
+
+-- Default-Konfigurations-Instanz für diesen Service.
+instance DefConfig ServerConfig where
+ defConfig = ServerConfig "0.0.0.0" 8080 20
+ ""
+ ""
+ ""
+ 0
+ ""
+ 0
+ ""
+ 0
+ ""
+ ""
+ ""
+ ""
+ False
+
+-- Kann auch aus dem ENV gefüllt werden
+instance FromEnv ServerConfig
+-- Und hübsch ausgegeben werden.
+instance Out ServerConfig
+
+
+instance Out Response
+instance FromBSON Repsonse -- FromBSON-Instanz geht immer davon aus, dass alle keys da sind (ggf. mit null bei Nothing).
Erster Schritt ist immer ein wünsch-dir-was bei der Api-Defenition.
+Die meisten Services haben offensichtliche Anforderungen (Schnittstellen nach draußen, Schnittstellen intern, …). Diese kann man immer sehr gut in einem Request -> Response
-Model erfassen.
Um die Anforderungen und Möglichkeiten des jeweiligen Services sauber zu erfassen und automatisiert zu prüfen, dummy-implementationen zu bekommen und vieles andere mehr, empfiehlt es sich den Openapi-generator zu nutzen.
+Diese Definition läuft über openapi-v3 und kann z.b. mit Echtzeit-Vorschau im http://editor.swagger.io/ erspielen. Per Default ist der noch auf openapi-v2 (aka swagger), kann aber auch v3.
+Nach der Definition, was man am Ende haben möchte, muss man sich entscheiden, in welcher Sprache man weiter entwickelt. Ich empfehle aus verschiedenen Gründen primär 2 Sprachen: Python-Microservices (weil die ML-Libraries sehr gut sind, allerdings Änderungen meist schwer sind und der Code wenig robust - meist nur 1 API-Endpunkt pro service) und Haskell (Stabilität, Performace, leicht zu ändern, gut anzupassen).
+Im folgenden wird (aus offensichtlichen Gründen) nur auf das Haskell-Projekt eingegangen.
+ +Zunächst erstellen wir in normales Haskell-Projekt ohne Funktionalität & Firlefanz:
+stack new myservice
Dies erstellt ein neues Verzeichnis und das generelle scaffolding. Nach einer kurzen Anpassung der stack.yaml
(resolver auf unserer setzen; aktuell: lts-17.4
) fügen wir am Ende der Datei
allow-newer: true
+ghc-options:
+ "$locals": -fwrite-ide-info
ein. Anschließend organisieren™ wir uns noch eine gute .gitignore
und initialisieren das git mittels git init; git add .; git commit -m "initial scaffold"
Da die API immer wieder neu generiert werden kann (und sollte!) liegt sich in einem unterverzeichnis des Hauptprojektes.
+Initial ist es das einfachste ein leeres temporäres Verzeichnis woanders zu erstellen, die api-doc.yml
hinein kopieren und folgendes ausführen:
openapi-generator generate -g haskell -o . -i api-doc.yml
Dieses erstellt einem dann eine komplette library inkl. Datentypen. Wichtig: Der Name in der api-doc
sollte vom Namen des Services (oben myservice
) abweichen - entweder in Casing oder im Namen direkt. Suffixe wie API schneidet der Generator hier leider ab. (Wieso das ganze? Es entstehen nachher 2 libraries, foo
& fooAPI
. Da der generator das API abschneidet endet man mit foo & foo und der compiler meckert, dass er nicht weiß, welche lib gemeint ist).
danach: wie gewohnt git init; git add .; git commit -m "initial"
. Auf dem Server der Wahl (github, gitea, gitlab, …) nun ein Repository erstellen (am Besten: myserviceAPI
- nach Konvention ist alles auf API endend autogeneriert!) und den Anweisungen nach ein remote hinzufügen & pushen.
In unserem eigentlichen Service müssen wir nun die API einbinden. Dazu erstellen wir ein Verzeichnis libs
(Konvention) und machen ein git submodule add <repository-url> libs/myserviceAPI
Git hat nun die API in das submodul gepackt und wir können das oben erstellte temporäre Verzeichnis wieder löschen.
+Anschließend müssen wir stack noch erklären, dass wir die API da nun liegen haben und passen wieder die stack.yaml
an, indem wir das Verzeichnis unter packages hinzufügen.
packages:
+ - .
+ - libs/myserviceAPI # <<
Nun können wir in der package.yaml
(oder myservice.cabal
, falls kein hpack
verwendet wird) unter den dependencies unsere API hinzufügen (name wie die cabal-Datei in libs/myserviceAPI
).
Funktioniert komplett analog zu dem vorgehen oben (ohne das generieren natürlich :grin:). stack.yaml
editieren und zu den packages hinzufügen:
packages:
+ - .
+ - libs/myserviceAPI
+ - libs/myCoolMLServiceAPI
in der package.yaml
(oder der cabal) die dependencies hinzufügen und schon haben wir die Features zur Verfügung und können gegen diese Services reden.
In git ist das entfernen von Submodules etwas frickelig, daher hier ein copy&paste der GitHub-Antwort:
+## Remove the submodule entry from .git/config
+git submodule deinit -f path/to/submodule
+
+## Remove the submodule directory from the superproject's .git/modules directory
+rm-rf .git/modules/path/to/submodule
+
+## Remove the entry in .gitmodules and remove the submodule directory located at path/to/submodule
+git rm-f path/to/submodule
Falls das nicht klappt, gibt es alternative Vorschläge unter dem Link oben.
+Keine Panik. Ein stack haddock --open
hilft da. Das generiert die Dokumentation für alle in der package.yaml
(oder cabal-file) eingetragenen dependencies inkl. aller upstream-dependencies. Man bekommt also eine komplette lokale Dokumentation von allem. Geöffnet wird dann die Paket-Startseite inkl. der direkten dependencies:
Es gibt 2 wichtige Pfade im Browser:
+...../all/index.html
- hier sind alle Pakete aufgeführt...../index.html
- hier sind nur die direkten dependencies aufgeführt.Wenn man einen lokalen Webserver startet kann man mittels “s” auch die interaktive Suche öffnen (Suche nach Typen, Funktionen, Signaturen, etc.). In Bash mit python3
geht das z.b. einfach über:
cd $(stack path --local-doc-root)
+python3 -m SimpleHTTPServer 8000
+firefox "http://localhost:8000"
Generelles Vorgehen:
+in app/Main.hs
: Hier ist quasi immer nur eine Zeile drin: main = myServiceMain
Grund: Applications tauchen nicht im Haddock auf. Also haben wir ein “src”-Modul, welches hier nur geladen & ausgeführt wird.
in src/MyService.hs
: myServiceMain :: IO ()
definieren
Für die Main kann man prinzipiell eine Main andere Services copy/pasten. Im folgenden eine Annotierte main-Funktion - zu den einzelnen Voraussetzungen kommen wir im Anschluss.
+++{{< dend >}} +{-# OPTIONS_GHC -Wno-name-shadowing #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +module MyService where + +-- generische imports aus den dependencies/base, nicht in der prelude +import Codec.MIME.Type +import Configuration.Dotenv as Dotenv +import Control.Concurrent (forkIO, threadDelay) +import Control.Concurrent.Async +import Control.Concurrent.STM +import Control.Monad +import Control.Monad.Catch +import Control.Monad.Except +import Conversion +import Conversion.Text () +import Data.Binary.Builder +import Data.String (IsString (..)) +import Data.Time +import Data.Time.Clock +import Data.Time.Format +import Data.Default +import Network.HostName +import Network.HTTP.Client as HTTP hiding + (withConnection) +import Network.HTTP.Types (Status, statusCode) +import Network.Mom.Stompl.Client.Queue +import Network.Wai (Middleware) +import Network.Wai.Logger +import Network.Wai.Middleware.Cors +import Network.Wai.Middleware.RequestLogger (OutputFormat (..), + logStdout, + mkRequestLogger, + outputFormat) +import Servant.Client (mkClientEnv, + parseBaseUrl) +import System.Directory +import System.Envy +import System.IO +import System.Log.FastLogger +import Text.PrettyPrint.GenericPretty + +-- generische imports, aber qualified, weil es sonst zu name-clashes kommt + +import qualified Data.ByteString as BS +-- import qualified Data.ByteString.Char8 as BS8 +import qualified Data.ByteString.Lazy as LBS +import qualified Network.HTTP.Client.TLS as UseDefaultHTTPSSettings (tlsManagerSettings) +import qualified Network.Mom.Stompl.Client.Queue as AMQ +import qualified Network.Wai as WAI + +-- Handler für den MyServiceBackend-Typen und Imports aus den Libraries +import MyService.Handler as H -- handler der H.myApiEndpointV1Post implementiert +import MyService.Types -- weitere Type (s. nächste box) +import MyServiceGen.API as MS -- aus der generierten library + + +myServicemain :: IO () +myServicemain = do + -- .env-Datei ins Prozess-Environment laden, falls noch nicht von außen gesetzt + void $ loadFile $ Dotenv.Config [".env"] [] False + -- Config holen (defaults + overrides aus dem Environment) + sc@ServerConfig{..} <- decodeWithDefaults defConfig + -- Backend-Setup + -- legt sowas wie Proxy-Server fest und wo man wie dran kommt. Benötigt für das Sprechen mit anderen Microservices + let defaultHTTPSSettings = UseDefaultHTTPSSettings.tlsManagerSettings { managerResponseTimeout = responseTimeoutMicro $ 1000 * 1000 * myserviceMaxTimeout } + createBackend url proxy = do + manager <- newManager . managerSetProxy proxy + $ defaultHTTPSSettings + url' <- parseBaseUrl url + return (mkClientEnv manager url') + internalProxy = case myserviceInternalProxyUrl of + "" -> noProxy + url -> useProxy $ HTTP.Proxy (fromString url) myserviceInternalProxyPort + -- externalProxy = case myserviceExternalProxyUrl of + -- "" -> noProxy + -- url -> useProxy $ HTTP.Proxy (fromString url) myserviceExternalProxyPort + + -- Definieren & Erzeugen der Funktionen um die anderen Services anzusprechen. + calls <- (,) + <$> createBackend myserviceAUri internalProxy + <*> createBackend myserviceBUri internalProxy + + -- Logging-Setup + hSetBuffering stdout LineBuffering + hSetBuffering stderr LineBuffering + + + -- Infos holen, brauchen wir später + myName <- getHostName + today <- formatTime defaultTimeLocale "%F" . utctDay <$> getCurrentTime + + + -- activeMQ-Transaktional-Queue zum schreiben nachher vorbereiten + amqPost <- newTQueueIO + + + -- bracket a b c == erst a machen, ergebnis an c als variablen übergeben. Schmeisst c ne exception/wird gekillt/..., werden die variablen an b übergeben. + bracket + -- logfiles öffnen + (LogFiles <$> openFile ("/logs/myservice-"<>myName<>"-"<>today<>".info") AppendMode + <*> openFile (if myserviceDebug then "/logs/myservice-"<>myName<>"-"<>today<>".debug" else "/dev/null") AppendMode + <*> openFile ("/logs/myservice-"<>myName<>"-"<>today<>".error") AppendMode + <*> openFile ("/logs/myservice-"<>myName<>"-"<>today<>".timings") AppendMode + ) + -- und bei exception/beendigung schlißen.h + (\(LogFiles a b c d) -> mapM_ hClose [a,b,c,d]) + $ \logfiles -> do + + + -- logschreibe-funktionen aliasen; log ist hier abstrakt, iolog spezialisiert auf io. + let log = printLogFiles logfiles :: MonadIO m => [LogItem] -> m () + iolog = printLogFilesIO logfiles :: [LogItem] -> IO () + + + -- H.myApiEndpointV1Post ist ein Handler (alle Handler werden mit alias H importiert) und in einer eigenen Datei + -- Per Default bekommen Handler sowas wie die server-config, die Funktionen um mit anderen Services zu reden, die AMQ-Queue um ins Kibana zu loggen und eine Datei-Logging-Funktion + -- Man kann aber noch viel mehr machen - z.b. gecachte Daten übergeben, eine Talk-Instanz, etc. pp. + server = MyServiceBackend{ myApiEndpointV1Post = H.myApiEndpointV1Post sc calls amqPost log + } + config = MS.Config $ "http://" ++ myserviceHost ++ ":" ++ show myservicePort ++ "/" + iolog . pure . Info $ "Using Server configuration:" + iolog . pure . Info $ pretty sc { myserviceActivemqPassword = "******" -- Do NOT log the password ;) + , myserviceMongoPassword = "******" + } + -- alle Services starten (Hintergrund-Aktionen wie z.b. einen MongoDB-Dumper, einen Talk-Server oder wie hier die ActiveMQ + void $ forkIO $ keepActiveMQConnected sc iolog amqPost + -- logging-Framework erzeugen + loggingMW <- loggingMiddleware + -- server starten + if myserviceDebug + then runMyServiceMiddlewareServer config (cors (\_ -> Just (simpleCorsResourcePolicy {corsRequestHeaders = ["Content-Type"]})) . loggingMW . logStdout) server + else runMyServiceMiddlewareServer config (cors (\_ -> Just (simpleCorsResourcePolicy {corsRequestHeaders = ["Content-Type"]}))) server + + +-- Sollte bald in die Library hs-stomp ausgelagert werden +-- ist ein Beispiel für einen ActiveMQ-Dumper +keepActiveMQConnected :: ServerConfig -> ([LogItem] -> IO ()) -> TQueue BS.ByteString -> IO () +keepActiveMQConnected sc@ServerConfig{..} printLog var = do + res <- handle (\(e :: SomeException) -> do + printLog . pure . Error $ "Exception in AMQ-Thread: "<>show e + return $ Right () + ) $ AMQ.try $ do -- catches all AMQ-Exception that we can handle. All others bubble up. + printLog . pure . Info $ "AMQ: connecting..." + withConnection myserviceActivemqHost myserviceActivemqPort [ OAuth myserviceActivemqUsername myserviceActivemqPassword + , OTmo (30*1000) {- 30 sec timeout -} + ] + [] $ \c -> do + let oconv = return + printLog . pure . Info $ "AMQ: connected" + withWriter c "Chaos-Logger for Kibana" "chaos.logs" [] [] oconv $ \writer -> do + printLog . pure . Info $ "AMQ: queue created" + let postfun = writeQ writer (Type (Application "json") []) [] + void $ race + (forever $ atomically (readTQueue var) >>= postfun) + (threadDelay (600*1000*1000)) -- wait 10 Minutes + -- close writer + -- close connection + -- get outside of all try/handle/...-constructions befor recursing. + case res of + Left ex -> do + printLog . pure . Error $ "AMQ: "<>show ex + keepActiveMQConnected sc printLog var + Right _ -> keepActiveMQConnected sc printLog var + + +-- Beispiel für eine Custom-Logging-Middleware. +-- Hier werden z.B. alle 4xx-Status-Codes inkl. Payload ins stdout-Log geschrieben. +-- Nützlich, wenn die Kollegen ihre Requests nicht ordentlich schreiben können und der Server das Format zurecht mit einem BadRequest ablehnt ;) +loggingMiddleware :: IO Middleware +loggingMiddleware = liftIO $ mkRequestLogger $ def { outputFormat = CustomOutputFormatWithDetails out } + where + out :: ZonedDate -> WAI.Request -> Status -> Maybe Integer -> NominalDiffTime -> [BS.ByteString] -> Builder -> LogStr + out _ r status _ _ payload _ + | statusCode status < 300 = "" + | statusCode status > 399 && statusCode status < 500 = "Error code "<>toLogStr (statusCode status) <>" sent. Request-Payload was: "<> mconcat (toLogStr <$> payload) <> "\n" + | otherwise = toLogStr (show r) <> "\n"
In der Myservice.Types
werden ein paar hilfreiche Typen und Typ-Instanzen definiert. Im Folgenden geht es dabei um Dinge für:
Envy
+$ENV_VAR
in DatentypenServerConfig
+ExtraTypes
+Out
/BSON
-Instanzen
+aeson
), daher werden hier die fehlenden definiert.BSON
: Kommunikation mit MongoDB
Out
: pretty-printing im Log
+Out
statt über Generics wie z.b. pretty-generic
oder die automatische Show-Instanz via prerryShow
macht.++{{< dend >}} +{-# OPTIONS_GHC -Wno-orphans #-} +{-# OPTIONS_GHC -Wno-name-shadowing #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE RecordWildCards #-} +module MyService.Types where + +import Data.Aeson (FromJSON, ToJSON) +import Data.Text +import Data.Time.Clock +import GHC.Generics +import System.Envy +import Text.PrettyPrint (text) +import Text.PrettyPrint.GenericPretty + +-- Out hat hierfür keine Instanzen, daher kurz eine einfach Definition. +instance Out Text where + doc = text . unpack + docPrec i a = text $ showsPrec i a "" + +instance Out UTCTime where + doc = text . show + docPrec i a = text $ showsPrec i a "" + +-- Der ServerConfig-Typ. Wird mit den defaults unten initialisiert, dann mit den Variablen aus der .env-Datei überschrieben und zum Schluss können Serveradmins diese via $MYSERVICE_FOO nochmal überschreiben. +data ServerConfig = ServerConfig + { myserviceHost :: String -- ^ Environment: $MYSERVICE_HOST + , myservicePort :: Int -- ^ Environment: $MYSERVICE_PORT + , myserviceMaxTimeout :: Int -- ^ Environment: $MYSERVICE_MAX_TIMEOUT + , myserviceInternalProxyUrl :: String -- ^ Environment: $MYSERVICE_INTERNAL_PROXY_URL + , myserviceInternalProxyPort :: Int -- ^ Environment: $MYSERVICE_INTERNAL_PROXY_PORT + , myserviceExternalProxyUrl :: String -- ^ Environment: $MYSERVICE_EXTERNAL_PROXY_URL + , myserviceExternalProxyPort :: Int -- ^ Environment: $MYSERVICE_EXTERNAL_PROXY_PORT + , myserviceActivemqHost :: String -- ^ Environment: $MYSERVICE_ACTIVEMQ_HOST + , myserviceActivemqPort :: Int -- ^ Environment: $MYSERVICE_ACTIVEMQ_PORT + , myserviceActivemqUsername :: String -- ^ Environment: $MYSERVICE_ACTIVEMQ_USERNAME + , myserviceActivemqPassword :: String -- ^ Environment: $MYSERVICE_ACTIVEMQ_PASSWORD + , myserviceMongoUsername :: String -- ^ Environment: $MYSERVICE_MONGO_USERNAME + , myserviceMongoPassword :: String -- ^ Environment: $MYSERVICE_MONGO_PASSWORD + , myserviceDebug :: Bool -- ^ Environment: $MYSERVICE_DEBUG + } deriving (Show, Eq, Generic) + +-- Default-Konfigurations-Instanz für diesen Service. +instance DefConfig ServerConfig where + defConfig = ServerConfig "0.0.0.0" 8080 20 + "" + "" + "" + 0 + "" + 0 + "" + 0 + "" + "" + "" + "" + False + +-- Kann auch aus dem ENV gefüllt werden +instance FromEnv ServerConfig +-- Und hübsch ausgegeben werden. +instance Out ServerConfig + + +instance Out Response +instance FromBSON Repsonse -- FromBSON-Instanz geht immer davon aus, dass alle keys da sind (ggf. mit null bei Nothing).
Den Service implementieren. Einfach ein neues Modul aufmachen (z.B. MyService.Handler
oder MyService.DieserEndpunktbereich
/MyService.JenerEndpunktbereich
) und dort die Funktion implementieren, die man in der Main.hs
benutzt hat. In dem Handler habt ihr dann keinen Stress mehr mit Validierung, networking, logging, etc. pp. weil alles in der Main abgehandelt wurde und ihr nur noch den “Happy-Case” implementieren müsst. Beispiel für unseren Handler oben:
myApiEndpointV1Post :: MonadIO m => ServerConfig -> (ClientEnv,ClientEnv) -> TQueue BS.ByteString -> ([LogItem] -> IO ()) -> Request -> m Response
+myApiEndpointV1Post sc calls amqPost log req = do
+ liftIO . log $ [Info $ "recieved "<>pretty req] -- input-logging
+ liftIO . atomically . writeTQueue . LBS.toStrict $ "{\"hey Kibana, i recieved:\"" <> A.encode (pretty req) <> "}" -- log in activeMQ/Kibana
+
+
+ --- .... gaaaanz viel komplizierter code um die Response zu erhalten ;)
+ let ret = Response 1337 Nothing -- dummy-response ;)
+ -- gegeben wir haben eine gültige mongodb-pipe;
+ -- mehr logik will ich in die Beispiele nicht packen.
+ -- Man kann die z.b. als weiteren Wert in einer TMVar (damit man sie ändern & updaten kann) an die Funktion übergeben.
+ liftIO . access pipe master "DatabaseName" $ do
+ ifM (auth (myServiceMongoUsername sc) (myServiceMongoPassword sc)) (return ()) (liftIO . printLog . pure . Error $ "MongoDB: Login failed.")
+ save "DatabaseCollection" ["_id" =: 1337, "entry" =: ret] -- selbe id wie oben ;)
+ return ret
Diese dummy-Antwort führt auf, wie gut man die ganzen Sachen mischen kann.
+stdout
- je nach KonfigurationHierzu erstellt man ein Verzeichnis static/
(Konvention; ist im generator so generiert, dass das ausgeliefert wird). Packt man hier z.b. eine index.html
rein, erscheint die, wenn man den Service ansurft.
Der Editor, der ganz am Anfang zum Einsatz gekommen ist, braucht nur die api-doc.yml
um diese Ansicht zu erzeugen. Daher empfiehlt sich hier ein angepasster Fork davon indem die Pfade in der index.html korrigiert sind. Am einfachsten (und von den meisten services so benutzt): In meiner Implementation liegt dann nach dem starten auf http://localhost:PORT/ui/ und kann direkt dort getestet werden.
stack build --file-watch --ghc-options '-freverse-errors -W -Wall -Wcompat' --interleaved-output
Was tut das?
+--file-watch
: automatisches (minimales) kompilieren bei dateiänderungen--ghc-options
+-freverse-errors
: Fehlermeldungen in umgekehrter Reihenfolge (Erster Fehler ganz unten; wenig scrollen )-W
: Warnungen an-Wall
: Alle sinnvollen Warnungen an (im gegensatz zu -Weverything
, was WIRKLICH alles ist )-Wcompat
: Warnungen für Sachen, die in der nächsten Compilerversion kaputt brechen werden & vermieden werden sollten--interleaved-output
: stack-log direkt ausgeben & nicht in Dateien schreiben und die dann am ende zusammen cat'en.Um pro Datei Warnungen auszuschalten (z.B. weil man ganz sicher weiss, was man tut -.-): {-# OPTIONS_GHC -Wno-whatsoever #-}
als pragma in die Datei.
Idealerweise sollte das Projekt keine Warnungen erzeugen.
+Als Beispiel sei hier ein einfaches Docker-Build mit Jenkins-CI gezeigt, weil ich das aus Gründen rumliegen hatte. Kann man analog in fast alle anderen CI übersetzen.
+Die angehängten Scripte gehen von einer Standard-Einrichtung aus (statische Sachen in static, 2-3 händische Anpassungen auf das eigene Projekt nach auspacken). Nachher liegt dann auch unter static/version die gebaute Versionsnummer & kann abgerufen werden. In der Dockerfile.release
und der Jenkinsfile
müssen noch Anpassungen gemacht werden. Konkret:
Dockerfile.release
: alle <<<HIER>>>
-Stellen sinnvoll befüllenJenkinsfile
die defs für “servicename” und “servicebinary” ausfüllen. Binary ist das, was bei stack exec aufgerufen wird; name ist der Image-Name für das docker-repository.Änderungen die dann noch gemacht werden müssen:
+Wenn das fertig gebaut ist, liegt im test/live-repository ein docker-image namens servicename:version
.
So even despite my exceptional1 successes that can be seen in my work i always struggled with issues even to the point of total breakdown. Of course i am also guilty of painting a rosy picture of me - just look at a summary of my experiences or the awesome things i did at university. If you only read that it is hard to believe that i basically had to delay my studies from 2007 to 2010 because i wasn’t even really able to leave the house.
+1 citation needed
Only thanks to the not-that-awful system in Germany and massive financial help from my parents i was even able to pursue this way.
+Well.. after 15 long years of therapy i finally get a hang of all my issues. Many of those are related, some are just the result of coping mechanisms of ignoring other issues.
+Currently i am successfully diagnosed with
+and i got a big suspician of
+All in all: when i feel well, am good rested and have nothing critical coming up i am more of what i would call a “high functioning Autist, but not THAT far on the spectrum”. But it is funny that while finding out who i really am, i met more people who basically had the same issue and a similar biography like mine. Some of them get the autism-diagnosis first, others the ADHD one - since until some time ago those diagnosis were mutually exclusive.
+That’s also why many people like me are only really diagnosed as adults, because autism hides many effects of ADHD and vice-versa - depending on which one is currently dominating. It is basically 2 modes: do everything all at once and start everything that grabs your attention - or do a deep dive into a single thing. And the exact opposite: The ADHD part being bored by the autism-project, the autism-part is completely overwhelmed by the ADHD chaos. Both then leading to exhaustion, not being able to do anything .. and basically feeling guilty for the things you did not manage to finish.
+Today i finally found myself. I currently have a great partner (with 3 kids) - and all of them have similar issues. Like i said: I best get along with similar people - and also fall in love with those.. and as AuDHD runs in the genes all offspring has a good chance of catching it to varies degrees, too.
+I think the most important thing was the ADHD-Diagnosis - as this enabled me to get metylphenidate to basically get into a “3-4 hours focused as long as the pill works” and total chaos afterwards. This enables me to have productive days/times where i can do all the boring-work that my ADHD-Part wants to sit out and the autism part is overwhelmed from even starting.
+To be continued …
+ + +Insgesamt haben wir die Vorlesung 3x gehalten, wobei von der ersten zur zweiten Iteration der Inhalt massiv überarbeitet wurde und bei der Iteration von der zweiten zur dritten Vorlesung die Übungen komplett neu erstellt wurden.
+Die gesamten Übungen sind unter anderem in der FFPiH-Organisation in meinem gitea hinterlegt: https://gitea.dresselhaus.cloud/FFPiH
+Einige der aktualisierten Übungen sind privat geschaltet, da diese iterativ aufeinander aufbauen und jeweils die Musterlösung der vorherigen enthalten.
+Vorausgesetzt wurde, dass die Studierenden das erste Semester abgeschlossen hatten und somit bereits leichte Grundlagen in Haskell kannten (aber z.b. Dinge wie Functor/Applicative/Monad noch nicht wirklich erklärt bekommen haben).
+Stück für Stück werden die Studis dann zunächst in abstrakte Konstrukte eingeführt, aber diese werden dann schnell in die Praxis umgesetzt. Etwa mit dem Schreiben eines eigenen Parsers.
+Schlussendlich gibt es dann einen “Rundumschlag” durch die gesamte Informatik. Erstellung eines Spieles (auf basis einer kleinen Grundlage), erstellung von WebApps mit Yesod, Parallelisierung und Nebenläufigkeit für rechenintensive Anwendungen inkl. synchronisation mittels STM.
+Optional gab es weitere Übungen zu dingen wie “verteiltes Rechnen”.
+Ziel hierbei war nicht, diese ganzen Themen in der Tiefe beizubringen, sondern aufzuzeigen, wie sie sehr schnell abstrakte Konstrukte, die ihnen ggf. 3 Semester später erst begegnen bugfrei benutzen können, da Haskell hier in sehr vielen Fällen einfach nur die “richtige” Lösung kompilieren lässt und alle gängigen Fallen schlicht ausschließt. Beispiel ist z.b. STM innerhalb von STM, Mischen von DB-Monade, Handler-Monade und Template-Engine in Yesod, Process () statt IO () in der Nutzung von CloudHaskell, etc. pp.
+Sehr gutes Feedback von den Studenten bekamen wir insbesondere für Übungen wie:
+Übung 2, Aufgabe 2, weil hier durch “einfaches” umformen hin zu Abstraktionen und mit den Regeln dieser im ersten Fall die Laufzeit (vor Compileroptimierungen) von O(n²) auf O(0) ändert.
+Übung 4, welche ein komplett fertigen (sehr rudimentären und simplen) Dungeon-Crawler bereitstellt, der “nur” 1-2 bugs hat und “wie ein echtes Projekt” erweitert werden muss. Diese Übung hat sich dann über 4 weitere Übungen gestreckt, wobei folgende Aufgaben gelöst werden müssen:
+StateT World
auf RWST GameConfig Log World
und somit nutzen von individuellen Konfigurationen für z.b. KeybindingsDie Idee dahinter ist, dass man Zugriffsabstraktionen über Daten verknüpfen
+kann. Also einfachen Datenstruktur kann man einen Record mit der entsprechenden
+Syntax nehmen.
data Person = P { name :: String
+ , addr :: Address
+ , salary :: Int }
+data Address = A { road :: String
+ , city :: String
+ , postcode :: String }
+-- autogeneriert unten anderem: addr :: Person -> Address
+
+ setName :: String -> Person -> Person
+ setName n p = p { name = n } --record update notation
+
+ setPostcode :: String -> Person -> Person
+ setPostcode pc p
+ = p { addr = addr p { postcode = pc } }
+ -- update of a record inside a record
Problem mit diesem Code:
+data Person = P { name :: String
+ , addr :: Address
+ , salary :: Int }
+-- a lens for each field
+lname :: Lens' Person String
+laddr :: Lens' Person Adress
+lsalary :: Lens' Person Int
+-- getter/setter for them
+view :: Lens' s a -> s -> a
+set :: Lens' s a -> a -> s -> s
+-- lens-composition
+composeL :: Lens' s1 s2 -> Lens s2 a -> Lens' s1 a
Mit diesen Dingen (wenn wir sie hätten) könnte man dann
+data Person = P { name :: String
+ , addr :: Address
+ , salary :: Int }
+data Address = A { road :: String
+ , city :: String
+ , postcode :: String }
+setPostcode :: String -> Person -> Person
+setPostcode pc p
+ = set (laddr `composeL` lpostcode) pc p
machen und wäre fertig.
+data LensR s a = L { viewR :: s -> a
+ , setR :: a -> s -> s }
+
+composeL (L v1 u1) (L v2 u2)
+ = L (\s -> v2 (v1 s))
+ (\a s -> u1 (u2 a (v1 s)) s)
Auslesen traversiert die Datenstruktur, dann wird die Function angewendet und
+zum setzen wird die Datenstruktur erneut traversiert:
over :: LensR s a -> (a -> a) -> s -> s
+over ln f s = setR l (f (viewR l s)) s
data LensR s a
+ = L { viewR :: s -> a
+ , setR :: a -> s -> s
+ , mod :: (a->a) -> s -> s
+ , modM :: (a->Maybe a) -> s -> Maybe s
+ , modIO :: (a->IO a) -> s -> IO s }
Neues Problem: Für jeden Spezialfall muss die Lens erweitert werden.
+Man kann alle Monaden abstrahieren. Functor reicht schon:
+data LensR s a
+ = L { viewR :: s -> a
+ , setR :: a -> s -> s
+ , mod :: (a->a) -> s -> s
+ , modF :: Functor f => (a->f a) -> s -> f s }
Idee: Die 3 darüberliegenden durch modF ausdrücken.
+Wenn man das berücksichtigt, dann hat einen Lens folgenden Typ:
+type Lens' s a = forall f. Functor f
+ => (a -> f a) -> s -> f s
Allerdings haben wir dann noch unseren getter/setter:
+data LensR s a = L { viewR :: s -> a
+ , setR :: a -> s -> s }
Stellt sich raus: Die sind isomorph! Auch wenn die von den Typen her komplett
+anders aussehen.
set :: Lens' s a -> (a -> s -> s)
+set ln a s = --...umm...
+--:t ln => (a -> f a) -> s -> f s
+-- => get s out of f s to return it
Wir können für f einfach die “Identity”-Monade nehmen, die wir nachher wegcasten
+können.
newtype Identity a = Identity a
+-- Id :: a -> Identity a
+
+runIdentity :: Identity s -> s
+runIdentity (Identity x) = x
+
+instance Functor Identity where
+ fmap f (Identity x) = Identity (f x)
somit ist set einfach nur
+set :: Lens' s a -> (a -> s -> s)
+set ln x s
+ = runIdentity (ls set_fld s)
+ where
+ set_fld :: a -> Identity a
+ set_fld _ = Identity x
+ -- a was the OLD value.
+ -- We throw that away and set the new value
oder kürzer (für nerds wie den Author der Lens-Lib)
+set :: Lens' s a -> (a -> s -> s)
+set ln x = runIdentity . ln (Identity . const x)
Dasselbe wie Set, nur dass wir den Parameter nicht entsorgen, sondern in die
+mitgelieferte Function stopfen.
over :: Lens' s a -> (a -> a) -> s -> s
+over ln f = runIdentity . ln (Identity . f)
view :: Lens' s a -> (s -> a)
+view ln s = --...umm...
+--:t ln => (a -> f a) -> s -> f s
+-- => get a out of the (f s) return-value
+-- Wait, WHAT?
Auch hier gibt es einen netten Funktor. Wir packen das “a” einfach in das “f”
+und werfen das “s” am End weg.
newtype Const v a = Const v
+
+getConst :: Const v a -> v
+getConst (Const x) = x
+
+instance Functor (Const v) where
+ fmap f (Const x) = Const x
+ -- throw f away. Nothing changes our const!
somit ergibt sich
+view :: Lens' s a -> (s -> a)
+view ln s
+ = getConst (ln Const s)
+ -- Const :: s -> Const a s
oder nerdig
+view :: Lens' s a -> (s -> a)
+view ln = getConst . ln Const
Nochmal kurz der Typ:
+type Lens' s a = forall f. Functor f
+ => (a -> f a) -> s -> f s
Für unser Personen-Beispiel vom Anfang:
+data Person = P { _name :: String, _salary :: Int }
+
+name :: Lens' Person String
+-- name :: Functor f => (String -> f String)
+-- -> Person -> f Person
+
+name elt_fn (P n s)
+ = fmap (\n' -> P n' s) (elt_fn n)
+-- fmap :: Functor f => (a->b) -> f a -> f b - der Funktor, der alles verknüpft
+-- \n' -> .. :: String -> Person - Funktion um das Element zu lokalisieren (WO wird ersetzt/gelesen/...)
+-- elt_fn n :: f String - Funktion um das Element zu verändern (setzen, ändern, ...)
Die Lambda-Funktion ersetzt einfach den Namen. Häufig sieht man auch
+name elt_fn (P n s)
+ = (\n' -> P n' s) <$> (elt_fn n)
+-- | Focus | |Function|
view name (P {_name="Fred", _salary=100})
+ -- inline view-function
+= getConst (name Const (P {_name="Fred", _salary=100})
+ -- inline name
+= getConst (fmap (\n' -> P n' 100) (Const "Fred"))
+ -- fmap f (Const x) = Const x - Definition von Const
+= getConst (Const "Fred")
+ -- getConst (Const x) = x
+= "Fred"
Dieser Aufruf hat KEINE Runtime-Kosten, weil der Compiler direkt die Address des
+Feldes einsetzen kann. Der gesamte Boilerplate-Code wird vom Compiler
+wegoptimiert.
Dies gilt für jeden Funktor mit newtype, da das nur ein Typalias ist.
+Wie sehen denn die Typen aus?
+Wir wollen ein
+++Lens’ s1 s2 -> Lens’ s2 a -> Lens’ s1 a
+
Wir haben 2 Lenses
+++ln1 :: (s2 -> f s2) -> (s1 -> f s1)
+
+ln2 :: (a -> f a) -> (s2 -> f s2)
wenn man scharf hinsieht, kann man die verbinden
+++ln1 . ln2 :: (a -> f s) -> (s1 -> f s1)
+
und erhält eine Lens. Sogar die Gewünschte!
+Somit ist Lens-Composition einfach nur Function-Composition (.).
Der Code um die Lenses zu bauen ist für records immer Identisch:
+data Person = P { _name :: String, _salary :: Int }
+
+name :: Lens' Person String
+name elt_fn (P n s) = (\n' -> P n' s) <$> (elt_fn n)
Daher kann man einfach
+import Control.Lens.TH
+data Person = P { _name :: String, _salary :: Int }
+
+$(makeLenses ''Person)
nehmen, was einem eine Lens für “name” und eine Lens für “salary” generiert.
+Mit anderen Templates kann man auch weitere Dinge steuern (etwa wofür Lenses
+generiert werden, welches Prefix (statt _) man haben will etc. pp.).
Will man das aber haben, muss man selbst in den Control.Lens.TH-Code schauen.
+import Control.Lens.TH
+
+data Person = P { _name :: String
+ , _addr :: Address
+ , _salary :: Int }
+data Address = A { _road :: String
+ , _city :: String
+ , _postcode :: String }
+
+$(makeLenses ''Person)
+$(makeLenses ''Address)
+
+setPostcode :: String -> Person -> Person
+setPostcode pc p = set (addr . postcode) pc p
-- ...
+
+setPostcode :: String -> Person -> Person
+setPostcode pc p = addr . postcode .~ pc $ p
+-- | Focus |set|to what|in where
+
+getPostcode :: Person -> String
+getPostcode p = p ^. $ addr . postcode
+-- |from|get| Focus |
Es gibt drölf-zillionen weitere Infix-Operatoren (für Folds,
+Listenkonvertierungen, -traversierungen, …)
Man kann mit Lenses sogar Felder emulieren, die gar nicht da sind. Angenommen
+folgender Code:
data Temp = T { _fahrenheit :: Float }
+
+$(makeLenses ''Temp)
+-- liefert Lens: fahrenheit :: Lens Temp Float
+
+centigrade :: Lens Temp Float
+centigrade centi_fn (T faren)
+ = (\centi' -> T (cToF centi'))
+ <$> (centi_fn (fToC faren))
+-- cToF & fToC as Converter-Functions defined someplace else
Hiermit kann man dann auch Funktionen, die auf Grad-Celsius rechnen auf Daten
+anwenden, die eigenlich nur Fahrenheit speichern, aber eine Umrechnung
+bereitstellen. Analog kann man auch einen Zeit-Datentypen definieren, der
+intern mit Sekunden rechnet (und somit garantiert frei von Fehlern wie -3
+Minuten oder 37 Stunden ist)
Das ganze kann man auch parametrisieren und auf Non-Record-Strukturen anwenden.
+Beispielhaft an einer Map verdeutlicht:
-- from Data.Lens.At
+at :: Ord k => k -> Lens' (Map k v) (Maybe v)
+
+-- oder identisch, wenn man die Lens' auflöst:
+at :: Ord k, forall f. Functor f => k -> (Maybe v -> f Maybe v) -> Map k v -> f Map k v
+
+at k mb_fn m
+ = wrap <$> (mb_fn mv)
+ where
+ mv = Map.lookup k m
+
+ wrap :: Maybe v -> Map k v
+ wrap (Just v') = Map.insert k v' m
+ wrap Nothing = case mv of
+ Nothing -> m
+ Just _ -> Map.delete k m
+
+-- mb_fn :: Maybe v -> f Maybe v
Bitfields auf Strukturen die Bits haben (Ints, …) in Data.Bits.Lens
Web-scraper in Package hexpat-lens
+p ^.. _HTML' . to allNodes
+ . traverse . named "a"
+ . traverse . ix "href"
+ . filtered isLocal
+ . to trimSpaces
Zieht alle externen Links aus dem gegebenen HTML-Code in p um weitere ziele
+fürs crawlen zu finden.
Bisher hatten wir Lenses nur auf Funktoren F. Die nächstmächtigere Klasse ist
+Applicative.
type Traversal' s a = forall f. Applicative f
+ => (a -> f a) -> (s -> f s)
Da wir den Container identisch lassen (weder s noch a wurde angefasst) muss sich
+etwas anderes ändern. Statt eines einzelnen Focus erhalten wir viele Foci.
Was ist ein Applicative überhaupt? Eine schwächere Monade (nur 1x Anwendung und
+kein Bind - dafür kann man die beliebig oft hintereinanderhängen).
class Functor f => Applicative f where
+ pure :: a -> f a
+ (<*>) :: f (a -> b) -> f a -> f b
+
+-- Monade als Applicative:
+pure = return
+mf <*> mx = do { f <- mf; x <- mx; return (f x) }
Recap: Was macht eine Lens:
+data Adress = A { _road :: String
+ , _city :: String
+ , _postcode :: String }
+
+road :: Lens' Adress String
+road elt_fn (A r c p) = (\r' -> A r' c p) <$> (elt_fn r)
+-- | "Hole" | | Thing to put in|
Wenn man nun road & city gleichzeitig bearbeiten will:
+addr_strs :: Traversal' Address String
+addr_strs elt_fn (A r c p)
+ = ... (\r' c' -> A r' c' p) .. (elt_fn r) .. (elt_fn c) ..
+-- | function with 2 "Holes"| first Thing | second Thing
fmap kann nur 1 Loch stopfen, aber nicht mit n Löchern umgehen. Applicative mit
+<*> kann das.
+Somit gibt sich
addr_strs :: Traversal' Address String
+addr_strs elt_fn (A r c p)
+ = pure (\r' c' -> A r' c' p) <*> (elt_fn r) <*> (elt_fn c)
+-- lift in Appl. | function with 2 "Holes"| first Thing | second Thing
+-- oder kürzer
+addr_strs :: Traversal' Address String
+addr_strs elt_fn (A r c p)
+ = (\r' c' -> A r' c' p) <$> (elt_fn r) <*> (elt_fn c)
+-- pure x <*> y == x <$> y
Wie würd eine modify-funktion aussehen?
+over :: Lens' s a -> (a -> a) -> s -> s
+over ln f = runIdentity . ln (Identity . f)
+
+over :: Traversal' s a -> (a -> a) -> s -> s
+over ln f = runIdentity . ln (Identity . f)
Der Code ist derselbe - nur der Typ ist generischer. Auch die anderen Dinge
+funktioniert diese Erweiterung (für Identity und Const muss man noch ein paar
+dummy-Instanzen schreiben um sie von Functor auf Applicative oder Monad zu heben
Man kann mit Foci sehr selektiv vorgehen. Auch kann man diese durch Funktionen
+steuern. Beispisweise eine Function anwenden auf
Traversals und Lenses kann man trivial kombinieren (lens . lens
=> lens
,
+lens . traversal
=> traversal
etc.)
In diesem Artikel wurde nur auf Monomorphic Lenses eingegangen. In der richtigen
+Library ist eine Lens
type Lens' s a = Lens s s a a
+type Lens s t a b = forall f. Functor f => (a -> f b) -> (s -> f t)
sodass sich auch die Typen ändern können um z.B. automatisch einen Konvertierten
+(sicheren) Typen aus einer unsicheren Datenstruktur zu geben.
Die modify-Funktion over ist auch
+> over :: Profunctor p => Setting p s t a b -> p a b -> s -> t
++Edward is deeply in thrall to abstractionitis - Simon Peyton Jones
+
Lens alleine definiert 39 newtypes, 34 data-types und 194 Typsynonyme…
+Ausschnitt
-- traverseOf :: Functor f => Iso s t a b -> (a -> f b) -> s -> f t
+-- traverseOf :: Functor f => Lens s t a b -> (a -> f b) -> s -> f t
+-- traverseOf :: Applicative f => Traversal s t a b -> (a -> f b) -> s -> f t
+
+traverseOf :: Over p f s t a b -> p a (f b) -> s -> f t
dafuq?
+ + +module Main where
+
+import System.Environment (getArgs)
+import Data.Monoid (mconcat)
+import Data.Functor ((<$>))
+
+main = do
+ ls <- readFile =<< head <$> getArgs
+ mconcat <$> mapM (putStrLn . unwords . reverse . words) (lines ls) --die eigentliche Funktion, ls ist das argument.
Was passiert hier an Vodoo? Und was machen die ganzen wilden Zeichen da?
+Gehen wir die Main zeilenweise durch: Wir lesen die Datei, die im ersten Kommandozeilen-Argument gegeben wird. getArgs hat folgende Signatur:
+getArgs :: IO [String]
Wir bekommen als eine Liste der Argumente. Wir wollen nur das erste. Also machen wir head getArgs. Allerdings fliegt uns dann ein Fehler. head sieht nämlich so aus:
+head :: [a] -> a
Irgendwie müssen wird as in das IO bekommen. Hierzu gibt es fmap. Somit ist
+fmap head :: IO [a] -> IO a
Ein inline-Alias (um die Funktion links und das Argument rechts zu schreiben und sich ne Menge Klammern zu sparen) ist <$>. Somit ist schlussendlich der Inhalt der Datei aus dem ersten Argument (lazy) in ls.
+Eine andere Möglichkeit sich das (in diesem Fall) zu merken, bzw. drauf zu kommen ist, dass [] AUCH ein Funktor (sogar eine Monade) ist. Man könnte das also auch so schreiben:
+head :: [] a -> a
+head :: Functor f => [] (f a) -> f a -- das "a" geschickt ersetzt zur Verdeutlichung
+getArgs :: IO [] String
+fmap head :: Functor f => f [] a -> f a
fmap “packt” die Funktion quasi 1 Umgebung (Funktor, Monade, ..) weiter rein - Sei es nun in Maybe, Either oder irgendwas anderes.
+Alternatives (ausführliches) Beispiel am Ende.
+Wenn wir uns die Signatur ansehen, dann haben wir nun
+head <$> getArgs :: IO String
readFile will aber nun ein String haben. Man kann nun
+f <- head <$> getArgs
+ls <- readFile f
kann man auch “inline” mit =<< die Sachen “auspacken”.
+Die 2. Zeile lesen wir nun einfach “von hinten”, wie man das meistens tun sollte. Hier ist ein
+lines ls :: [String]
was uns den Inhalt der Datei zeilenweise gibt. Mit jeder Zeile möchten wir nun folgendes machen:
+Wenn wir uns die Signatur ansehen:
+(putStrLn . unwords . reverse . words) :: String -> IO ()
Das mag im ersten Moment verwirren, daher noch die Signaturen der Einzelfunktionen:
+words :: String -> [String]
+reverse :: [a] -> [a]
+unwords :: [String] -> String
+putStrLn :: String -> IO ()
Da wir am Ende in der IO-Monade landen müssen wir das auf unsere Zeilen mit mapM statt map anwenden. Dies sorgt auch dafür, dass die Liste der reihe nach durchgegangen wird. mapM mit unserer Funktion schaut dann so aus:
+mapM (putStrLn . unwords . reverse . words) :: [String] -> [IO ()]
eek! Das [IO ()] sieht ekelig aus. Wir haben eine Liste von IO-gar nichts. Das können wir eigentlich entsorgen. Da wir innerhalb der main-Funktion in einer IO-Monade sind, wollen wir IO () anstatt [IO ()] zurück haben.
+Wenn wir uns jetzt erinnern, dass [] auch nur eine Monade ist und dass jede Monade ein Monoid ist, dann ist die Lösung einfach. Monoide haben eine “append”-funktion (mappend oder (<>) genannt). Wenn wir “nichts” an “nichts” anhängen, dann erhalten wir …. Trommelwirbel “nichts”! Wir müssen die [IO ()]-Liste also “nur noch” mit mappend falten. Hierzu gibt es schon eine vorgefertigte Funktion:
+mconcat :: [a] -> a
+mconcat = foldr mappend mempty
Was genau die gewünschte Faltung macht. Wir müssen nun wieder fmap nehmen, da wir die Liste selbst falten wollen - und nicht map, welches auf den IO () innerhalb der Liste arbeiten würde. Durch die Faltung fällt die Liste nun auf IO () zusammen.
+Viel Voodoo in wenig Code, aber wenn man sich dran gewöhnt hat, sind Monaden in Monaden auch nicht schlimm. Man muss sich immer nur richtig “rein” fmap’en.
+Kleinen Tipp gab es noch: mapM_ macht genau das, was oben mit mconcat erreicht werden sollte. Somit kann man auch
+mapM_ (putStrLn . unwords . reverse . words) (lines ls)
schreiben. Ich hab es aber mal wegen der klarheit oben so gelassen.
+Nehmen wir als alternatives Beispiel mal an:
+a :: IO Maybe State t
Um Funktionen vom Typ
+f :: IO a -> IO a
+f a -- valide
zu nehmen, brauchen wir nichts machen. Bei
+f' :: Maybe a -> Maybe a
brauchen wir 1 fmap, also ein
+f' a -- error
+f' <$> a
um eine Funktion
+f'' :: State t -> State t
zu benutzen folglich:
+f'' a -- error
+f'' <$> a -- error
+fmap f'' <$> a
Backup eines Blogposts eines Kommilitonen
+This weekend I spend some time on Morphisms.
+Knowing that this might sound daunting to many dabbling Haskellers (like I am), I decided to write a real short MergeSort hylomorphism quickstarter.
+For those who need a refresher: MergeSort works by creating a balanced binary tree from the input list and directly collapsing it back into itself while treating the children as sorted lists and merging these with an O(n) algorithm.
+First the usual prelude:
+{-# LANGUAGE DeriveFunctor #-}
+{-# LANGUAGE TypeFamilies #-}
+
+import Data.Functor.Foldable
+import Data.List (splitAt, unfoldr)
We will use a binary tree like this. Note that there is no explicit recursion used, but NodeF
has two holes. These will eventually filled later.
data TreeF c f = EmptyF | LeafF c | NodeF f f
+ deriving (Eq, Show, Functor)
Aside: We could use this as a normal binary tree by wrapping it in Fix
: type Tree a = Fix (TreeF a)
But this would require us to write our tree like Fix (NodeF (Fix (LeafF 'l')) (Fix (LeafF 'r')))
which would get tedious fast. Luckily Edward build a much better way to do this into recursion-schemes. I will touch on this later.
Without further ado we start to write a Coalgebra, which in my book is just a scary name for “function that is used to construct datastructures”.
+unflatten :: [a] -> TreeF a [a]
+unflatten ( []) = EmptyF
+unflatten (x:[]) = LeafF x
+unflatten ( xs) = NodeF l r where (l,r) = splitAt (length xs `div` 2) xs
From the type signature it’s immediately obvious, that we take a list of ’a’s and use it to create a part of our tree.
+The nice thing is that due to the fact that we haven’t commited to a type in our tree nodes we can just put lists in there.
+Aside: At this point we could use this Coalgebra to construct (unsorted) binary trees from lists:
+example1 = ana unflatten [1,3] == Fix (NodeF (Fix (LeafF 1)) (Fix (LeafF 3)))
On to our sorting, tree-collapsing Algebra. Which again is just a creepy word for “function that is used to deconstruct datastructures”.
+The function mergeList
is defined below and just merges two sorted lists into one sorted list in O(n), I would probably take this from the ordlist
package if I were to implement this for real.
Again we see that we can just construct our sorted output list from a TreeF
that apparently contains just lists.
flatten :: Ord a => TreeF a [a] -> [a]
+flatten EmptyF = []
+flatten (LeafF c) = [c]
+flatten (NodeF l r) = mergeLists l r
Aside: We could use a Coalgebra to deconstruct trees:
+example2 = cata flatten (Fix (NodeF (Fix (LeafF 3)) (Fix (LeafF 1)))) == [1,3]
Now we just combine the Coalgebra and the Algebra with one from the functions from Edwards recursion-schemes
library:
mergeSort :: Ord a => [a] -> [a]
+mergeSort = hylo flatten unflatten
+
+example3 = mergeSort [5,2,7,9,1,4] == [1,2,4,5,7,9]
What have we gained?
+We have implemented a MergeSort variant in 9 lines of code, not counting the mergeLists
function below. Not bad, but this implementation is not much longer.
On the other hand the morphism based implementation cleanly describes what happens during construction and deconstruction of our intermediate structure.
+My guess is that, as soon as the algortihms get more complex, this will really make a difference.
+At this point I wasn’t sure if this was useful or remotely applicable. Telling someone “I spend a whole weekend learning about Hylomorphism” isn’t something the cool developer kids do.
+It appeared to me that maybe I should have a look at the Core to see what the compiler finally comes up with (edited for brevity):
+ mergeSort :: [Integer] -> [Integer]
+ mergeSort =
+ \ (x :: [Integer]) ->
+ case x of wild {
+ [] -> [];
+ : x1 ds ->
+ case ds of _ {
+ [] -> : x1 ([]);
+ : ipv ipv1 ->
+ unfoldr
+ lvl9
+ (let {
+ p :: ([Integer], [Integer])
+ p =
+ case $wlenAcc wild 0 of ww { __DEFAULT ->
+ case divInt# ww 2 of ww4 { __DEFAULT ->
+ case tagToEnum# (<# ww4 0) of _ {
+ False ->
+ case $wsplitAt# ww4 wild of _ { (# ww2, ww3 #) -> (ww2, ww3) };
+ True -> ([], wild)
+ }
+ }
+ } } in
+ (case p of _ { (x2, ds1) -> mergeSort x2 },
+ case p of _ { (ds1, y) -> mergeSort y }))
+ }
+ }
+ end Rec }
While I am not really competent in reading Core and this is actually the first time I bothered to try, it is immediately obvious that there is no trace of any intermediate tree structure.
+This is when it struck me. I was dazzled and amazed. And am still. Although we are writing our algorithm as if we are working on a real tree structure the library and the compiler are able to just remove the whole intermediate step.
+Aftermath:
+In the beginning I promised a way to work on non-functor data structures. Actually that was how I began to work with the recursion-schemes
library.
We are able to create a ‘normal’ version of our tree from above:
+data Tree c = Empty | Leaf c | Node (Tree c) (Tree c)
+ deriving (Eq, Show)
But we can not use this directly with our (Co-)Algebras. Luckily Edward build a little bit of type magic into the library:
+type instance Base (Tree c) = (TreeF c)
+
+instance Unfoldable (Tree c) where
+ embed EmptyF = Empty
+ embed (LeafF c) = Leaf c
+ embed (NodeF l r) = Node l r
+
+instance Foldable (Tree c) where
+ project Empty = EmptyF
+ project (Leaf c) = LeafF c
+ project (Node l r) = NodeF l r
Without going into detail by doing this we establish a relationship between Tree
and TreeF
and teach the compiler how to translate between these types.
Now we can use our Alebra on our non functor type:
+example4 = cata flatten (Node (Leaf 'l') (Leaf 'r')) == "lr"
The great thing about this is that, looking at the Core output again, there is no traces of the TreeF
structure to be found. As far as I can tell, the algorithm is working directly on our Tree
type.
Literature:
+Appendix:
+mergeLists :: Ord a => [a] -> [a] -> [a]
+mergeLists = curry $ unfoldr c where
+ c ([], []) = Nothing
+ c ([], y:ys) = Just (y, ([], ys))
+ c (x:xs, []) = Just (x, (xs, []))
+ c (x:xs, y:ys) | x <= y = Just (x, (xs, y:ys))
+ | x > y = Just (y, (x:xs, ys))
Gründe Haskell zu nutzen und wo Vorteile liegen.
+Einige mögen sagen: “duh!”, aber es ist erschreckend, wie viele Leute meinen, dass ihnen die Uni etwas schuldet oder das Dozenten und Tutoren dafür verantwortlich sind, dass man hier etwas lernt. Studium ist eine komplett freiwillige Veranstaltung. Man kann jederzeit sagen: “Passt mir nicht. Ich gehe.” An der Uni wird erwartet, dass man sich ggf. einarbeitet, wenn man etwas nicht weiss; dass man Sekundärliteratur fragt (z.B. in Mathe auch mal in Bücher schaut um eine andere Erklärung zu bekommen, als der Prof an die Tafel geklatscht hat).
+Es gibt einen sehr schönen Talk von Edwand Kmett in dem er über seine Erfahrungen berichtet. Kurzum: Man lernt durch stete Wiederholung. Und der beste Moment etwas zu wiederholen ist, kurz bevor man es vergisst. Das stimmt ziemlich genau mit meiner Erfahrung überein.
+Grade die oben genannte Theorie steht beim Auswendiglernen im Vordergrund. Wenn man etwas langfristig auswendig lernen will (Fremdsprachen, etc.), dann gibt es hierzu Software, die herausfindet, wann es der beste Zeitpunkt ist, dich wieder abzufragen: Anki gibt es für jede Platform kostenlos (außer iPhone - hier 25$, weil Apple so viel Geld für das einstellen im AppStore haben will). Anki ist dazu gedacht, dass man zu jedem Thema einen Stapel hat (z.b. Klausurfragen, Sprachen, …) und jeden Tag lernt. Nach einiger Zeit wird die vorhersage der Lernzeit ziemlich genau. Anfangs beantwortet man noch viele Fragen täglich, aber je häufiger man die Antworten kennt, desto weiter hinten landen sie im Stapel. Schlussendlich kommt dieselbe Frage dann nur noch 1x/Monat oder noch seltener.
+Ich benutze dies insbesondere zum Auswendiglernen von Fakten, Formeln, Fachbegriffen etc. Bei Mathe bietet sich zum Beispiel an einen Stapel mit allen Definitionen zu haben; in der Biologie eine Liste der Schema und Kreisläufe etc.
+Man kann auch einen Hardcore-Lernmarathon machen. Meine letzten beiden Klausuren waren nur auf “bestehen” - also ohne Note. Ich habe mir eine alte Klausur organisiert (mehr genaues unten) und dann daraus Karten erstellt. Dies hat nur wenige Stunden gedauert (2-3 verteilt auf 2 Tage). Damit habe ich dann am Tag vor der Klausur 2x gelernt (1x nach dem Aufstehen, 1x vorm schlafengehen; jeweils nach 30 Minuten hatte ich alle Fragen min. 1x korrekt beantwortet). Am Morgen der Klausur hab ich die Fragen vor dem Aufstehen noch einmal durchgemacht (wieder 25-30 min), habe mir zur Klausur fertig gemacht und bin 30 Min vor der Klausur die Fragen nochmals durchgegangen (15-30 min), aber konnte sie mittlerweile alle auswendig. Insgesamt habe ich mit Anki so für die Klausur effektiv 2h gelernt (+2-3h für das erstellen der Karten), habe die Klausur geschrieben und mit einer 3.0 bestanden (also wäre 3.0 gewesen, wenn es nicht unbenotet gewesen wäre). Kommilitonen, die sich (nach eigener Aussage) 1-2 Wochen auf die Klausur vorbereitet haben und eine Note wollten, schnitten teilweise schlechter ab (viele aber auch viel besser).
+Im Gegensatz zum plumpen auswendig lernen gibt es dann auch Anforderungen, wo es darum geht Methoden und Anwendungen zu verstehen. Inbesondere ist dies in Vorbereitung auf z.B. mündliche Prüfungen der Fall. Hier steht eher die Theorie im Vordergrund.
+Um solche Konzepte zu verstehen braucht es leider Zeit. Hier hilft kein 48h-Lernmarathon um das “mal eben” auf die Kette zu kriegen. Am besten bereitet man sich das gesamte Semester über vor (haha! Als ob! :p). Das “Geheimnis” hier liegt in einer Kombination der Ansätze. Zum einen muss man natürlich verstehen, worum es geht. Hier hilft es Definitionen und Fachbegriffe z.B. mit Anki zu lernen. Allerdings muss man sich zusätzlich noch nach jeder(!) Vorlesung hinsetzen und versuchen den Inhalt zu verdauen. Dies können nur 10 Minuten sein oder auch 2h. Hier kommen dann Dinge zum Tragen, wie Sekundärliteratur, Wikipedia, Google, … Man muss die Zusammenhänge einmal verstehen - da kommt man nicht drumherum. ABER: Unser Gehirn arbeitet Assoziativ. Zusammenhänge sind meist logisch oder krass widersprüchlich. Hieraus kann man dann z.B. “Stichwortketten” bauen, von denen man nur das erste auswendig lernt und von da aus sich an den Rest “erinnert”.
+Kleines Beispiel aus der Welt der Mathematik:
+Vektorraum -> Ist zu einer Basis definiert
+ -> Basis ist die größtmögliche Zahl lin. unabh. Vektoren. Lin. Hülle der Basis ist der VR
+ -> Lin. Hülle ist jede Lin.-Komb. von Vektoren
+ -> Hat eine Vektoraddition und skalare Multiplikation
+ -> Wird über einem Körper aufgespannt
+ -> Körper sind 2 abelsche Gruppen mit Distributivgesetz
+ -> abelsche Gruppe ist Menge mit K.A.I.N.
+ -> ....
+So kann man sich über 5-6 Stichwörter fast am gesamten Stoff der Vorlesung entlanghangeln und merkt schnell, wo es hakt. Hier kann man dann nochmal gezielt nachhaken. Auch kann man bei so einer Struktur aus jedem “a -> b -> c” Anki-Karten machen mit “a” auf der Vorderseite, “b” auf der Rückseite bzw. “b” auf der Vorderseite und “c” auf der Rückseite und so gezielt diese “Ketten” trainieren. Grade in einer mündlichen Prüfung hangeln sich Prüfer ebenfalls an diesen Ketten entlang.
+Je nach Klausurtyp dann mit Anki stumpf Karten machen und auswendig lernen (z.b. Ankreuzklausur, Grafik-annotations-Klausur, ..) oder Übungsaufgaben/Altklausuren durchrechnen
+Wenn ihr einen Reihe von Protokollen vorliegen habt, dann schreibt alle Fragen heraus und notiert, wie häufig diese Frage gestellt wurde. So findet ihr heraus, auf welche Punkte der Prüfer besonders Wert legt (z.B. häufig sein eigenes Forschungsfeld). Diese Fragen dann restlos klären und zu Anki-Karten verarbeiten. Das reicht meistens für ein Bestehen. Wenn ihr auf eine gute Note wert legt, dann solltet ihr auch noch die Vorlesung, wie im Bereich “Methodik lernen” erwähnt, nacharbeiten. Insbesondere helfen hier die Assoziationsketten weiter den Stoff auch in der Prüfung in der richtigen Reihenfolge abzurufen. Vielleicht erkennt ihr solche Ketten schon aus den Prüfungsprotokollen und könnt euch ausmalen, wie man z.b. von da aus auf andere Themen der Vorlesung kommt (die z.b. neu sind oder überarbeitet wurden).
+Einige Dozenten machen unterschiedliche Anforderungen, ob sie einen Bachelor oder einen Master-Studenten prüfen. Abgesehen von der anderen Prüfungszeit (15-30min bei bachelor, 25-45 bei Master) ist hier auch das Vorgehen anders. Bei einem Bachelor wird klassischerweise alles oberflächlich abgefragt und nur wenig in die Tiefe gegangen. Bei einem Master wir nur noch stichpunktartig gefragt, dafür aber bis ins Detail.
+Beispiel: Ich hatte eine mündliche Masterprüfung, bei der in der Vorlesung 7 verschiedene Themen behandelt wurden. In der Prüfung wurden dann nur die Themenübersicht abgefragt und bei 2 Themen komplett in die Tiefe gegangen - inkl. Formeln, Bedeutung, Übertragung auf in der Vorlesung nicht angesprochene Aspekte etc. Die anderen 5 Themen kamen nicht dran. Bei meinen Bachelorprüfungen war das eher umgekehrt: Hier wurde sich grob an der Vorlesung entlang gehangelt und zumindest alles einmal kurz angetestet, ob die zentralen Inhalte der Vorlesung verstanden wurden.
+Dies hat häufig auch damit zu tun, dass man im Bachelor eher Grundlagen hört und somit ein grobes Verständnis aller Dinge wichtig ist, während im Master auf die Aneignung von Tiefenwissen ankommt.
+Zu guter Letzt noch ein paar Worte zum Thema Prüfungsangst. Es ist normal, dass man vor einer Prüfung angespannt ist. Es ist nicht normal, wenn die Anspannung so ausartet, dass man sich übergibt, Krämpfe bekommt oder ähnlich starke Symptome zeigt. Ich leide selbst an solchen Problemen und habe mich schon mehrfach vor Prüfungen übergeben. Eine klassische Konfrontationstherapie funktioniert aufgrund der Seltenheit der Prüfungen nicht oder nur sehr schwer. Ich habe mich an meinen Arzt gewendet und habe nun genau für solche Situationen ein Medikament. 1-2h vor einer Prüfung nehme ich das und komme in einen komischen Zustand. Ich merke zwar noch, dass ich Angespannt bin und eigentlich Angst hätte, aber es “stört” mich nicht wirklich. Es versetzt mich nicht in Panik oder sonstwas. Es schaltet mein Gehirn nicht aus oder hat andere negative Effekte. Natürlich geht das auch mit Nachteilen einher: ein paar Tage keinen Alkohol, kein Auto fahren, etc. - Aber meist ist das ja nur 2-3x/Semester der Fall. Wenn man nicht so stark betroffen ist, dann ist davon allerdings abzuraten. Das Medikament gleicht die Panik durch Gelassenheit aus - wenn man keine Panik hat, dann wird man hierdurch so “gelassen” dass man mehrere Stunden einschläft - was in einer Prüfung vielleicht nicht ganz so gut ist ;)
+Es gibt auch zahlreiche Regularien und Rechtsansprüche, die ihr bei sowas habt. Ihr habt zum Beispiel (sofern ein (Amts?-)Arzt eine Prüfungsangst bestätigt hat) Anspruch auf mehr Prüfungszeit, die Prüfung alleine abzulegen (z.b. bei einem Mitarbeiter, während andere im Hörsaal schreiben), eine mündliche durch eine schriftliche zu tauschen (oder umgekehrt), etc. Das kann man individuell mit dem Prüfer absprechen. Ich weiss nicht, wie das in anderen Fakultäten läuft - aber in der Technischen Fakultät hat fast jeder Prüfer dafür volles Verständnis (einige litten sogar früher selbst an sowas).
+Die kostenlose psychologische Beratung an der Uni (aka. “Das rote Sofa” im X) bietet hier auch Hilfestellung bei und vermittelt in schwereren Fällen auch gleich noch eine Therapie/Ärzte. Hier kann man z.b. Prüfungssimulationen abhalten oder sich Hilfe holen, wenn ein Dozent sich querstellt. Die Mitarbeiter begleiten einen z.B. auch zu einer Prüfung (nach Absprache mit dem Veranstalter), falls das hilft, etc.
+Es ist keine Schande so ein Problem zu haben und es gibt genug, die sich damit rumschlagen. Aber man ist hier an der Uni auch nicht alleine damit. Es gibt zahlreiche Hilfsangebote.
+Viel Erfolg bei euren Prüfungen. Falls euch dieser Artikel geholfen hat oder ihr noch Anregungen/Verbessenguswünsche habt, schreibt mir einfach.
+ + +Warnung: Diese Seite enthält Material, von dem SIE nicht wollen, dass es bekannt wird. Speichern Sie diese Seite nicht auf Ihrer lokalen Platte ab, denn sonst sind Sie auch dran, wenn SIE plötzlich bei Ihnen vor der Tür stehen; und das passiert schneller als man denkt. Auch sollten Sie versuchen, alle Hinweise darauf, dass Sie diese Seite jemals gelesen haben, zu vernichten. Tragen Sie diese Seite auf keinen Fall in ihre Hotlist/Bookmarks/etc… ein!
+Vielen Dank für die Beachtung aller Sicherheitsvorschriften.
+Vor einigen Jahren fiel es einigen Unerschrockenen zum ersten Mal auf, dass in den Medien immer wieder von einer Stadt namens ‘Bielefeld’ die Rede war, dass aber niemand jemanden aus Bielefeld kannte, geschweige denn selbst schon einmal dort war. Zuerst hielten sie dies für eine belanglose Sache, aber dann machte es sie doch neugierig. Sie unterhielten sich mit anderen darüber, ohne zu ahnen, dass dies bereits ein Fehler war: Aus heutiger Sicht steht fest, dass jemand geplaudert haben muss, denn sofort darauf wurden SIE aktiv. Plötzlich tauchten Leute auf, die vorgaben, schon einmal in Bielefeld gewesen zu sein; sogar Personen, die vormals noch laut Zweifel geäußert hatten, berichteten jetzt davon, sich mit eigenen Augen von der Existenz vergewissert zu haben - immer hatten diese Personen bei ihren Berichten einen seltsam starren Blick. Doch da war es schon zu spät - die Saat des Zweifels war gesät. Weitere Personen stießen zu der Kerngruppe der Zweifler, immer noch nicht sicher, was oder wem man da auf der Spur war.
+Dann, im Oktober 1993, der Durchbruch: Auf der Fahrt von Essen nach Kiel auf der A2 erhielten vier der hartnäckigsten Streiter für die Aufdeckung der Verschwörung ein Zeichen: Jemand hatte auf allen Schildern den Namen ‘Bielefeld’ mit orangem Klebeband durchgestrichen. Da wußte die Gruppe: Man ist nicht alleine, es gibt noch andere, im Untergrund arbeitende Zweifler, womöglich über ganz Deutschland verteilt, die auch vor spektakulären Aktionen nicht zurückschrecken. Von da an war uns klar: Wir müssen diese Scharade aufdecken, koste es, was es wolle! Das Ausmaß der Verschwörung
+Der Aufwand, mit dem die Täuschung der ganzen Welt betrieben wird, ist enorm. Die Medien, von denen ja bekannt ist, dass sie unter IHRER Kontrolle stehen, berichten tagaus, tagein von Bielefeld, als sei dies eine Stadt wie jede andere, um der Bevölkerung das Gefühl zu geben, hier sei alles ganz normal. Aber auch handfestere Beweise werden gefälscht: SIE kaufen hunderttausende von Autos, versehen sie mit gefälschten ’BI-’Kennzeichen und lassen diese durch ganz Deutschland fahren. SIE stellen, wie bereits oben geschildert, entlang der Autobahnen große Schilder auf, auf denen Bielefeld erwähnt wird. SIE veröffentlichen Zeitungen, die angeblich in Bielefeld gedruckt werden. Anscheinend haben SIE auch die Deutsche Post AG in Ihrer Hand, denn auch im PLZB findet man einen Eintrag für Bielefeld; und ebenso wird bei der Telekom ein komplettes Ortsnetz für Bielefeld simuliert. Einige Leute behaupten sogar in Bielefeld studiert zu haben und können auch gut gefälschte Diplome u.ä. der angeblich existenten Uni Bielefeld vorweisen. Auch Bundeskanzler Gerhard Schröder behauptet, 1965 das “Westfalen-Kolleg” in Bielefeld besucht zu haben, wie seinem Lebenslauf unter dem Link Bildungsweg zu entnehmen ist.
+Aber auch vor dem Internet machen SIE nicht halt. SIE vergeben Mail-Adressen für die Domain uni-bielefeld.de, und SIE folgen auch den neuesten Trends: SIE bieten im WWW eine “Stadtinfo über Bielefeld” an, sogar mit Bildern; das Vorgarten-Foto, das dem Betrachter als “Botanischer Garten” verkauft werden sollte, ist nach der Entlarvung auf dieser Seite jedoch inzwischen wieder entfernt worden. Aber auch die noch vorhandenen Bilder sind sogar für den Laien als Fotomontagen zu erkennen. Wir sind noch nicht dahinter gekommen, wo der Rechner steht, auf dem die Domains .bielefeld.de und uni-bielefeld.de gefälscht werden; wir arbeiten daran. Inzwischen wurde auch von einem IHRER Agenten - der Täter ist uns bekannt - versucht, diese WWW-Seite zu sabotieren, ich konnte den angerichteten Schaden jedoch zum Glück wieder beheben.
+Ein anonymer Informant, der ganz offensichtlich zu IHNEN zu gehören scheint oder zumindest gute Kontakte zu IHNEN hat, hat mich kürzlich in einer Mail auf die nächste Stufe IHRER Planung hingewiesen: “Ich schätze, spätestens in 10 Jahren wird es heißen: Bielefeld muss Hauptstadt werden.” Was das bedeutet, muss ja wohl nicht extra betont werden.
+Die schrecklichste Maßnahme, die SIE ergriffen haben, ist aber zweifelsohne immer noch die Gehirnwäsche, der immer wieder harmlose Menschen unterzogen werden, die dann anschließend auch die Existenz von Bielefeld propagieren. Immer wieder verschwinden Menschen, gerade solche, die sich öffentlich zu ihren Bielefeldzweifeln bekannt haben, nur um dann nach einiger Zeit wieder aufzutauchen und zu behaupten, sie seien in Bielefeld gewesen. Womöglich wurden einige Opfer sogar mit Telenosestrahlen behandelt. Diesen armen Menschen konnten wir bisher nicht helfen. Wir haben allerdings inzwischen einen Verdacht, wo diese Gehirnwäsche durchgeführt wird: Im sogenannten Bielefeld-Zentrum, wobei SIE sogar die Kaltblütigkeit besitzen, den Weg zu diesem Ort des Schreckens von der Autobahn aus mit großen Schildern auszuschildern. Wir sind sprachlos, welchen Einfluß SIE haben.
+Inzwischen sind - wohl auch durch mehrere Berichte in den wenigen nicht von IHNEN kontrollierten Medien - mehr und mehr Leute wachsamer geworden und machen uns auf weitere Aspekte der Verschwörung aufmerksam. So berichtet zum Beispiel Holger Blaschka:
+“Auch der DFB ist in diesen gewaltigen Skandal verwickelt, spielt in der ersten Liga doch ein Verein, den SIE Arminia Bielefeld getauft haben, der innert 2 Jahren aus dem Nichts der Amateur-Regionen im bezahlten Fußball auftauchte und jetzt im Begriff ist, sich zu IHRER besten Waffe gegen all die Zweifler zu entwickeln. Den Gästefans wird vorgetäuscht mit ihren Bussen nach Bielefeld zu kommen, wo sie von IHNEN abgefangen werden, um direkt ins Stadion geleitet zu werden. Es besteht keine Chance sich die Stadt näher anzuschauen, und auch die Illusion des Heimpublikums wird durch eine größere Menge an bezahlten Statisten aufrechterhalten. Selbst ehemalige Top-Spieler, die Ihren Leistungszenit bei weitem überschritten haben, werden zu diesem Zweck von IHNEN mißbraucht. Mit genialen Manövern, u.a. vorgetäuschten Faustschlägen und Aufständen gegen das Präsidium eines baldigen Drittligisten wurde von langer Hand die wohl aufwendigste Täuschung aller Zeiten inszeniert. Es gibt noch mehr Beweise: Das sich im Rohbau befindende Stadion, das gefälschte und verpanschte Bier und nicht zuletzt die Tatsache, dass dieser Verein nur einen Sponsor hat. SIE, getarnt als Modefirma Gerry Weber.”
+Dies ist die Frage, auf die wir auch nach jahrelangen Untersuchungen immer noch keine befriedigende Antwort geben können. Allerdings gibt es einige Indizien, die auf bestimmte Gruppierungen hinweisen:
+Zum einen können wir alle an den Bundestag, das Europaparlament und die UNO schreiben, um endlich zu erreichen, dass SIE nicht mehr von den Politikern gedeckt werden. Da aber zu befürchten ist, dass SIE die Politik - so wie auch das organisierte Verbrechen und die großen Weltreligionen - unter Kontrolle haben, sind die Erfolgschancen dieses Weges doch eher zweifelhaft.
+Eine weitere Möglichkeit besteht darin, dass sich alle Bielefeldzweifler treffen und gemeinsam durch transzendentale Meditation (TM) soviel positive Ausstrahlung erzeugen, dass der Schwindel auffliegt. Eine ähnliche Vorgehensweise hat in Washington, D.C. für eine Senkung der Verbrechensrate um über 20% gesorgt. Besonders effektiv ist dies im Zusammenwirken mit Hopi-Kerzen im Ohr und Yogischem Schweben.
+Ab und zu nimmt in einer der eigentlich von IHNEN kontrollierten Zeitungen ein Redakteur allen Mut zusammen und riskiert es, in einer der Ausgaben zumindest andeutungsweise auf die Verschwörung hinzuweisen. So wurde in der FAZ Bielefeld als “Die Mutter aller Un-Städte” bezeichnet, und die taz überschrieb einen Artikel mit “Das Bermuda-Dreieck bei Bielefeld”. Auf Nachfrage bekommt man dann natürlich zu hören, das habe man alles ganz anders gemeint, bei der taz hieß es sogar, es hätte in Wirklichkeit “Bitterfeld” heißen sollen, aber für einen kurzen Moment wurden die Leser darauf aufmerksam gemacht, dass mit Bielefeld etwas nicht stimmt. An dem Mut dieser Redakteure, über deren weiteres Schicksal uns leider nichts bekannt ist, sollten wir uns alle ein Beispiel nehmen.
+Das, was wir alle aber für uns im kleinen tun können, ist folgendes: Kümmert euch um die bedauernswerten Opfer der Gehirnwäsche, umsorgt sie, macht ihnen behutsam klar, dass sie einer Fehlinformation unterliegen. Und, bekennt euch alle immer offen, damit SIE merken, dass wir uns nicht länger täuschen lassen: Bielefeld gibt es nicht!!!
+ + +{"use strict";Ys.formatArgs=a2e;Ys.save=s2e;Ys.load=o2e;Ys.useColors=i2e;Ys.storage=l2e();Ys.destroy=(()=>{let t=!1;return()=>{t||(t=!0,console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`."))}})();Ys.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"];function i2e(){if(typeof window<"u"&&window.process&&(window.process.type==="renderer"||window.process.__nwjs))return!0;if(typeof navigator<"u"&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))return!1;let t;return typeof document<"u"&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||typeof window<"u"&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||typeof navigator<"u"&&navigator.userAgent&&(t=navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/))&&parseInt(t[1],10)>=31||typeof navigator<"u"&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)}o(i2e,"useColors");function a2e(t){if(t[0]=(this.useColors?"%c":"")+this.namespace+(this.useColors?" %c":" ")+t[0]+(this.useColors?"%c ":" ")+"+"+Bb.exports.humanize(this.diff),!this.useColors)return;let e="color: "+this.color;t.splice(1,0,e,"color: inherit");let r=0,n=0;t[0].replace(/%[a-zA-Z%]/g,i=>{i!=="%%"&&(r++,i==="%c"&&(n=r))}),t.splice(n,0,e)}o(a2e,"formatArgs");Ys.log=console.debug||console.log||(()=>{});function s2e(t){try{t?Ys.storage.setItem("debug",t):Ys.storage.removeItem("debug")}catch{}}o(s2e,"save");function o2e(){let t;try{t=Ys.storage.getItem("debug")}catch{}return!t&&typeof process<"u"&&"env"in process&&(t=process.env.DEBUG),t}o(o2e,"load");function l2e(){try{return localStorage}catch{}}o(l2e,"localstorage");Bb.exports=KF()(Ys);var{formatters:c2e}=Bb.exports;c2e.j=function(t){try{return JSON.stringify(t)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}}});var uit,ZF=R(()=>{"use strict";MF();GF();HF();YF();WF();uit=Xi(QF(),1)});var FC,BC,JF,Fb,u2e,zb,V1=R(()=>{"use strict";ut();ZF();FC={body:' Qi))l.moveTo(0,0);else if(v>Gp-Qi)l.moveTo(m*Dh(g),m*xl(g)),l.arc(0,0,m,g,y,!x),p>Qi&&(l.moveTo(p*Dh(y),p*xl(y)),l.arc(0,0,p,y,g,x));else{var b=g,w=y,S=g,T=y,E=v,_=v,A=s.apply(this,arguments)/2,L=A>Qi&&(n?+n.apply(this,arguments):gd(p*p+m*m)),M=R3(J8(m-p)/2,+r.apply(this,arguments)),N=M,k=M,I,C;if(L>Qi){var O=e_(L/p*xl(A)),D=e_(L/m*xl(A));(E-=O*2)>Qi?(O*=x?1:-1,S+=O,T-=O):(E=0,S=T=(g+y)/2),(_-=D*2)>Qi?(D*=x?1:-1,b+=D,w-=D):(_=0,b=w=(g+y)/2)}var P=m*Dh(b),F=m*xl(b),B=p*Dh(T),$=p*xl(T);if(M>Qi){var z=m*Dh(w),Y=m*xl(w),Q=p*Dh(S),X=p*xl(S),ie;if(v ${this.parser.parseInline(e)} An error occurred: ${i.tokens?.map(n).join("")}",e}},cxe={\u00EE:"\u0131\u0302",\u00EF:"\u0131\u0308",\u00ED:"\u0131\u0301",\u00EC:"\u0131\u0300"},ms=class{static{o(this,"SymbolNode")}constructor(e,r,n,i,a,s,l,u){this.text=void 0,this.height=void 0,this.depth=void 0,this.italic=void 0,this.skew=void 0,this.width=void 0,this.maxFontSize=void 0,this.classes=void 0,this.style=void 0,this.text=e,this.height=r||0,this.depth=n||0,this.italic=i||0,this.skew=a||0,this.width=s||0,this.classes=l||[],this.style=u||{},this.maxFontSize=0;var h=X2e(this.text.charCodeAt(0));h&&this.classes.push(h+"_fallback"),/[îïíì]/.test(this.text)&&(this.text=cxe[this.text])}hasClass(e){return Vt.contains(this.classes,e)}toNode(){var e=document.createTextNode(this.text),r=null;this.italic>0&&(r=document.createElement("span"),r.style.marginRight=ct(this.italic)),this.classes.length>0&&(r=r||document.createElement("span"),r.className=dh(this.classes));for(var n in this.style)this.style.hasOwnProperty(n)&&(r=r||document.createElement("span"),r.style[n]=this.style[n]);return r?(r.appendChild(e),r):e}toMarkup(){var e=!1,r="0&&(n+="margin-right:"+this.italic+"em;");for(var i in this.style)this.style.hasOwnProperty(i)&&(n+=Vt.hyphenate(i)+":"+this.style[i]+";");n&&(e=!0,r+=' style="'+Vt.escape(n)+'"');var a=Vt.escape(this.text);return e?(r+=">",r+=a,r+="",r):a}},ll=class{static{o(this,"SvgNode")}constructor(e,r){this.children=void 0,this.attributes=void 0,this.children=e||[],this.attributes=r||{}}toNode(){var e="http://www.w3.org/2000/svg",r=document.createElementNS(e,"svg");for(var n in this.attributes)Object.prototype.hasOwnProperty.call(this.attributes,n)&&r.setAttribute(n,this.attributes[n]);for(var i=0;i
/gi,Cbe=o(t=>t?_$(t).replace(/\\n/g,"#br#").split("#br#"):[""],"getRows"),Sbe=(()=>{let t=!1;return()=>{t||(Abe(),t=!0)}})();o(Abe,"setupDompurifyHooks");A$=o(t=>(Sbe(),bp.default.sanitize(t)),"removeScript"),C$=o((t,e)=>{if(e.flowchart?.htmlLabels!==!1){let r=e.securityLevel;r==="antiscript"||r==="strict"?t=A$(t):r!=="loose"&&(t=_$(t),t=t.replace(/
"),"placeholderToBreak"),_$=o(t=>t.replace(Qf,"#br#"),"breakToPlaceholder"),Nbe=o(t=>{let e="";return t&&(e=window.location.protocol+"//"+window.location.host+window.location.pathname+window.location.search,e=e.replaceAll(/\(/g,"\\("),e=e.replaceAll(/\)/g,"\\)")),e},"getUrl"),yr=o(t=>!(t===!1||["false","null","0"].includes(String(t).trim().toLowerCase())),"evaluate"),Mbe=o(function(...t){let e=t.filter(r=>!isNaN(r));return Math.max(...e)},"getMax"),Ibe=o(function(...t){let e=t.filter(r=>!isNaN(r));return Math.min(...e)},"getMin"),gh=o(function(t){let e=t.split(/(,)/),r=[];for(let n=0;n0)for(var r=new Array(i),n=0,i,a;n{"use strict";J$()});var z4,wS,TS=R(()=>{"use strict";z4="http://www.w3.org/1999/xhtml",wS={svg:"http://www.w3.org/2000/svg",xhtml:z4,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"}});function ic(t){var e=t+="",r=e.indexOf(":");return r>=0&&(e=t.slice(0,r))!=="xmlns"&&(t=t.slice(r+1)),wS.hasOwnProperty(e)?{space:wS[e],local:t}:t}var G4=R(()=>{"use strict";TS();o(ic,"default")});function I4e(t){return function(){var e=this.ownerDocument,r=this.namespaceURI;return r===z4&&e.documentElement.namespaceURI===z4?e.createElement(t):e.createElementNS(r,t)}}function O4e(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}function dy(t){var e=ic(t);return(e.local?O4e:I4e)(e)}var kS=R(()=>{"use strict";G4();TS();o(I4e,"creatorInherit");o(O4e,"creatorFixed");o(dy,"default")});function P4e(){}function wh(t){return t==null?P4e:function(){return this.querySelector(t)}}var $4=R(()=>{"use strict";o(P4e,"none");o(wh,"default")});function ES(t){typeof t!="function"&&(t=wh(t));for(var e=this._groups,r=e.length,n=new Array(r),i=0;i{"use strict";hl();o(FS,"default")});function zS(){for(var t=this._groups,e=-1,r=t.length;++e0;){if(h=fy(s,l,r),h===u)return n[i]=s,n[a]=l,e(n);if(h>0)s=Math.floor(s/h)*h,l=Math.ceil(l/h)*h;else if(h<0)s=Math.ceil(s*h)/h,l=Math.floor(l*h)/h;else break;u=h}return t},t}function gl(){var t=By();return t.copy=function(){return x3(t,gl())},Ah.apply(t,arguments),U5e(t)}var XH=R(()=>{"use strict";bh();R8();Py();qH();o(U5e,"linearish");o(gl,"linear")});function M8(t,e){t=t.slice();var r=0,n=t.length-1,i=t[r],a=t[n],s;return a{"use strict";o(M8,"nice")});function dn(t,e,r,n){function i(a){return t(a=arguments.length===0?new Date:new Date(+a)),a}return o(i,"interval"),i.floor=a=>(t(a=new Date(+a)),a),i.ceil=a=>(t(a=new Date(a-1)),e(a,1),t(a),a),i.round=a=>{let s=i(a),l=i.ceil(a);return a-s0))return u;let h;do u.push(h=new Date(+a)),e(a,l),t(a);while(hdn(s=>{if(s>=s)for(;t(s),!a(s);)s.setTime(s-1)},(s,l)=>{if(s>=s)if(l<0)for(;++l<=0;)for(;e(s,-1),!a(s););else for(;--l>=0;)for(;e(s,1),!a(s););}),r&&(i.count=(a,s)=>(I8.setTime(+a),O8.setTime(+s),t(I8),t(O8),Math.floor(r(I8,O8))),i.every=a=>(a=Math.floor(a),!isFinite(a)||!(a>0)?null:a>1?i.filter(n?s=>n(s)%a===0:s=>i.count(0,s)%a===0):i)),i}var I8,O8,mu=R(()=>{"use strict";I8=new Date,O8=new Date;o(dn,"timeInterval")});var oc,KH,P8=R(()=>{"use strict";mu();oc=dn(()=>{},(t,e)=>{t.setTime(+t+e)},(t,e)=>e-t);oc.every=t=>(t=Math.floor(t),!isFinite(t)||!(t>0)?null:t>1?dn(e=>{e.setTime(Math.floor(e/t)*t)},(e,r)=>{e.setTime(+e+r*t)},(e,r)=>(r-e)/t):oc);KH=oc.range});var Ks,QH,B8=R(()=>{"use strict";mu();Ks=dn(t=>{t.setTime(t-t.getMilliseconds())},(t,e)=>{t.setTime(+t+e*1e3)},(t,e)=>(e-t)/1e3,t=>t.getUTCSeconds()),QH=Ks.range});var gu,H5e,b3,Y5e,F8=R(()=>{"use strict";mu();gu=dn(t=>{t.setTime(t-t.getMilliseconds()-t.getSeconds()*1e3)},(t,e)=>{t.setTime(+t+e*6e4)},(t,e)=>(e-t)/6e4,t=>t.getMinutes()),H5e=gu.range,b3=dn(t=>{t.setUTCSeconds(0,0)},(t,e)=>{t.setTime(+t+e*6e4)},(t,e)=>(e-t)/6e4,t=>t.getUTCMinutes()),Y5e=b3.range});var yu,W5e,w3,q5e,z8=R(()=>{"use strict";mu();yu=dn(t=>{t.setTime(t-t.getMilliseconds()-t.getSeconds()*1e3-t.getMinutes()*6e4)},(t,e)=>{t.setTime(+t+e*36e5)},(t,e)=>(e-t)/36e5,t=>t.getHours()),W5e=yu.range,w3=dn(t=>{t.setUTCMinutes(0,0,0)},(t,e)=>{t.setTime(+t+e*36e5)},(t,e)=>(e-t)/36e5,t=>t.getUTCHours()),q5e=w3.range});var Do,X5e,zy,j5e,T3,K5e,G8=R(()=>{"use strict";mu();Do=dn(t=>t.setHours(0,0,0,0),(t,e)=>t.setDate(t.getDate()+e),(t,e)=>(e-t-(e.getTimezoneOffset()-t.getTimezoneOffset())*6e4)/864e5,t=>t.getDate()-1),X5e=Do.range,zy=dn(t=>{t.setUTCHours(0,0,0,0)},(t,e)=>{t.setUTCDate(t.getUTCDate()+e)},(t,e)=>(e-t)/864e5,t=>t.getUTCDate()-1),j5e=zy.range,T3=dn(t=>{t.setUTCHours(0,0,0,0)},(t,e)=>{t.setUTCDate(t.getUTCDate()+e)},(t,e)=>(e-t)/864e5,t=>Math.floor(t/864e5)),K5e=T3.range});function fd(t){return dn(e=>{e.setDate(e.getDate()-(e.getDay()+7-t)%7),e.setHours(0,0,0,0)},(e,r)=>{e.setDate(e.getDate()+r*7)},(e,r)=>(r-e-(r.getTimezoneOffset()-e.getTimezoneOffset())*6e4)/6048e5)}function dd(t){return dn(e=>{e.setUTCDate(e.getUTCDate()-(e.getUTCDay()+7-t)%7),e.setUTCHours(0,0,0,0)},(e,r)=>{e.setUTCDate(e.getUTCDate()+r*7)},(e,r)=>(r-e)/6048e5)}var yl,_h,k3,E3,cc,C3,S3,JH,Q5e,Z5e,J5e,ewe,twe,rwe,pd,Bp,eY,tY,Lh,rY,nY,iY,nwe,iwe,awe,swe,owe,lwe,$8=R(()=>{"use strict";mu();o(fd,"timeWeekday");yl=fd(0),_h=fd(1),k3=fd(2),E3=fd(3),cc=fd(4),C3=fd(5),S3=fd(6),JH=yl.range,Q5e=_h.range,Z5e=k3.range,J5e=E3.range,ewe=cc.range,twe=C3.range,rwe=S3.range;o(dd,"utcWeekday");pd=dd(0),Bp=dd(1),eY=dd(2),tY=dd(3),Lh=dd(4),rY=dd(5),nY=dd(6),iY=pd.range,nwe=Bp.range,iwe=eY.range,awe=tY.range,swe=Lh.range,owe=rY.range,lwe=nY.range});var vu,cwe,A3,uwe,V8=R(()=>{"use strict";mu();vu=dn(t=>{t.setDate(1),t.setHours(0,0,0,0)},(t,e)=>{t.setMonth(t.getMonth()+e)},(t,e)=>e.getMonth()-t.getMonth()+(e.getFullYear()-t.getFullYear())*12,t=>t.getMonth()),cwe=vu.range,A3=dn(t=>{t.setUTCDate(1),t.setUTCHours(0,0,0,0)},(t,e)=>{t.setUTCMonth(t.getUTCMonth()+e)},(t,e)=>e.getUTCMonth()-t.getUTCMonth()+(e.getUTCFullYear()-t.getUTCFullYear())*12,t=>t.getUTCMonth()),uwe=A3.range});var Qs,hwe,vl,fwe,U8=R(()=>{"use strict";mu();Qs=dn(t=>{t.setMonth(0,1),t.setHours(0,0,0,0)},(t,e)=>{t.setFullYear(t.getFullYear()+e)},(t,e)=>e.getFullYear()-t.getFullYear(),t=>t.getFullYear());Qs.every=t=>!isFinite(t=Math.floor(t))||!(t>0)?null:dn(e=>{e.setFullYear(Math.floor(e.getFullYear()/t)*t),e.setMonth(0,1),e.setHours(0,0,0,0)},(e,r)=>{e.setFullYear(e.getFullYear()+r*t)});hwe=Qs.range,vl=dn(t=>{t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},(t,e)=>{t.setUTCFullYear(t.getUTCFullYear()+e)},(t,e)=>e.getUTCFullYear()-t.getUTCFullYear(),t=>t.getUTCFullYear());vl.every=t=>!isFinite(t=Math.floor(t))||!(t>0)?null:dn(e=>{e.setUTCFullYear(Math.floor(e.getUTCFullYear()/t)*t),e.setUTCMonth(0,1),e.setUTCHours(0,0,0,0)},(e,r)=>{e.setUTCFullYear(e.getUTCFullYear()+r*t)});fwe=vl.range});function sY(t,e,r,n,i,a){let s=[[Ks,1,1e3],[Ks,5,5*1e3],[Ks,15,15*1e3],[Ks,30,30*1e3],[a,1,6e4],[a,5,5*6e4],[a,15,15*6e4],[a,30,30*6e4],[i,1,36e5],[i,3,3*36e5],[i,6,6*36e5],[i,12,12*36e5],[n,1,864e5],[n,2,2*864e5],[r,1,6048e5],[e,1,2592e6],[e,3,3*2592e6],[t,1,31536e6]];function l(h,f,d){let p=f{"use strict";bh();P8();B8();F8();z8();G8();$8();V8();U8();o(sY,"ticker");[pwe,mwe]=sY(vl,A3,pd,T3,w3,b3),[H8,Y8]=sY(Qs,vu,yl,Do,yu,gu)});var _3=R(()=>{"use strict";P8();B8();F8();z8();G8();$8();V8();U8();oY()});function W8(t){if(0<=t.y&&t.y<100){var e=new Date(-1,t.m,t.d,t.H,t.M,t.S,t.L);return e.setFullYear(t.y),e}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function q8(t){if(0<=t.y&&t.y<100){var e=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return e.setUTCFullYear(t.y),e}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function Gy(t,e,r){return{y:t,m:e,d:r,H:0,M:0,S:0,L:0}}function X8(t){var e=t.dateTime,r=t.date,n=t.time,i=t.periods,a=t.days,s=t.shortDays,l=t.months,u=t.shortMonths,h=$y(i),f=Vy(i),d=$y(a),p=Vy(a),m=$y(s),g=Vy(s),y=$y(l),v=Vy(l),x=$y(u),b=Vy(u),w={a:P,A:F,b:B,B:$,c:null,d:dY,e:dY,f:Fwe,g:Xwe,G:Kwe,H:Owe,I:Pwe,j:Bwe,L:vY,m:zwe,M:Gwe,p:z,q:Y,Q:gY,s:yY,S:$we,u:Vwe,U:Uwe,V:Hwe,w:Ywe,W:Wwe,x:null,X:null,y:qwe,Y:jwe,Z:Qwe,"%":mY},S={a:Q,A:X,b:ie,B:j,c:null,d:pY,e:pY,f:tTe,g:hTe,G:dTe,H:Zwe,I:Jwe,j:eTe,L:bY,m:rTe,M:nTe,p:J,q:Z,Q:gY,s:yY,S:iTe,u:aTe,U:sTe,V:oTe,w:lTe,W:cTe,x:null,X:null,y:uTe,Y:fTe,Z:pTe,"%":mY},T={a:M,A:N,b:k,B:I,c:C,d:hY,e:hY,f:Rwe,g:uY,G:cY,H:fY,I:fY,j:Awe,L:Dwe,m:Swe,M:_we,p:L,q:Cwe,Q:Mwe,s:Iwe,S:Lwe,u:bwe,U:wwe,V:Twe,w:xwe,W:kwe,x:O,X:D,y:uY,Y:cY,Z:Ewe,"%":Nwe};w.x=E(r,w),w.X=E(n,w),w.c=E(e,w),S.x=E(r,S),S.X=E(n,S),S.c=E(e,S);function E(H,q){return function(K){var se=[],ce=-1,ue=0,te=H.length,De,oe,ke;for(K instanceof Date||(K=new Date(+K));++ce{"use strict";am();Kp();o(v6e,"copyObject");Bo=v6e});function x6e(t,e){for(var r=-1,n=Array(t);++r
"},r),We.lineBreakRegex.test(t)))return t;let n=t.split(" ").filter(Boolean),i=[],a="";return n.forEach((s,l)=>{let u=Cl(`${s} `,r),h=Cl(a,r);if(u>e){let{hyphenatedStrings:p,remainingWord:m}=sCe(s,e,"-",r);i.push(a,...p),a=m}else h+u>=e?(i.push(a),a=s):a=[a,s].filter(Boolean).join(" ");l+1===n.length&&i.push(a)}),i.filter(s=>s!=="").join(r.joinWith)},(t,e,r)=>`${t}${e}${r.fontSize}${r.fontWeight}${r.fontFamily}${r.joinWith}`),sCe=qp((t,e,r="-",n)=>{n=Object.assign({fontSize:12,fontWeight:400,fontFamily:"Arial",margin:0},n);let i=[...t],a=[],s="";return i.forEach((l,u)=>{let h=`${s}${l}`;if(Cl(h,n)>=e){let d=u+1,p=i.length===d,m=`${h}${r}`;a.push(p?h:m),s=""}else s=h}),{hyphenatedStrings:a,remainingWord:s}},(t,e,r="-",n)=>`${t}${e}${r}${n.fontSize}${n.fontWeight}${n.fontFamily}`);o(g5,"calculateTextHeight");o(Cl,"calculateTextWidth");t9=qp((t,e)=>{let{fontSize:r=12,fontFamily:n="Arial",fontWeight:i=400}=e;if(!t)return{width:0,height:0};let[,a]=mc(r),s=["sans-serif",n],l=t.split(We.lineBreakRegex),u=[],h=$e("body");if(!h.remove)return{width:0,height:0,lineHeight:0};let f=h.append("svg");for(let p of s){let m=0,g={width:0,height:0,lineHeight:0};for(let y of l){let v=iCe();v.text=y||K_;let x=aCe(f,v).style("font-size",a).style("font-weight",i).style("font-family",p),b=(x._groups||x)[0][0].getBBox();if(b.width===0&&b.height===0)throw new Error("svg element not in render tree");g.width=Math.round(Math.max(g.width,b.width)),m=Math.round(b.height),g.height+=m,g.lineHeight=Math.round(Math.max(g.lineHeight,m))}u.push(g)}f.remove();let d=isNaN(u[1].height)||isNaN(u[1].width)||isNaN(u[1].lineHeight)||u[0].height>u[1].height&&u[0].width>u[1].width&&u[0].lineHeight>u[1].lineHeight?0:1;return u[d]},(t,e)=>`${t}${e.fontSize}${e.fontWeight}${e.fontFamily}`),j_=class{constructor(e=!1,r){this.count=0;this.count=r?r.length:0,this.next=e?()=>this.count++:()=>Date.now()}static{o(this,"InitIDGenerator")}},oCe=o(function(t){return m5=m5||document.createElement("div"),t=escape(t).replace(/%26/g,"&").replace(/%23/g,"#").replace(/%3B/g,";"),m5.innerHTML=t,unescape(m5.textContent)},"entityDecode");o(r9,"isDetailedError");lCe=o((t,e,r,n)=>{if(!n)return;let i=t.node()?.getBBox();i&&t.append("text").text(n).attr("x",i.x+i.width/2).attr("y",-r).attr("class",e)},"insertTitle"),mc=o(t=>{if(typeof t=="number")return[t,t+"px"];let e=parseInt(t??"",10);return Number.isNaN(e)?[void 0,void 0]:t===String(e)?[e,t+"px"]:[e,t]},"parseFontSize");o(Ts,"cleanAndMerge");Lt={assignWithDepth:On,wrapLabel:e9,calculateTextHeight:g5,calculateTextWidth:Cl,calculateTextDimensions:t9,cleanAndMerge:Ts,detectInit:j6e,detectDirective:kX,isSubstringInArray:K6e,interpolateToCurve:om,calcLabelPosition:eCe,calcCardinalityPosition:tCe,calcTerminalLabelPosition:rCe,formatUrl:Q6e,getStylesFromArray:lm,generateId:Z_,random:J_,runFunc:Z6e,entityDecode:oCe,insertTitle:lCe,parseFontSize:mc,InitIDGenerator:j_},SX=o(function(t){let e=t;return e=e.replace(/style.*:\S*#.*;/g,function(r){return r.substring(0,r.length-1)}),e=e.replace(/classDef.*:\S*#.*;/g,function(r){return r.substring(0,r.length-1)}),e=e.replace(/#\w+;/g,function(r){let n=r.substring(1,r.length-1);return/^\+?\d+$/.test(n)?"\uFB02\xB0\xB0"+n+"\xB6\xDF":"\uFB02\xB0"+n+"\xB6\xDF"}),e},"encodeEntities"),to=o(function(t){return t.replace(/fl°°/g,"").replace(/fl°/g,"&").replace(/¶ß/g,";")},"decodeEntities"),y5=o((t,e,{counter:r=0,prefix:n,suffix:i})=>`${n?`${n}_`:""}${t}_${e}_${r}${i?`_${i}`:""}`,"getEdgeId")});function Sl(t,e,r,n,i){if(!e[t].width)if(r)e[t].text=e9(e[t].text,i,n),e[t].textLines=e[t].text.split(We.lineBreakRegex).length,e[t].width=i,e[t].height=g5(e[t].text,n);else{let a=e[t].text.split(We.lineBreakRegex);e[t].textLines=a.length;let s=0;e[t].height=0,e[t].width=0;for(let l of a)e[t].width=Math.max(Cl(l,n),e[t].width),s=g5(l,n),e[t].height=e[t].height+s}}function RX(t,e,r,n,i){let a=new w5(i);a.data.widthLimit=r.data.widthLimit/Math.min(n9,n.length);for(let[s,l]of n.entries()){let u=0;l.image={width:0,height:0,Y:0},l.sprite&&(l.image.width=48,l.image.height=48,l.image.Y=u,u=l.image.Y+l.image.height);let h=l.wrap&&Nt.wrap,f=v5(Nt);if(f.fontSize=f.fontSize+2,f.fontWeight="bold",Sl("label",l,h,f,a.data.widthLimit),l.label.Y=u+8,u=l.label.Y+l.label.height,l.type&&l.type.text!==""){l.type.text="["+l.type.text+"]";let g=v5(Nt);Sl("type",l,h,g,a.data.widthLimit),l.type.Y=u+5,u=l.type.Y+l.type.height}if(l.descr&&l.descr.text!==""){let g=v5(Nt);g.fontSize=g.fontSize-2,Sl("descr",l,h,g,a.data.widthLimit),l.descr.Y=u+20,u=l.descr.Y+l.descr.height}if(s==0||s%n9===0){let g=r.data.startx+Nt.diagramMarginX,y=r.data.stopy+Nt.diagramMarginY+u;a.setData(g,g,y,y)}else{let g=a.data.stopx!==a.data.startx?a.data.stopx+Nt.diagramMarginX:a.data.startx,y=a.data.starty;a.setData(g,g,y,y)}a.name=l.alias;let d=i.db.getC4ShapeArray(l.alias),p=i.db.getC4ShapeKeys(l.alias);p.length>0&&DX(a,t,d,p),e=l.alias;let m=i.db.getBoundarys(e);m.length>0&&RX(t,e,a,m,i),l.alias!=="global"&&LX(t,l,a),r.data.stopy=Math.max(a.data.stopy+Nt.c4ShapeMargin,r.data.stopy),r.data.stopx=Math.max(a.data.stopx+Nt.c4ShapeMargin,r.data.stopx),x5=Math.max(x5,r.data.stopx),b5=Math.max(b5,r.data.stopy)}}var x5,b5,_X,n9,Nt,w5,i9,hv,v5,cCe,LX,DX,ks,AX,uCe,hCe,fCe,a9,NX=R(()=>{"use strict";Zt();AW();ut();VC();rr();lS();_t();cp();xr();Yn();x5=0,b5=0,_X=4,n9=2;U1.yy=hy;Nt={},w5=class{static{o(this,"Bounds")}constructor(e){this.name="",this.data={},this.data.startx=void 0,this.data.stopx=void 0,this.data.starty=void 0,this.data.stopy=void 0,this.data.widthLimit=void 0,this.nextData={},this.nextData.startx=void 0,this.nextData.stopx=void 0,this.nextData.starty=void 0,this.nextData.stopy=void 0,this.nextData.cnt=0,i9(e.db.getConfig())}setData(e,r,n,i){this.nextData.startx=this.data.startx=e,this.nextData.stopx=this.data.stopx=r,this.nextData.starty=this.data.starty=n,this.nextData.stopy=this.data.stopy=i}updateVal(e,r,n,i){e[r]===void 0?e[r]=n:e[r]=i(n,e[r])}insert(e){this.nextData.cnt=this.nextData.cnt+1;let r=this.nextData.startx===this.nextData.stopx?this.nextData.stopx+e.margin:this.nextData.stopx+e.margin*2,n=r+e.width,i=this.nextData.starty+e.margin*2,a=i+e.height;(r>=this.data.widthLimit||n>=this.data.widthLimit||this.nextData.cnt>_X)&&(r=this.nextData.startx+e.margin+Nt.nextLinePaddingX,i=this.nextData.stopy+e.margin*2,this.nextData.stopx=n=r+e.width,this.nextData.starty=this.nextData.stopy,this.nextData.stopy=a=i+e.height,this.nextData.cnt=1),e.x=r,e.y=i,this.updateVal(this.data,"startx",r,Math.min),this.updateVal(this.data,"starty",i,Math.min),this.updateVal(this.data,"stopx",n,Math.max),this.updateVal(this.data,"stopy",a,Math.max),this.updateVal(this.nextData,"startx",r,Math.min),this.updateVal(this.nextData,"starty",i,Math.min),this.updateVal(this.nextData,"stopx",n,Math.max),this.updateVal(this.nextData,"stopy",a,Math.max)}init(e){this.name="",this.data={startx:void 0,stopx:void 0,starty:void 0,stopy:void 0,widthLimit:void 0},this.nextData={startx:void 0,stopx:void 0,starty:void 0,stopy:void 0,cnt:0},i9(e.db.getConfig())}bumpLastMargin(e){this.data.stopx+=e,this.data.stopy+=e}},i9=o(function(t){On(Nt,t),t.fontFamily&&(Nt.personFontFamily=Nt.systemFontFamily=Nt.messageFontFamily=t.fontFamily),t.fontSize&&(Nt.personFontSize=Nt.systemFontSize=Nt.messageFontSize=t.fontSize),t.fontWeight&&(Nt.personFontWeight=Nt.systemFontWeight=Nt.messageFontWeight=t.fontWeight)},"setConf"),hv=o((t,e)=>({fontFamily:t[e+"FontFamily"],fontSize:t[e+"FontSize"],fontWeight:t[e+"FontWeight"]}),"c4ShapeFont"),v5=o(t=>({fontFamily:t.boundaryFontFamily,fontSize:t.boundaryFontSize,fontWeight:t.boundaryFontWeight}),"boundaryFont"),cCe=o(t=>({fontFamily:t.messageFontFamily,fontSize:t.messageFontSize,fontWeight:t.messageFontWeight}),"messageFont");o(Sl,"calcC4ShapeTextWH");LX=o(function(t,e,r){e.x=r.data.startx,e.y=r.data.starty,e.width=r.data.stopx-r.data.startx,e.height=r.data.stopy-r.data.starty,e.label.y=Nt.c4ShapeMargin-35;let n=e.wrap&&Nt.wrap,i=v5(Nt);i.fontSize=i.fontSize+2,i.fontWeight="bold";let a=Cl(e.label.text,i);Sl("label",e,n,i,a),Tl.drawBoundary(t,e,Nt)},"drawBoundary"),DX=o(function(t,e,r,n){let i=0;for(let a of n){i=0;let s=r[a],l=hv(Nt,s.typeC4Shape.text);switch(l.fontSize=l.fontSize-2,s.typeC4Shape.width=Cl("\xAB"+s.typeC4Shape.text+"\xBB",l),s.typeC4Shape.height=l.fontSize+2,s.typeC4Shape.Y=Nt.c4ShapePadding,i=s.typeC4Shape.Y+s.typeC4Shape.height-4,s.image={width:0,height:0,Y:0},s.typeC4Shape.text){case"person":case"external_person":s.image.width=48,s.image.height=48,s.image.Y=i,i=s.image.Y+s.image.height;break}s.sprite&&(s.image.width=48,s.image.height=48,s.image.Y=i,i=s.image.Y+s.image.height);let u=s.wrap&&Nt.wrap,h=Nt.width-Nt.c4ShapePadding*2,f=hv(Nt,s.typeC4Shape.text);if(f.fontSize=f.fontSize+2,f.fontWeight="bold",Sl("label",s,u,f,h),s.label.Y=i+8,i=s.label.Y+s.label.height,s.type&&s.type.text!==""){s.type.text="["+s.type.text+"]";let m=hv(Nt,s.typeC4Shape.text);Sl("type",s,u,m,h),s.type.Y=i+5,i=s.type.Y+s.type.height}else if(s.techn&&s.techn.text!==""){s.techn.text="["+s.techn.text+"]";let m=hv(Nt,s.techn.text);Sl("techn",s,u,m,h),s.techn.Y=i+5,i=s.techn.Y+s.techn.height}let d=i,p=s.label.width;if(s.descr&&s.descr.text!==""){let m=hv(Nt,s.typeC4Shape.text);Sl("descr",s,u,m,h),s.descr.Y=i+20,i=s.descr.Y+s.descr.height,p=Math.max(s.label.width,s.descr.width),d=i-s.descr.textLines*5}p=p+Nt.c4ShapePadding,s.width=Math.max(s.width||Nt.width,p,Nt.width),s.height=Math.max(s.height||Nt.height,d,Nt.height),s.margin=s.margin||Nt.c4ShapeMargin,t.insert(s),Tl.drawC4Shape(e,s,Nt)}t.bumpLastMargin(Nt.c4ShapeMargin)},"drawC4ShapeArray"),ks=class{static{o(this,"Point")}constructor(e,r){this.x=e,this.y=r}},AX=o(function(t,e){let r=t.x,n=t.y,i=e.x,a=e.y,s=r+t.width/2,l=n+t.height/2,u=Math.abs(r-i),h=Math.abs(n-a),f=h/u,d=t.height/t.width,p=null;return n==a&&ri?p=new ks(r,l):r==i&&na&&(p=new ks(s,n)),r>i&&n=f?p=new ks(r,l+f*t.width/2):p=new ks(s-u/h*t.height/2,n+t.height):r=f?p=new ks(r+t.width,l+f*t.width/2):p=new ks(s+u/h*t.height/2,n+t.height):ra?d>=f?p=new ks(r+t.width,l-f*t.width/2):p=new ks(s+t.height/2*u/h,n):r>i&&n>a&&(d>=f?p=new ks(r,l-t.width/2*f):p=new ks(s-t.height/2*u/h,n)),p},"getIntersectPoint"),uCe=o(function(t,e){let r={x:0,y:0};r.x=e.x+e.width/2,r.y=e.y+e.height/2;let n=AX(t,r);r.x=t.x+t.width/2,r.y=t.y+t.height/2;let i=AX(e,r);return{startPoint:n,endPoint:i}},"getIntersectPoints"),hCe=o(function(t,e,r,n){let i=0;for(let a of e){i=i+1;let s=a.wrap&&Nt.wrap,l=cCe(Nt);n.db.getC4Type()==="C4Dynamic"&&(a.label.text=i+": "+a.label.text);let h=Cl(a.label.text,l);Sl("label",a,s,l,h),a.techn&&a.techn.text!==""&&(h=Cl(a.techn.text,l),Sl("techn",a,s,l,h)),a.descr&&a.descr.text!==""&&(h=Cl(a.descr.text,l),Sl("descr",a,s,l,h));let f=r(a.from),d=r(a.to),p=uCe(f,d);a.startPoint=p.startPoint,a.endPoint=p.endPoint}Tl.drawRels(t,e,Nt)},"drawRels");o(RX,"drawInsideBoundary");fCe=o(function(t,e,r,n){Nt=de().c4;let i=de().securityLevel,a;i==="sandbox"&&(a=$e("#i"+e));let s=i==="sandbox"?$e(a.nodes()[0].contentDocument.body):$e("body"),l=n.db;n.db.setWrap(Nt.wrap),_X=l.getC4ShapeInRow(),n9=l.getC4BoundaryInRow(),V.debug(`C:${JSON.stringify(Nt,null,2)}`);let u=i==="sandbox"?s.select(`[id="${e}"]`):$e(`[id="${e}"]`);Tl.insertComputerIcon(u),Tl.insertDatabaseIcon(u),Tl.insertClockIcon(u);let h=new w5(n);h.setData(Nt.diagramMarginX,Nt.diagramMarginX,Nt.diagramMarginY,Nt.diagramMarginY),h.data.widthLimit=screen.availWidth,x5=Nt.diagramMarginX,b5=Nt.diagramMarginY;let f=n.db.getTitle(),d=n.db.getBoundarys("");RX(u,"",h,d,n),Tl.insertArrowHead(u),Tl.insertArrowEnd(u),Tl.insertArrowCrossHead(u),Tl.insertArrowFilledHead(u),hCe(u,n.db.getRels(),n.db.getC4Shape,n),h.data.stopx=x5,h.data.stopy=b5;let p=h.data,g=p.stopy-p.starty+2*Nt.diagramMarginY,v=p.stopx-p.startx+2*Nt.diagramMarginX;f&&u.append("text").text(f).attr("x",(p.stopx-p.startx)/2-4*Nt.diagramMarginX).attr("y",p.starty+Nt.diagramMarginY),Sr(u,g,v,Nt.useMaxWidth);let x=f?60:0;u.attr("viewBox",p.startx-Nt.diagramMarginX+" -"+(Nt.diagramMarginY+x)+" "+v+" "+(g+x)),V.debug("models:",p)},"draw"),a9={drawPersonOrSystemArray:DX,drawBoundary:LX,setConf:i9,draw:fCe}});var dCe,MX,IX=R(()=>{"use strict";dCe=o(t=>`.person {
+ stroke: ${t.personBorder};
+ fill: ${t.personBkg};
+ }
+`,"getStyles"),MX=dCe});var OX={};hr(OX,{diagram:()=>pCe});var pCe,PX=R(()=>{"use strict";VC();lS();NX();IX();pCe={parser:rz,db:hy,renderer:a9,styles:MX,init:o(({c4:t,wrap:e})=>{a9.setConf(t),hy.setWrap(e)},"init")}});function o9(t){let e=[];for(let r of t){let n=dv.get(r);n?.styles&&(e=[...e,...n.styles??[]].map(i=>i.trim())),n?.textStyles&&(e=[...e,...n.textStyles??[]].map(i=>i.trim()))}return e}var vCe,zX,cm,$h,Es,dv,Cu,l9,c9,T5,s9,Fo,k5,E5,C5,S5,xCe,bCe,wCe,TCe,kCe,ECe,CCe,u9,SCe,ACe,_Ce,GX,LCe,DCe,h9,$X,VX,RCe,UX,NCe,MCe,ICe,OCe,PCe,fv,HX,YX,BCe,FCe,WX,zCe,GCe,$Ce,VCe,UCe,qX,XX,HCe,YCe,WCe,qCe,XCe,jCe,A5,f9=R(()=>{"use strict";Zt();xr();_t();rr();ut();bi();vCe="flowchart-",zX=0,cm=de(),$h=new Map,Es=[],dv=new Map,Cu=[],l9=new Map,c9=new Map,T5=0,s9=!0,E5=[],C5=o(t=>We.sanitizeText(t,cm),"sanitizeText"),S5=o(function(t){for(let e of $h.values())if(e.id===t)return e.domId;return t},"lookUpDomId"),xCe=o(function(t,e,r,n,i,a,s={}){if(!t||t.trim().length===0)return;let l,u=$h.get(t);u===void 0&&(u={id:t,labelType:"text",domId:vCe+t+"-"+zX,styles:[],classes:[]},$h.set(t,u)),zX++,e!==void 0?(cm=de(),l=C5(e.text.trim()),u.labelType=e.type,l.startsWith('"')&&l.endsWith('"')&&(l=l.substring(1,l.length-1)),u.text=l):u.text===void 0&&(u.text=t),r!==void 0&&(u.type=r),n?.forEach(function(h){u.styles.push(h)}),i?.forEach(function(h){u.classes.push(h)}),a!==void 0&&(u.dir=a),u.props===void 0?u.props=s:s!==void 0&&Object.assign(u.props,s)},"addVertex"),bCe=o(function(t,e,r){let a={start:t,end:e,type:void 0,text:"",labelType:"text"};V.info("abc78 Got edge...",a);let s=r.text;if(s!==void 0&&(a.text=C5(s.text.trim()),a.text.startsWith('"')&&a.text.endsWith('"')&&(a.text=a.text.substring(1,a.text.length-1)),a.labelType=s.type),r!==void 0&&(a.type=r.type,a.stroke=r.stroke,a.length=r.length>10?10:r.length),Es.length<(cm.maxEdges??500))V.info("Pushing edge..."),Es.push(a);else throw new Error(`Edge limit exceeded. ${Es.length} edges found, but the limit is ${cm.maxEdges}.
+
+Initialize mermaid with maxEdges set to a higher number to allow more edges.
+You cannot set this config via configuration inside the diagram as it is a secure config.
+You have to call mermaid.initialize.`)},"addSingleLink"),wCe=o(function(t,e,r){V.info("addLink",t,e,r);for(let n of t)for(let i of e)bCe(n,i,r)},"addLink"),TCe=o(function(t,e){t.forEach(function(r){r==="default"?Es.defaultInterpolate=e:Es[r].interpolate=e})},"updateLinkInterpolate"),kCe=o(function(t,e){t.forEach(function(r){if(typeof r=="number"&&r>=Es.length)throw new Error(`The index ${r} for linkStyle is out of bounds. Valid indices for linkStyle are between 0 and ${Es.length-1}. (Help: Ensure that the index is within the range of existing edges.)`);r==="default"?Es.defaultStyle=e:(Es[r].style=e,(Es[r]?.style?.length??0)>0&&!Es[r]?.style?.some(n=>n?.startsWith("fill"))&&Es[r]?.style?.push("fill:none"))})},"updateLink"),ECe=o(function(t,e){t.split(",").forEach(function(r){let n=dv.get(r);n===void 0&&(n={id:r,styles:[],textStyles:[]},dv.set(r,n)),e?.forEach(function(i){if(/color/.exec(i)){let a=i.replace("fill","bgFill");n.textStyles.push(a)}n.styles.push(i)})})},"addClass"),CCe=o(function(t){Fo=t,/.*/.exec(Fo)&&(Fo="LR"),/.*v/.exec(Fo)&&(Fo="TB"),Fo==="TD"&&(Fo="TB")},"setDirection"),u9=o(function(t,e){for(let r of t.split(",")){let n=$h.get(r);n&&n.classes.push(e);let i=l9.get(r);i&&i.classes.push(e)}},"setClass"),SCe=o(function(t,e){if(e!==void 0){e=C5(e);for(let r of t.split(","))c9.set(k5==="gen-1"?S5(r):r,e)}},"setTooltip"),ACe=o(function(t,e,r){let n=S5(t);if(de().securityLevel!=="loose"||e===void 0)return;let i=[];if(typeof r=="string"){i=r.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/);for(let s=0;s
+`:"'+(n?a:ro(a,!0))+`
+`}blockquote({tokens:e}){return`"+(n?a:ro(a,!0))+`
+${this.parser.parse(e)}
+`}html({text:e}){return e}heading({tokens:e,depth:r}){return`
+`}list(e){let r=e.ordered,n=e.start,i="";for(let l=0;l
+
+`+r+`
+`+i+`
+`}tablerow({text:e}){return`
+${e}
+`}tablecell(e){let r=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+r+`${n}>
+`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${e}
`}br(e){return"
"}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:r,tokens:n}){let i=this.parser.parseInline(n),a=ZX(e);if(a===null)return i;e=a;let s='"+i+"",s}image({href:e,title:r,text:n}){let i=ZX(e);if(i===null)return n;e=i;let a=`",a}text(e){return"tokens"in e&&e.tokens?this.parser.parseInline(e.tokens):e.text}},yv=class{static{o(this,"_TextRenderer")}strong({text:e}){return e}em({text:e}){return e}codespan({text:e}){return e}del({text:e}){return e}html({text:e}){return e}text({text:e}){return e}link({text:e}){return""+e}image({text:e}){return""+e}br(){return""}},Au=class t{static{o(this,"_Parser")}options;renderer;textRenderer;constructor(e){this.options=e||Sd,this.options.renderer=this.options.renderer||new fm,this.renderer=this.options.renderer,this.renderer.options=this.options,this.renderer.parser=this,this.textRenderer=new yv}static parse(e,r){return new t(r).parse(e)}static parseInline(e,r){return new t(r).parseInline(e)}parse(e,r=!0){let n="";for(let i=0;i
"+ro(n.message+"",!0)+"
";return r?Promise.resolve(i):i}if(r)return Promise.reject(n);throw n}}},Cd=new p9;o(jr,"marked");jr.options=jr.setOptions=function(t){return Cd.setOptions(t),jr.defaults=Cd.defaults,rj(jr.defaults),jr};jr.getDefaults=m9;jr.defaults=Sd;jr.use=function(...t){return Cd.use(...t),jr.defaults=Cd.defaults,rj(jr.defaults),jr};jr.walkTokens=function(t,e){return Cd.walkTokens(t,e)};jr.parseInline=Cd.parseInline;jr.Parser=Au;jr.parser=Au.parse;jr.Renderer=fm;jr.TextRenderer=yv;jr.Lexer=Su;jr.lexer=Su.lex;jr.Tokenizer=hm;jr.Hooks=um;jr.parse=jr;mkt=jr.options,gkt=jr.setOptions,ykt=jr.use,vkt=jr.walkTokens,xkt=jr.parseInline,bkt=Au.parse,wkt=Su.lex});function R7e(t,{markdownAutoWrap:e}){let n=t.replace(/
/g,`
+`).replace(/\n{2,}/g,`
+`),i=Gb(n);return e===!1?i.replace(/ /g," "):i}function dj(t,e={}){let r=R7e(t,e),n=jr.lexer(r),i=[[]],a=0;function s(l,u="normal"){l.type==="text"?l.text.split(`
+`).forEach((f,d)=>{d!==0&&(a++,i.push([])),f.split(" ").forEach(p=>{p&&i[a].push({content:p,type:u})})}):l.type==="strong"||l.type==="em"?l.tokens.forEach(h=>{s(h,l.type)}):l.type==="html"&&i[a].push({content:l.text,type:"normal"})}return o(s,"processNode"),n.forEach(l=>{l.type==="paragraph"?l.tokens?.forEach(u=>{s(u)}):l.type==="html"&&i[a].push({content:l.text,type:"normal"})}),i}function pj(t,{markdownAutoWrap:e}={}){let r=jr.lexer(t);function n(i){return i.type==="text"?e===!1?i.text.replace(/\n */g,"
").replace(/ /g," "):i.text.replace(/\n */g,"
"):i.type==="strong"?`${i.tokens?.map(n).join("")}`:i.type==="em"?`${i.tokens?.map(n).join("")}`:i.type==="paragraph"?`
/g,"
"),d=dj(f.replace("
","
"),h),p=P7e(l,t,d,e?u:!1);if(s){/stroke:/.exec(r)&&(r=r.replace("stroke:","lineColor:"));let m=r.replace(/stroke:[^;]+;?/g,"").replace(/stroke-width:[^;]+;?/g,"").replace(/fill:[^;]+;?/g,"").replace(/color:/g,"fill:");$e(p).attr("style",m)}else{let m=r.replace(/stroke:[^;]+;?/g,"").replace(/stroke-width:[^;]+;?/g,"").replace(/fill:[^;]+;?/g,"").replace(/background:/g,"fill:");$e(p).select("rect").attr("style",m.replace(/background:/g,"fill:"));let g=r.replace(/stroke:[^;]+;?/g,"").replace(/stroke-width:[^;]+;?/g,"").replace(/fill:[^;]+;?/g,"").replace(/color:/g,"fill:");$e(p).select("text").attr("style",g)}return p}},"createText")});function wj(t,e){e&&t.attr("style",e)}function B7e(t){let e=$e(document.createElementNS("http://www.w3.org/2000/svg","foreignObject")),r=e.append("xhtml:div"),n=t.label,i=t.isNode?"nodeLabel":"edgeLabel",a=r.append("span");return a.html(n),wj(a,t.labelStyle),a.attr("class",i),wj(r,t.labelStyle),r.style("display","inline-block"),r.style("white-space","nowrap"),r.attr("xmlns","http://www.w3.org/1999/xhtml"),e.node()}var F7e,ra,bv=R(()=>{"use strict";Zt();ut();_t();rr();xr();Al();o(wj,"applyStyle");o(B7e,"addHtmlLabel");F7e=o((t,e,r,n)=>{let i=t||"";if(typeof i=="object"&&(i=i[0]),yr(de().flowchart.htmlLabels)){i=i.replace(/\\n|\n/g,"
"),V.debug("vertexText"+i);let a={isNode:n,label:E9(to(i)),labelStyle:e.replace("fill:","color:")};return B7e(a)}else{let a=document.createElementNS("http://www.w3.org/2000/svg","text");a.setAttribute("style",e.replace("color:","fill:"));let s=[];typeof i=="string"?s=i.split(/\\n|\n|
/gi):Array.isArray(i)?s=i:s=[];for(let l of s){let u=document.createElementNS("http://www.w3.org/2000/svg","tspan");u.setAttributeNS("http://www.w3.org/XML/1998/namespace","xml:space","preserve"),u.setAttribute("dy","1em"),u.setAttribute("x","0"),r?u.setAttribute("class","title-row"):u.setAttribute("class","row"),u.textContent=l.trim(),a.appendChild(u)}return a}},"createLabel"),ra=F7e});function z7e(t,e){return t.intersect(e)}var Tj,kj=R(()=>{"use strict";o(z7e,"intersectNode");Tj=z7e});function G7e(t,e,r,n){var i=t.x,a=t.y,s=i-n.x,l=a-n.y,u=Math.sqrt(e*e*l*l+r*r*s*s),h=Math.abs(e*r*s/u);n.x{"use strict";o(G7e,"intersectEllipse");R5=G7e});function $7e(t,e,r){return R5(t,e,e,r)}var Ej,Cj=R(()=>{"use strict";C9();o($7e,"intersectCircle");Ej=$7e});function V7e(t,e,r,n){var i,a,s,l,u,h,f,d,p,m,g,y,v,x,b;if(i=e.y-t.y,s=t.x-e.x,u=e.x*t.y-t.x*e.y,p=i*r.x+s*r.y+u,m=i*n.x+s*n.y+u,!(p!==0&&m!==0&&Sj(p,m))&&(a=n.y-r.y,l=r.x-n.x,h=n.x*r.y-r.x*n.y,f=a*t.x+l*t.y+h,d=a*e.x+l*e.y+h,!(f!==0&&d!==0&&Sj(f,d))&&(g=i*l-a*s,g!==0)))return y=Math.abs(g/2),v=s*h-l*u,x=v<0?(v-y)/g:(v+y)/g,v=a*u-i*h,b=v<0?(v-y)/g:(v+y)/g,{x,y:b}}function Sj(t,e){return t*e>0}var Aj,_j=R(()=>{"use strict";o(V7e,"intersectLine");o(Sj,"sameSign");Aj=V7e});function U7e(t,e,r){var n=t.x,i=t.y,a=[],s=Number.POSITIVE_INFINITY,l=Number.POSITIVE_INFINITY;typeof e.forEach=="function"?e.forEach(function(g){s=Math.min(s,g.x),l=Math.min(l,g.y)}):(s=Math.min(s,e.x),l=Math.min(l,e.y));for(var u=n-t.width/2-s,h=i-t.height/2-l,f=0;f]*>/g,"").trim()==="";await Promise.all([...v].map(b=>new Promise(w=>{function S(){if(b.style.display="flex",b.style.flexDirection="column",x){let T=i.fontSize?i.fontSize:window.getComputedStyle(document.body).fontSize,_=parseInt(T,10)*5+"px";b.style.minWidth=_,b.style.maxWidth=_}else b.style.width="100%";w(b)}o(S,"setupImage"),setTimeout(()=>{b.complete&&S()}),b.addEventListener("error",S),b.addEventListener("load",S)})))}p=g.getBoundingClientRect(),y.attr("width",p.width),y.attr("height",p.height)}return s?u.attr("transform","translate("+-p.width/2+", "+-p.height/2+")"):u.attr("transform","translate(0, "+-p.height/2+")"),e.centerLabel&&u.attr("transform","translate("+-p.width/2+", "+-p.height/2+")"),u.insert("rect",":first-child"),{shapeSvg:l,bbox:p,halfPadding:m,label:u}},"labelHelper"),kn=o((t,e)=>{let r=e.node().getBBox();t.width=r.width,t.height=r.height},"updateNodeBounds");o(_l,"insertPolygonShape")});var Y7e,Rj,Nj=R(()=>{"use strict";N5();ut();_t();A9();Y7e=o(async(t,e)=>{e.useHtmlLabels||de().flowchart.htmlLabels||(e.centerLabel=!0);let{shapeSvg:n,bbox:i,halfPadding:a}=await Ti(t,e,"node "+e.classes,!0);V.info("Classes = ",e.classes);let s=n.insert("rect",":first-child");return s.attr("rx",e.rx).attr("ry",e.ry).attr("x",-i.width/2-a).attr("y",-i.height/2-a).attr("width",i.width+e.padding).attr("height",i.height+e.padding),kn(e,s),e.intersect=function(l){return Tn.rect(e,l)},n},"note"),Rj=Y7e});function _9(t,e,r,n){let i=[],a=o(l=>{i.push(l,0)},"addBorder"),s=o(l=>{i.push(0,l)},"skipBorder");e.includes("t")?(V.debug("add top border"),a(r)):s(r),e.includes("r")?(V.debug("add right border"),a(n)):s(n),e.includes("b")?(V.debug("add bottom border"),a(r)):s(r),e.includes("l")?(V.debug("add left border"),a(n)):s(n),t.attr("stroke-dasharray",i.join(" "))}var Mj,no,Ij,W7e,q7e,X7e,j7e,K7e,Q7e,Z7e,J7e,eSe,tSe,rSe,nSe,iSe,aSe,sSe,oSe,lSe,cSe,uSe,Oj,hSe,fSe,Pj,dm,pm,Bj,Fj,wv,M5=R(()=>{"use strict";Zt();_t();rr();ut();KX();bv();A9();Nj();N5();Mj=o(t=>t?" "+t:"","formatClass"),no=o((t,e)=>`${e||"node default"}${Mj(t.classes)} ${Mj(t.class)}`,"getClassesFromNode"),Ij=o(async(t,e)=>{let{shapeSvg:r,bbox:n}=await Ti(t,e,no(e,void 0),!0),i=n.width+e.padding,a=n.height+e.padding,s=i+a,l=[{x:s/2,y:0},{x:s,y:-s/2},{x:s/2,y:-s},{x:0,y:-s/2}];V.info("Question main (Circle)");let u=_l(r,s,s,l);return u.attr("style",e.style),kn(e,u),e.intersect=function(h){return V.warn("Intersect called"),Tn.polygon(e,l,h)},r},"question"),W7e=o((t,e)=>{let r=t.insert("g").attr("class","node default").attr("id",e.domId||e.id),n=28,i=[{x:0,y:n/2},{x:n/2,y:0},{x:0,y:-n/2},{x:-n/2,y:0}];return r.insert("polygon",":first-child").attr("points",i.map(function(s){return s.x+","+s.y}).join(" ")).attr("class","state-start").attr("r",7).attr("width",28).attr("height",28),e.width=28,e.height=28,e.intersect=function(s){return Tn.circle(e,14,s)},r},"choice"),q7e=o(async(t,e)=>{let{shapeSvg:r,bbox:n}=await Ti(t,e,no(e,void 0),!0),i=4,a=n.height+e.padding,s=a/i,l=n.width+2*s+e.padding,u=[{x:s,y:0},{x:l-s,y:0},{x:l,y:-a/2},{x:l-s,y:-a},{x:s,y:-a},{x:0,y:-a/2}],h=_l(r,l,a,u);return h.attr("style",e.style),kn(e,h),e.intersect=function(f){return Tn.polygon(e,u,f)},r},"hexagon"),X7e=o(async(t,e)=>{let{shapeSvg:r,bbox:n}=await Ti(t,e,void 0,!0),i=2,a=n.height+2*e.padding,s=a/i,l=n.width+2*s+e.padding,u=jX(e.directions,n,e),h=_l(r,l,a,u);return h.attr("style",e.style),kn(e,h),e.intersect=function(f){return Tn.polygon(e,u,f)},r},"block_arrow"),j7e=o(async(t,e)=>{let{shapeSvg:r,bbox:n}=await Ti(t,e,no(e,void 0),!0),i=n.width+e.padding,a=n.height+e.padding,s=[{x:-a/2,y:0},{x:i,y:0},{x:i,y:-a},{x:-a/2,y:-a},{x:0,y:-a/2}];return _l(r,i,a,s).attr("style",e.style),e.width=i+a,e.height=a,e.intersect=function(u){return Tn.polygon(e,s,u)},r},"rect_left_inv_arrow"),K7e=o(async(t,e)=>{let{shapeSvg:r,bbox:n}=await Ti(t,e,no(e),!0),i=n.width+e.padding,a=n.height+e.padding,s=[{x:-2*a/6,y:0},{x:i-a/6,y:0},{x:i+2*a/6,y:-a},{x:a/6,y:-a}],l=_l(r,i,a,s);return l.attr("style",e.style),kn(e,l),e.intersect=function(u){return Tn.polygon(e,s,u)},r},"lean_right"),Q7e=o(async(t,e)=>{let{shapeSvg:r,bbox:n}=await Ti(t,e,no(e,void 0),!0),i=n.width+e.padding,a=n.height+e.padding,s=[{x:2*a/6,y:0},{x:i+a/6,y:0},{x:i-2*a/6,y:-a},{x:-a/6,y:-a}],l=_l(r,i,a,s);return l.attr("style",e.style),kn(e,l),e.intersect=function(u){return Tn.polygon(e,s,u)},r},"lean_left"),Z7e=o(async(t,e)=>{let{shapeSvg:r,bbox:n}=await Ti(t,e,no(e,void 0),!0),i=n.width+e.padding,a=n.height+e.padding,s=[{x:-2*a/6,y:0},{x:i+2*a/6,y:0},{x:i-a/6,y:-a},{x:a/6,y:-a}],l=_l(r,i,a,s);return l.attr("style",e.style),kn(e,l),e.intersect=function(u){return Tn.polygon(e,s,u)},r},"trapezoid"),J7e=o(async(t,e)=>{let{shapeSvg:r,bbox:n}=await Ti(t,e,no(e,void 0),!0),i=n.width+e.padding,a=n.height+e.padding,s=[{x:a/6,y:0},{x:i-a/6,y:0},{x:i+2*a/6,y:-a},{x:-2*a/6,y:-a}],l=_l(r,i,a,s);return l.attr("style",e.style),kn(e,l),e.intersect=function(u){return Tn.polygon(e,s,u)},r},"inv_trapezoid"),eSe=o(async(t,e)=>{let{shapeSvg:r,bbox:n}=await Ti(t,e,no(e,void 0),!0),i=n.width+e.padding,a=n.height+e.padding,s=[{x:0,y:0},{x:i+a/2,y:0},{x:i,y:-a/2},{x:i+a/2,y:-a},{x:0,y:-a}],l=_l(r,i,a,s);return l.attr("style",e.style),kn(e,l),e.intersect=function(u){return Tn.polygon(e,s,u)},r},"rect_right_inv_arrow"),tSe=o(async(t,e)=>{let{shapeSvg:r,bbox:n}=await Ti(t,e,no(e,void 0),!0),i=n.width+e.padding,a=i/2,s=a/(2.5+i/50),l=n.height+s+e.padding,u="M 0,"+s+" a "+a+","+s+" 0,0,0 "+i+" 0 a "+a+","+s+" 0,0,0 "+-i+" 0 l 0,"+l+" a "+a+","+s+" 0,0,0 "+i+" 0 l 0,"+-l,h=r.attr("label-offset-y",s).insert("path",":first-child").attr("style",e.style).attr("d",u).attr("transform","translate("+-i/2+","+-(l/2+s)+")");return kn(e,h),e.intersect=function(f){let d=Tn.rect(e,f),p=d.x-e.x;if(a!=0&&(Math.abs(p)
"):d,e.labelStyle,!0,!0));if(yr(de().flowchart.htmlLabels)){let y=m.children[0],v=$e(m);f=y.getBoundingClientRect(),v.attr("width",f.width),v.attr("height",f.height)}let g=e.padding/2;return $e(m).attr("transform","translate( "+(f.width>p.width?0:(p.width-f.width)/2)+", "+(p.height+g+5)+")"),$e(h).attr("transform","translate( "+(f.width
"),V.info("vertexText"+i);let a={isNode:n,label:to(i).replace(/fa[blrs]?:fa-[\w-]+/g,l=>``),labelStyle:e&&e.replace("fill:","color:")};return await kSe(a)}else{let a=document.createElementNS("http://www.w3.org/2000/svg","text");a.setAttribute("style",e.replace("color:","fill:"));let s=[];typeof i=="string"?s=i.split(/\\n|\n|
/gi):Array.isArray(i)?s=i:s=[];for(let l of s){let u=document.createElementNS("http://www.w3.org/2000/svg","tspan");u.setAttributeNS("http://www.w3.org/XML/1998/namespace","xml:space","preserve"),u.setAttribute("dy","1em"),u.setAttribute("x","0"),r?u.setAttribute("class","title-row"):u.setAttribute("class","row"),u.textContent=l.trim(),a.appendChild(u)}return a}},"createLabel"),gc=ESe});var _u,Sv=R(()=>{"use strict";_u=o((t,e,r,n,i)=>["M",t+i,e,"H",t+r-i,"A",i,i,0,0,1,t+r,e+i,"V",e+n-i,"A",i,i,0,0,1,t+r-i,e+n,"H",t+i,"A",i,i,0,0,1,t,e+n-i,"V",e+i,"A",i,i,0,0,1,t+i,e,"Z"].join(" "),"createRoundedRectPathD")});var Lu,Jj,CSe,Br,Fr,ki=R(()=>{"use strict";_t();Lu=o(t=>{let{handDrawnSeed:e}=de();return{fill:t,hachureAngle:120,hachureGap:4,fillWeight:2,roughness:.7,stroke:t,seed:e}},"solidStateFill"),Jj=o(t=>{let e=CSe([...t.cssCompiledStyles||[],...t.cssStyles||[]]);return{stylesMap:e,stylesArray:[...e]}},"compileStyles"),CSe=o(t=>{let e=new Map;return t.forEach(r=>{let[n,i]=r.split(":");e.set(n.trim(),i?.trim())}),e},"styles2Map"),Br=o(t=>{let{stylesArray:e}=Jj(t),r=[],n=[],i=[],a=[];return e.forEach(s=>{let l=s[0];l==="color"||l==="font-size"||l==="font-family"||l==="font-weight"||l==="font-style"||l==="text-decoration"||l==="text-align"||l==="text-transform"||l==="line-height"||l==="letter-spacing"||l==="word-spacing"||l==="text-shadow"||l==="text-overflow"||l==="white-space"||l==="word-wrap"||l==="word-break"||l==="overflow-wrap"||l==="hyphens"?r.push(s.join(":")+" !important"):(n.push(s.join(":")+" !important"),l.includes("stroke")&&i.push(s.join(":")+" !important"),l==="fill"&&a.push(s.join(":")+" !important"))}),{labelStyles:r.join(";"),nodeStyles:n.join(";"),stylesArray:e,borderStyles:i,backgroundStyles:a}},"styles2String"),Fr=o((t,e)=>{let{themeVariables:r,handDrawnSeed:n}=de(),{nodeBorder:i,mainBkg:a}=r,{stylesMap:s}=Jj(t);return Object.assign({roughness:.7,fill:s.get("fill")||a,fillStyle:"hachure",fillWeight:4,stroke:s.get("stroke")||i,seed:n,strokeWidth:1.3},e)},"userNodeOverrides")});var eK,SSe,ASe,_Se,LSe,DSe,tK,Y5,rK,X9=R(()=>{"use strict";_t();rr();ut();_d();Zt();ti();Al();q9();H5();Sv();ki();eK=o(async(t,e)=>{V.info("Creating subgraph rect for ",e.id,e);let r=de(),{themeVariables:n,handDrawnSeed:i}=r,{clusterBkg:a,clusterBorder:s}=n,{labelStyles:l,nodeStyles:u,borderStyles:h,backgroundStyles:f}=Br(e),d=t.insert("g").attr("class","cluster "+e.cssClasses).attr("id",e.id).attr("data-look",e.look),p=yr(r.flowchart.htmlLabels),m=d.insert("g").attr("class","cluster-label "),g=await ta(m,e.label,{style:e.labelStyle,useHtmlLabels:p,isNode:!0}),y=g.getBBox();if(yr(r.flowchart.htmlLabels)){let _=g.children[0],A=$e(g);y=_.getBoundingClientRect(),A.attr("width",y.width),A.attr("height",y.height)}let v=e.width<=y.width+e.padding?y.width+e.padding:e.width;e.width<=y.width+e.padding?e.diff=(v-e.width)/2-e.padding:e.diff=-e.padding;let x=e.height,b=e.x-v/2,w=e.y-x/2;V.trace("Data ",e,JSON.stringify(e));let S;if(e.look==="handDrawn"){let _=Jt.svg(d),A=Fr(e,{roughness:.7,fill:a,stroke:s,fillWeight:3,seed:i}),L=_.path(_u(b,w,v,x,0),A);S=d.insert(()=>(V.debug("Rough node insert CXC",L),L),":first-child"),S.select("path:nth-child(2)").attr("style",h.join(";")),S.select("path").attr("style",f.join(";").replace("fill","stroke"))}else S=d.insert("rect",":first-child"),S.attr("style",u).attr("rx",e.rx).attr("ry",e.ry).attr("x",b).attr("y",w).attr("width",v).attr("height",x);let{subGraphTitleTopMargin:T}=io(r);if(m.attr("transform",`translate(${e.x-y.width/2}, ${e.y-e.height/2+T})`),l){let _=m.select("span");_&&_.attr("style",l)}let E=S.node().getBBox();return e.offsetX=0,e.width=E.width,e.height=E.height,e.offsetY=y.height-e.padding/2,e.intersect=function(_){return Dd(e,_)},{cluster:d,labelBBox:y}},"rect"),SSe=o((t,e)=>{let r=t.insert("g").attr("class","note-cluster").attr("id",e.id),n=r.insert("rect",":first-child"),i=0*e.padding,a=i/2;n.attr("rx",e.rx).attr("ry",e.ry).attr("x",e.x-e.width/2-a).attr("y",e.y-e.height/2-a).attr("width",e.width+i).attr("height",e.height+i).attr("fill","none");let s=n.node().getBBox();return e.width=s.width,e.height=s.height,e.intersect=function(l){return Dd(e,l)},{cluster:r,labelBBox:{width:0,height:0}}},"noteGroup"),ASe=o(async(t,e)=>{let r=de(),{themeVariables:n,handDrawnSeed:i}=r,{altBackground:a,compositeBackground:s,compositeTitleBackground:l,nodeBorder:u}=n,h=t.insert("g").attr("class",e.cssClasses).attr("id",e.id).attr("data-id",e.id).attr("data-look",e.look),f=h.insert("g",":first-child"),d=h.insert("g").attr("class","cluster-label"),p=h.append("rect"),m=d.node().appendChild(await gc(e.label,e.labelStyle,void 0,!0)),g=m.getBBox();if(yr(r.flowchart.htmlLabels)){let L=m.children[0],M=$e(m);g=L.getBoundingClientRect(),M.attr("width",g.width),M.attr("height",g.height)}let y=0*e.padding,v=y/2,x=(e.width<=g.width+e.padding?g.width+e.padding:e.width)+y;e.width<=g.width+e.padding?e.diff=(x-e.width)/2-e.padding:e.diff=-e.padding;let b=e.height+y,w=e.height+y-g.height-6,S=e.x-x/2,T=e.y-b/2;e.width=x;let E=e.y-e.height/2-v+g.height+2,_;if(e.look==="handDrawn"){let L=e.cssClasses.includes("statediagram-cluster-alt"),M=Jt.svg(h),N=e.rx||e.ry?M.path(_u(S,T,x,b,10),{roughness:.7,fill:l,fillStyle:"solid",stroke:u,seed:i}):M.rectangle(S,T,x,b,{seed:i});_=h.insert(()=>N,":first-child");let k=M.rectangle(S,E,x,w,{fill:L?a:s,fillStyle:L?"hachure":"solid",stroke:u,seed:i});_=h.insert(()=>N,":first-child"),p=h.insert(()=>k)}else _=f.insert("rect",":first-child"),_.attr("class","outer").attr("x",S).attr("y",T).attr("width",x).attr("height",b).attr("data-look",e.look),p.attr("class","inner").attr("x",S).attr("y",E).attr("width",x).attr("height",w);d.attr("transform",`translate(${e.x-g.width/2}, ${T+1-(yr(r.flowchart.htmlLabels)?0:3)})`);let A=_.node().getBBox();return e.height=A.height,e.offsetX=0,e.offsetY=g.height-e.padding/2,e.labelBBox=g,e.intersect=function(L){return Dd(e,L)},{cluster:h,labelBBox:g}},"roundedWithTitle"),_Se=o((t,e)=>{let r=de(),{themeVariables:n,handDrawnSeed:i}=r,{nodeBorder:a}=n,s=t.insert("g").attr("class",e.cssClasses).attr("id",e.id).attr("data-look",e.look),l=s.insert("g",":first-child"),u=0*e.padding,h=e.width+u;e.diff=-e.padding;let f=e.height+u,d=e.x-h/2,p=e.y-f/2;e.width=h;let m;if(e.look==="handDrawn"){let v=Jt.svg(s).rectangle(d,p,h,f,{fill:"lightgrey",roughness:.5,strokeLineDash:[5],stroke:a,seed:i});m=s.insert(()=>v,":first-child")}else m=l.insert("rect",":first-child"),m.attr("class","divider").attr("x",d).attr("y",p).attr("width",h).attr("height",f).attr("data-look",e.look);let g=m.node().getBBox();return e.height=g.height,e.offsetX=0,e.offsetY=0,e.intersect=function(y){return Dd(e,y)},{cluster:s,labelBBox:{}}},"divider"),LSe=eK,DSe={rect:eK,squareRect:LSe,roundedWithTitle:ASe,noteGroup:SSe,divider:_Se},tK=new Map,Y5=o(async(t,e)=>{let r=e.shape||"rect",n=await DSe[r](t,e);return tK.set(e.id,n),n},"insertCluster"),rK=o(()=>{tK=new Map},"clear")});function W5(t,e){if(t===void 0||e===void 0)return{angle:0,deltaX:0,deltaY:0};t=q5(t),e=q5(e);let[r,n]=[t.x,t.y],[i,a]=[e.x,e.y],s=i-r,l=a-n;return{angle:Math.atan(l/s),deltaX:s,deltaY:l}}var Uh,q5,X5,j9=R(()=>{"use strict";Uh={aggregation:18,extension:18,composition:18,dependency:6,lollipop:13.5,arrow_point:4};o(W5,"calculateDeltaAndAngle");q5=o(t=>Array.isArray(t)?{x:t[0],y:t[1]}:t,"pointTransformer"),X5=o(t=>({x:o(function(e,r,n){let i=0;if(r===0&&Object.hasOwn(Uh,t.arrowTypeStart)){let{angle:a,deltaX:s}=W5(n[0],n[1]);i=Uh[t.arrowTypeStart]*Math.cos(a)*(s>=0?1:-1)}else if(r===n.length-1&&Object.hasOwn(Uh,t.arrowTypeEnd)){let{angle:a,deltaX:s}=W5(n[n.length-1],n[n.length-2]);i=Uh[t.arrowTypeEnd]*Math.cos(a)*(s>=0?1:-1)}return q5(e).x+i},"x"),y:o(function(e,r,n){let i=0;if(r===0&&Object.hasOwn(Uh,t.arrowTypeStart)){let{angle:a,deltaY:s}=W5(n[0],n[1]);i=Uh[t.arrowTypeStart]*Math.abs(Math.sin(a))*(s>=0?1:-1)}else if(r===n.length-1&&Object.hasOwn(Uh,t.arrowTypeEnd)){let{angle:a,deltaY:s}=W5(n[n.length-1],n[n.length-2]);i=Uh[t.arrowTypeEnd]*Math.abs(Math.sin(a))*(s>=0?1:-1)}return q5(e).y+i},"y")}),"getLineFunctionsWithOffset")});var iK,RSe,nK,aK=R(()=>{"use strict";ut();iK=o((t,e,r,n,i)=>{e.arrowTypeStart&&nK(t,"start",e.arrowTypeStart,r,n,i),e.arrowTypeEnd&&nK(t,"end",e.arrowTypeEnd,r,n,i)},"addEdgeMarkers"),RSe={arrow_cross:"cross",arrow_point:"point",arrow_barb:"barb",arrow_circle:"circle",aggregation:"aggregation",extension:"extension",composition:"composition",dependency:"dependency",lollipop:"lollipop"},nK=o((t,e,r,n,i,a)=>{let s=RSe[r];if(!s){V.warn(`Unknown arrow type: ${r}`);return}let l=e==="start"?"Start":"End";t.attr(`marker-${e}`,`url(${n}#${i}_${a}-${s}${l})`)},"addEdgeMarker")});function j5(t,e){de().flowchart.htmlLabels&&t&&(t.style.width=e.length*9+"px",t.style.height="12px")}function ISe(t){let e=[],r=[];for(let n=1;n]*>/g,"").trim()==="";await Promise.all([...m].map(y=>new Promise(v=>{function x(){if(y.style.display="flex",y.style.flexDirection="column",g){let b=de().fontSize?de().fontSize:window.getComputedStyle(document.body).fontSize,S=parseInt(b,10)*5+"px";y.style.minWidth=S,y.style.maxWidth=S}else y.style.width="100%";v(y)}o(x,"setupImage"),setTimeout(()=>{y.complete&&x()}),y.addEventListener("error",x),y.addEventListener("load",x)})))}h=d.getBoundingClientRect(),p.attr("width",h.width),p.attr("height",h.height)}return i?s.attr("transform","translate("+-h.width/2+", "+-h.height/2+")"):s.attr("transform","translate(0, "+-h.height/2+")"),e.centerLabel&&s.attr("transform","translate("+-h.width/2+", "+-h.height/2+")"),s.insert("rect",":first-child"),{shapeSvg:a,bbox:h,halfPadding:f,label:s}},"labelHelper"),ar=o((t,e)=>{let r=e.node().getBBox();t.width=r.width,t.height=r.height},"updateNodeBounds"),En=o((t,e)=>(t.look==="handDrawn"?"rough-node":"node")+" "+t.cssClasses+" "+(e||""),"getNodeClasses")});function qSe(t,e){return t.intersect(e)}var cK,uK=R(()=>{"use strict";o(qSe,"intersectNode");cK=qSe});function XSe(t,e,r,n){var i=t.x,a=t.y,s=i-n.x,l=a-n.y,u=Math.sqrt(e*e*l*l+r*r*s*s),h=Math.abs(e*r*s/u);n.x{"use strict";o(XSe,"intersectEllipse");tw=XSe});function jSe(t,e,r){return tw(t,e,e,r)}var hK,fK=R(()=>{"use strict";Z9();o(jSe,"intersectCircle");hK=jSe});function KSe(t,e,r,n){var i,a,s,l,u,h,f,d,p,m,g,y,v,x,b;if(i=e.y-t.y,s=t.x-e.x,u=e.x*t.y-t.x*e.y,p=i*r.x+s*r.y+u,m=i*n.x+s*n.y+u,!(p!==0&&m!==0&&dK(p,m))&&(a=n.y-r.y,l=r.x-n.x,h=n.x*r.y-r.x*n.y,f=a*t.x+l*t.y+h,d=a*e.x+l*e.y+h,!(f!==0&&d!==0&&dK(f,d))&&(g=i*l-a*s,g!==0)))return y=Math.abs(g/2),v=s*h-l*u,x=v<0?(v-y)/g:(v+y)/g,v=a*u-i*h,b=v<0?(v-y)/g:(v+y)/g,{x,y:b}}function dK(t,e){return t*e>0}var pK,mK=R(()=>{"use strict";o(KSe,"intersectLine");o(dK,"sameSign");pK=KSe});function QSe(t,e,r){let n=t.x,i=t.y,a=[],s=Number.POSITIVE_INFINITY,l=Number.POSITIVE_INFINITY;typeof e.forEach=="function"?e.forEach(function(f){s=Math.min(s,f.x),l=Math.min(l,f.y)}):(s=Math.min(s,e.x),l=Math.min(l,e.y));let u=n-t.width/2-s,h=i-t.height/2-l;for(let f=0;f
"):p,e.labelStyle,!0,!0)),y=g.children[0],v=$e(g);d=y.getBoundingClientRect(),v.attr("width",d.width),v.attr("height",d.height);let x=(e.padding||0)/2;$e(g).attr("transform","translate( "+(d.width>m.width?0:(m.width-d.width)/2)+", "+(m.height+x+5)+")"),$e(f).attr("transform","translate( "+(d.width0&&r(l)?e>1?VQ(l,e-1,r,n,i):Em(i,l):n||(i[i.length]=l)}return i}var bc,Cm=R(()=>{"use strict";hw();$Q();o(VQ,"baseFlatten");bc=VQ});function r8e(t){var e=t==null?0:t.length;return e?bc(t,1):[]}var Gr,fw=R(()=>{"use strict";Cm();o(r8e,"flatten");Gr=r8e});function n8e(t){return d5(f5(t,void 0,Gr),t+"")}var UQ,HQ=R(()=>{"use strict";fw();H_();W_();o(n8e,"flatRest");UQ=n8e});function i8e(t,e,r){var n=-1,i=t.length;e<0&&(e=-e>i?0:i+e),r=r>i?i:r,r<0&&(r+=i),i=e>r?0:r-e>>>0,e>>>=0;for(var a=Array(i);++n{"use strict";o(i8e,"baseSlice");dw=i8e});function d8e(t){return f8e.test(t)}var a8e,s8e,o8e,l8e,c8e,u8e,h8e,f8e,YQ,WQ=R(()=>{"use strict";a8e="\\ud800-\\udfff",s8e="\\u0300-\\u036f",o8e="\\ufe20-\\ufe2f",l8e="\\u20d0-\\u20ff",c8e=s8e+o8e+l8e,u8e="\\ufe0e\\ufe0f",h8e="\\u200d",f8e=RegExp("["+h8e+a8e+c8e+u8e+"]");o(d8e,"hasUnicode");YQ=d8e});function p8e(t,e,r,n){var i=-1,a=t==null?0:t.length;for(n&&a&&(r=t[++i]);++i{"use strict";o(p8e,"arrayReduce");qQ=p8e});function m8e(t,e){return t&&Bo(e,Dr(e),t)}var jQ,KQ=R(()=>{"use strict";kd();vc();o(m8e,"baseAssign");jQ=m8e});function g8e(t,e){return t&&Bo(e,bs(e),t)}var QQ,ZQ=R(()=>{"use strict";kd();zh();o(g8e,"baseAssignIn");QQ=g8e});function y8e(t,e){for(var r=-1,n=t==null?0:t.length,i=0,a=[];++r=l)return u;var h=r[n];return u*(h=="desc"?-1:1)}}return t.index-e.index}var bee,wee=R(()=>{"use strict";xee();o(sDe,"compareMultiple");bee=sDe});function oDe(t,e,r){e.length?e=Ss(e,function(a){return wt(a)?function(s){return Yh(s,a.length===1?a[0]:a)}:a}):e=[ea];var n=-1;e=Ss(e,Oo(cn));var i=Uw(t,function(a,s,l){var u=Ss(e,function(h){return h(a)});return{criteria:u,index:++n,value:a}});return gee(i,function(a,s){return bee(a,s,r)})}var Tee,kee=R(()=>{"use strict";Md();Dv();Qa();DL();yee();Td();wee();Eu();Bn();o(oDe,"baseOrderBy");Tee=oDe});var lDe,Eee,Cee=R(()=>{"use strict";SL();lDe=Pw("length"),Eee=lDe});function bDe(t){for(var e=See.lastIndex=0;See.test(t);)++e;return e}var Aee,cDe,uDe,hDe,fDe,dDe,pDe,zL,GL,mDe,_ee,Lee,Dee,gDe,Ree,Nee,yDe,vDe,xDe,See,Mee,Iee=R(()=>{"use strict";Aee="\\ud800-\\udfff",cDe="\\u0300-\\u036f",uDe="\\ufe20-\\ufe2f",hDe="\\u20d0-\\u20ff",fDe=cDe+uDe+hDe,dDe="\\ufe0e\\ufe0f",pDe="["+Aee+"]",zL="["+fDe+"]",GL="\\ud83c[\\udffb-\\udfff]",mDe="(?:"+zL+"|"+GL+")",_ee="[^"+Aee+"]",Lee="(?:\\ud83c[\\udde6-\\uddff]){2}",Dee="[\\ud800-\\udbff][\\udc00-\\udfff]",gDe="\\u200d",Ree=mDe+"?",Nee="["+dDe+"]?",yDe="(?:"+gDe+"(?:"+[_ee,Lee,Dee].join("|")+")"+Nee+Ree+")*",vDe=Nee+Ree+yDe,xDe="(?:"+[_ee+zL+"?",zL,Lee,Dee,pDe].join("|")+")",See=RegExp(GL+"(?="+GL+")|"+xDe+vDe,"g");o(bDe,"unicodeSize");Mee=bDe});function wDe(t){return YQ(t)?Mee(t):Eee(t)}var Oee,Pee=R(()=>{"use strict";Cee();WQ();Iee();o(wDe,"stringSize");Oee=wDe});function TDe(t,e){return jw(t,e,function(r,n){return Ow(t,n)})}var Bee,Fee=R(()=>{"use strict";FL();CL();o(TDe,"basePick");Bee=TDe});var kDe,Fd,zee=R(()=>{"use strict";Fee();HQ();kDe=UQ(function(t,e){return t==null?{}:Bee(t,e)}),Fd=kDe});function SDe(t,e,r,n){for(var i=-1,a=CDe(EDe((e-t)/(r||1)),0),s=Array(a);a--;)s[n?a:++i]=t,t+=r;return s}var EDe,CDe,Gee,$ee=R(()=>{"use strict";EDe=Math.ceil,CDe=Math.max;o(SDe,"baseRange");Gee=SDe});function ADe(t){return function(e,r,n){return n&&typeof n!="number"&&eo(e,r,n)&&(r=n=void 0),e=vm(e),r===void 0?(r=e,e=0):r=vm(r),n=n===void 0?e{"use strict";Pt();Ec();o(Ste,"addBorderSegments");o(Cte,"addBorderNode")});function Lte(t){var e=t.graph().rankdir.toLowerCase();(e==="lr"||e==="rl")&&Rte(t)}function Dte(t){var e=t.graph().rankdir.toLowerCase();(e==="bt"||e==="rl")&&rRe(t),(e==="lr"||e==="rl")&&(nRe(t),Rte(t))}function Rte(t){Ee(t.nodes(),function(e){_te(t.node(e))}),Ee(t.edges(),function(e){_te(t.edge(e))})}function _te(t){var e=t.width;t.width=t.height,t.height=e}function rRe(t){Ee(t.nodes(),function(e){QL(t.node(e))}),Ee(t.edges(),function(e){var r=t.edge(e);Ee(r.points,QL),Xe(r,"y")&&QL(r)})}function QL(t){t.y=-t.y}function nRe(t){Ee(t.nodes(),function(e){ZL(t.node(e))}),Ee(t.edges(),function(e){var r=t.edge(e);Ee(r.points,ZL),Xe(r,"x")&&ZL(r)})}function ZL(t){var e=t.x;t.x=t.y,t.y=e}var Nte=R(()=>{"use strict";Pt();o(Lte,"adjust");o(Dte,"undo");o(Rte,"swapWidthHeight");o(_te,"swapWidthHeightOne");o(rRe,"reverseY");o(QL,"reverseYOne");o(nRe,"swapXY");o(ZL,"swapXYOne")});function Mte(t){t.graph().dummyChains=[],Ee(t.edges(),function(e){aRe(t,e)})}function aRe(t,e){var r=e.v,n=t.node(r).rank,i=e.w,a=t.node(i).rank,s=e.name,l=t.edge(e),u=l.labelRank;if(a!==n+1){t.removeEdge(e);var h,f,d;for(d=0,++n;n{"use strict";Pt();Ec();o(Mte,"run");o(aRe,"normalizeEdge");o(Ite,"undo")});function Iv(t){var e={};function r(n){var i=t.node(n);if(Xe(e,n))return i.rank;e[n]=!0;var a=Ll(qe(t.outEdges(n),function(s){return r(s.w)-t.edge(s).minlen}));return(a===Number.POSITIVE_INFINITY||a===void 0||a===null)&&(a=0),i.rank=a}o(r,"dfs"),Ee(t.sources(),r)}function $d(t,e){return t.node(e.w).rank-t.node(e.v).rank-t.edge(e).minlen}var tT=R(()=>{"use strict";Pt();o(Iv,"longestPath");o($d,"slack")});function rT(t){var e=new lr({directed:!1}),r=t.nodes()[0],n=t.nodeCount();e.setNode(r,{});for(var i,a;sRe(e,t)h)&&Sre(r,p,f)})})}o(n,"scan");function i(a,s){var l=-1,u,h=0;return Ee(s,function(f,d){if(t.node(f).dummy==="border"){var p=t.predecessors(f);p.length&&(u=t.node(p[0]).order,n(s,h,d,l,u),h=d,l=u)}n(s,h,s.length,u,a.length)}),s}return o(i,"visitLayer"),Vr(e,i),r}function MRe(t,e){if(t.node(e).dummy)return Za(t.predecessors(e),function(r){return t.node(r).dummy})}function Sre(t,e,r){if(e>r){var n=e;e=r,r=n}var i=t[e];i||(t[e]=i={}),i[r]=!0}function IRe(t,e,r){if(e>r){var n=e;e=r,r=n}return Xe(t[e],r)}function ORe(t,e,r,n){var i={},a={},s={};return Ee(e,function(l){Ee(l,function(u,h){i[u]=u,a[u]=u,s[u]=h})}),Ee(e,function(l){var u=-1;Ee(l,function(h){var f=n(h);if(f.length){f=Tc(f,function(y){return s[y]});for(var d=(f.length-1)/2,p=Math.floor(d),m=Math.ceil(d);p<=m;++p){var g=f[p];a[h]===h&&u{"use strict";Pt();ya();Ec();o(RRe,"findType1Conflicts");o(NRe,"findType2Conflicts");o(MRe,"findOtherInnerSegmentNode");o(Sre,"addConflict");o(IRe,"hasConflict");o(ORe,"verticalAlignment");o(PRe,"horizontalCompaction");o(BRe,"buildBlockGraph");o(FRe,"findSmallestWidthAlignment");o(zRe,"alignCoordinates");o(GRe,"balance");o(Are,"positionX");o($Re,"sep");o(VRe,"width")});function Lre(t){t=eT(t),URe(t),ML(Are(t),function(e,r){t.node(r).x=e})}function URe(t){var e=Qh(t),r=t.graph().ranksep,n=0;Ee(e,function(i){var a=_s(qe(i,function(s){return t.node(s).height}));Ee(i,function(s){t.node(s).y=n+a/2}),n+=a+r})}var Dre=R(()=>{"use strict";Pt();Ec();_re();o(Lre,"position");o(URe,"positionY")});function lo(t,e){var r=e&&e.debugTiming?kte:Ete;r("layout",function(){var n=r(" buildLayoutGraph",function(){return eNe(t)});r(" runLayout",function(){HRe(n,r)}),r(" updateInputGraph",function(){YRe(t,n)})})}function HRe(t,e){e(" makeSpaceForEdgeLabels",function(){tNe(t)}),e(" removeSelfEdges",function(){uNe(t)}),e(" acyclic",function(){gte(t)}),e(" nestingGraph.run",function(){ere(t)}),e(" rank",function(){hD(eT(t))}),e(" injectEdgeLabelProxies",function(){rNe(t)}),e(" removeEmptyRanks",function(){wte(t)}),e(" nestingGraph.cleanup",function(){rre(t)}),e(" normalizeRanks",function(){bte(t)}),e(" assignRankMinMax",function(){nNe(t)}),e(" removeEdgeLabelProxies",function(){iNe(t)}),e(" normalize.run",function(){Mte(t)}),e(" parentDummyChains",function(){Ere(t)}),e(" addBorderSegments",function(){Ste(t)}),e(" order",function(){Tre(t)}),e(" insertSelfEdges",function(){hNe(t)}),e(" adjustCoordinateSystem",function(){Lte(t)}),e(" position",function(){Lre(t)}),e(" positionSelfEdges",function(){fNe(t)}),e(" removeBorderNodes",function(){cNe(t)}),e(" normalize.undo",function(){Ite(t)}),e(" fixupEdgeLabelCoords",function(){oNe(t)}),e(" undoCoordinateSystem",function(){Dte(t)}),e(" translateGraph",function(){aNe(t)}),e(" assignNodeIntersects",function(){sNe(t)}),e(" reversePoints",function(){lNe(t)}),e(" acyclic.undo",function(){yte(t)})}function YRe(t,e){Ee(t.nodes(),function(r){var n=t.node(r),i=e.node(r);n&&(n.x=i.x,n.y=i.y,e.children(r).length&&(n.width=i.width,n.height=i.height))}),Ee(t.edges(),function(r){var n=t.edge(r),i=e.edge(r);n.points=i.points,Xe(i,"x")&&(n.x=i.x,n.y=i.y)}),t.graph().width=e.graph().width,t.graph().height=e.graph().height}function eNe(t){var e=new lr({multigraph:!0,compound:!0}),r=mD(t.graph());return e.setGraph(Gh({},qRe,pD(r,WRe),Fd(r,XRe))),Ee(t.nodes(),function(n){var i=mD(t.node(n));e.setNode(n,Xh(pD(i,jRe),KRe)),e.setParent(n,t.parent(n))}),Ee(t.edges(),function(n){var i=mD(t.edge(n));e.setEdge(n,Gh({},ZRe,pD(i,QRe),Fd(i,JRe)))}),e}function tNe(t){var e=t.graph();e.ranksep/=2,Ee(t.edges(),function(r){var n=t.edge(r);n.minlen*=2,n.labelpos.toLowerCase()!=="c"&&(e.rankdir==="TB"||e.rankdir==="BT"?n.width+=n.labeloffset:n.height+=n.labeloffset)})}function rNe(t){Ee(t.edges(),function(e){var r=t.edge(e);if(r.width&&r.height){var n=t.node(e.v),i=t.node(e.w),a={rank:(i.rank-n.rank)/2+n.rank,e};kc(t,"edge-proxy",a,"_ep")}})}function nNe(t){var e=0;Ee(t.nodes(),function(r){var n=t.node(r);n.borderTop&&(n.minRank=t.node(n.borderTop).rank,n.maxRank=t.node(n.borderBottom).rank,e=_s(e,n.maxRank))}),t.graph().maxRank=e}function iNe(t){Ee(t.nodes(),function(e){var r=t.node(e);r.dummy==="edge-proxy"&&(t.edge(r.e).labelRank=r.rank,t.removeNode(e))})}function aNe(t){var e=Number.POSITIVE_INFINITY,r=0,n=Number.POSITIVE_INFINITY,i=0,a=t.graph(),s=a.marginx||0,l=a.marginy||0;function u(h){var f=h.x,d=h.y,p=h.width,m=h.height;e=Math.min(e,f-p/2),r=Math.max(r,f+p/2),n=Math.min(n,d-m/2),i=Math.max(i,d+m/2)}o(u,"getExtremes"),Ee(t.nodes(),function(h){u(t.node(h))}),Ee(t.edges(),function(h){var f=t.edge(h);Xe(f,"x")&&u(f)}),e-=s,n-=l,Ee(t.nodes(),function(h){var f=t.node(h);f.x-=e,f.y-=n}),Ee(t.edges(),function(h){var f=t.edge(h);Ee(f.points,function(d){d.x-=e,d.y-=n}),Xe(f,"x")&&(f.x-=e),Xe(f,"y")&&(f.y-=n)}),a.width=r-e+s,a.height=i-n+l}function sNe(t){Ee(t.edges(),function(e){var r=t.edge(e),n=t.node(e.v),i=t.node(e.w),a,s;r.points?(a=r.points[0],s=r.points[r.points.length-1]):(r.points=[],a=i,s=n),r.points.unshift(XL(n,a)),r.points.push(XL(i,s))})}function oNe(t){Ee(t.edges(),function(e){var r=t.edge(e);if(Xe(r,"x"))switch((r.labelpos==="l"||r.labelpos==="r")&&(r.width-=r.labeloffset),r.labelpos){case"l":r.x-=r.width/2+r.labeloffset;break;case"r":r.x+=r.width/2+r.labeloffset;break}})}function lNe(t){Ee(t.edges(),function(e){var r=t.edge(e);r.reversed&&r.points.reverse()})}function cNe(t){Ee(t.nodes(),function(e){if(t.children(e).length){var r=t.node(e),n=t.node(r.borderTop),i=t.node(r.borderBottom),a=t.node(ma(r.borderLeft)),s=t.node(ma(r.borderRight));r.width=Math.abs(s.x-a.x),r.height=Math.abs(i.y-n.y),r.x=a.x+r.width/2,r.y=n.y+r.height/2}}),Ee(t.nodes(),function(e){t.node(e).dummy==="border"&&t.removeNode(e)})}function uNe(t){Ee(t.edges(),function(e){if(e.v===e.w){var r=t.node(e.v);r.selfEdges||(r.selfEdges=[]),r.selfEdges.push({e,label:t.edge(e)}),t.removeEdge(e)}})}function hNe(t){var e=Qh(t);Ee(e,function(r){var n=0;Ee(r,function(i,a){var s=t.node(i);s.order=a+n,Ee(s.selfEdges,function(l){kc(t,"selfedge",{width:l.label.width,height:l.label.height,rank:s.rank,order:a+ ++n,e:l.e,label:l.label},"_se")}),delete s.selfEdges})})}function fNe(t){Ee(t.nodes(),function(e){var r=t.node(e);if(r.dummy==="selfedge"){var n=t.node(r.e.v),i=n.x+n.width/2,a=n.y,s=r.x-i,l=n.height/2;t.setEdge(r.e,r.label),t.removeNode(e),r.label.points=[{x:i+2*s/3,y:a-l},{x:i+5*s/6,y:a-l},{x:i+s,y:a},{x:i+5*s/6,y:a+l},{x:i+2*s/3,y:a+l}],r.label.x=r.x,r.label.y=r.y}})}function pD(t,e){return Pd(Fd(t,e),Number)}function mD(t){var e={};return Ee(t,function(r,n){e[n.toLowerCase()]=r}),e}var WRe,qRe,XRe,jRe,KRe,QRe,ZRe,JRe,Rre=R(()=>{"use strict";Pt();ya();Ate();Nte();qL();JL();fD();nre();kre();Cre();Dre();Ec();o(lo,"layout");o(HRe,"runLayout");o(YRe,"updateInputGraph");WRe=["nodesep","edgesep","ranksep","marginx","marginy"],qRe={ranksep:50,edgesep:20,nodesep:50,rankdir:"tb"},XRe=["acyclicer","ranker","rankdir","align"],jRe=["width","height"],KRe={width:0,height:0},QRe=["minlen","weight","width","height","labeloffset"],ZRe={minlen:1,weight:1,width:0,height:0,labeloffset:10,labelpos:"r"},JRe=["labelpos"];o(eNe,"buildLayoutGraph");o(tNe,"makeSpaceForEdgeLabels");o(rNe,"injectEdgeLabelProxies");o(nNe,"assignRankMinMax");o(iNe,"removeEdgeLabelProxies");o(aNe,"translateGraph");o(sNe,"assignNodeIntersects");o(oNe,"fixupEdgeLabelCoords");o(lNe,"reversePointsForReversedEdges");o(cNe,"removeBorderNodes");o(uNe,"removeSelfEdges");o(hNe,"insertSelfEdges");o(fNe,"positionSelfEdges");o(pD,"selectNumberAttrs");o(mD,"canonicalize")});var Vd=R(()=>{"use strict";qL();Rre();JL();fD()});function zn(t){var e={options:{directed:t.isDirected(),multigraph:t.isMultigraph(),compound:t.isCompound()},nodes:dNe(t),edges:pNe(t)};return er(t.graph())||(e.value=Qr(t.graph())),e}function dNe(t){return qe(t.nodes(),function(e){var r=t.node(e),n=t.parent(e),i={v:e};return er(r)||(i.value=r),er(n)||(i.parent=n),i})}function pNe(t){return qe(t.edges(),function(e){var r=t.edge(e),n={v:e.v,w:e.w};return er(e.name)||(n.name=e.name),er(r)||(n.value=r),n})}var Pv=R(()=>{"use strict";Pt();Zw();o(zn,"write");o(dNe,"writeNodes");o(pNe,"writeEdges")});var cr,Ud,Mre,Ire,aT,mNe,Ore,Pre,gNe,Bm,Nre,Bre,Fre,zre,Gre,$re=R(()=>{"use strict";ut();ya();Pv();cr=new Map,Ud=new Map,Mre=new Map,Ire=o(()=>{Ud.clear(),Mre.clear(),cr.clear()},"clear"),aT=o((t,e)=>{let r=Ud.get(e)||[];return V.trace("In isDescendant",e," ",t," = ",r.includes(t)),r.includes(t)},"isDescendant"),mNe=o((t,e)=>{let r=Ud.get(e)||[];return V.info("Descendants of ",e," is ",r),V.info("Edge is ",t),t.v===e||t.w===e?!1:r?r.includes(t.v)||aT(t.v,e)||aT(t.w,e)||r.includes(t.w):(V.debug("Tilt, ",e,",not in descendants"),!1)},"edgeInCluster"),Ore=o((t,e,r,n)=>{V.warn("Copying children of ",t,"root",n,"data",e.node(t),n);let i=e.children(t)||[];t!==n&&i.push(t),V.warn("Copying (nodes) clusterId",t,"nodes",i),i.forEach(a=>{if(e.children(a).length>0)Ore(a,e,r,n);else{let s=e.node(a);V.info("cp ",a," to ",n," with parent ",t),r.setNode(a,s),n!==e.parent(a)&&(V.warn("Setting parent",a,e.parent(a)),r.setParent(a,e.parent(a))),t!==n&&a!==t?(V.debug("Setting parent",a,t),r.setParent(a,t)):(V.info("In copy ",t,"root",n,"data",e.node(t),n),V.debug("Not Setting parent for node=",a,"cluster!==rootId",t!==n,"node!==clusterId",a!==t));let l=e.edges(a);V.debug("Copying Edges",l),l.forEach(u=>{V.info("Edge",u);let h=e.edge(u.v,u.w,u.name);V.info("Edge data",h,n);try{mNe(u,n)?(V.info("Copying as ",u.v,u.w,h,u.name),r.setEdge(u.v,u.w,h,u.name),V.info("newGraph edges ",r.edges(),r.edge(r.edges()[0]))):V.info("Skipping copy of edge ",u.v,"-->",u.w," rootId: ",n," clusterId:",t)}catch(f){V.error(f)}})}V.debug("Removing node",a),e.removeNode(a)})},"copy"),Pre=o((t,e)=>{let r=e.children(t),n=[...r];for(let i of r)Mre.set(i,t),n=[...n,...Pre(i,e)];return n},"extractDescendants"),gNe=o((t,e,r)=>{let n=t.edges().filter(u=>u.v===e||u.w===e),i=t.edges().filter(u=>u.v===r||u.w===r),a=n.map(u=>({v:u.v===e?r:u.v,w:u.w===e?e:u.w})),s=i.map(u=>({v:u.v,w:u.w}));return a.filter(u=>s.some(h=>u.v===h.v&&u.w===h.w))},"findCommonEdges"),Bm=o((t,e,r)=>{let n=e.children(t);if(V.trace("Searching children of id ",t,n),n.length<1)return t;let i;for(let a of n){let s=Bm(a,e,r),l=gNe(e,r,s);if(s)if(l.length>0)i=s;else return s}return i},"findNonClusterChild"),Nre=o(t=>!cr.has(t)||!cr.get(t).externalConnections?t:cr.has(t)?cr.get(t).id:t,"getAnchorId"),Bre=o((t,e)=>{if(!t||e>10){V.debug("Opting out, no graph ");return}else V.debug("Opting in, graph ");t.nodes().forEach(function(r){t.children(r).length>0&&(V.warn("Cluster identified",r," Replacement id in edges: ",Bm(r,t,r)),Ud.set(r,Pre(r,t)),cr.set(r,{id:Bm(r,t,r),clusterData:t.node(r)}))}),t.nodes().forEach(function(r){let n=t.children(r),i=t.edges();n.length>0?(V.debug("Cluster identified",r,Ud),i.forEach(a=>{let s=aT(a.v,r),l=aT(a.w,r);s^l&&(V.warn("Edge: ",a," leaves cluster ",r),V.warn("Descendants of XXX ",r,": ",Ud.get(r)),cr.get(r).externalConnections=!0)})):V.debug("Not a cluster ",r,Ud)});for(let r of cr.keys()){let n=cr.get(r).id,i=t.parent(n);i!==r&&cr.has(i)&&!cr.get(i).externalConnections&&(cr.get(r).id=i)}t.edges().forEach(function(r){let n=t.edge(r);V.warn("Edge "+r.v+" -> "+r.w+": "+JSON.stringify(r)),V.warn("Edge "+r.v+" -> "+r.w+": "+JSON.stringify(t.edge(r)));let i=r.v,a=r.w;if(V.warn("Fix XXX",cr,"ids:",r.v,r.w,"Translating: ",cr.get(r.v)," --- ",cr.get(r.w)),cr.get(r.v)||cr.get(r.w)){if(V.warn("Fixing and trying - removing XXX",r.v,r.w,r.name),i=Nre(r.v),a=Nre(r.w),t.removeEdge(r.v,r.w,r.name),i!==r.v){let s=t.parent(i);cr.get(s).externalConnections=!0,n.fromCluster=r.v}if(a!==r.w){let s=t.parent(a);cr.get(s).externalConnections=!0,n.toCluster=r.w}V.warn("Fix Replacing with XXX",i,a,r.name),t.setEdge(i,a,n,r.name)}}),V.warn("Adjusted Graph",zn(t)),Fre(t,0),V.trace(cr)},"adjustClustersAndEdges"),Fre=o((t,e)=>{if(V.warn("extractor - ",e,zn(t),t.children("D")),e>10){V.error("Bailing out");return}let r=t.nodes(),n=!1;for(let i of r){let a=t.children(i);n=n||a.length>0}if(!n){V.debug("Done, no node has children",t.nodes());return}V.debug("Nodes = ",r,e);for(let i of r)if(V.debug("Extracting node",i,cr,cr.has(i)&&!cr.get(i).externalConnections,!t.parent(i),t.node(i),t.children("D")," Depth ",e),!cr.has(i))V.debug("Not a cluster",i,e);else if(!cr.get(i).externalConnections&&t.children(i)&&t.children(i).length>0){V.warn("Cluster without external connections, without a parent and with children",i,e);let s=t.graph().rankdir==="TB"?"LR":"TB";cr.get(i)?.clusterData?.dir&&(s=cr.get(i).clusterData.dir,V.warn("Fixing dir",cr.get(i).clusterData.dir,s));let l=new lr({multigraph:!0,compound:!0}).setGraph({rankdir:s,nodesep:50,ranksep:50,marginx:8,marginy:8}).setDefaultEdgeLabel(function(){return{}});V.warn("Old graph before copy",zn(t)),Ore(i,t,l,i),t.setNode(i,{clusterNode:!0,id:i,clusterData:cr.get(i).clusterData,label:cr.get(i).label,graph:l}),V.warn("New graph after copy node: (",i,")",zn(l)),V.debug("Old graph after copy",zn(t))}else V.warn("Cluster ** ",i," **not meeting the criteria !externalConnections:",!cr.get(i).externalConnections," no parent: ",!t.parent(i)," children ",t.children(i)&&t.children(i).length>0,t.children("D"),e),V.debug(cr);r=t.nodes(),V.warn("New list of nodes",r);for(let i of r){let a=t.node(i);V.warn(" Now next level",i,a),a?.clusterNode&&Fre(a.graph,e+1)}},"extractor"),zre=o((t,e)=>{if(e.length===0)return[];let r=Object.assign([],e);return e.forEach(n=>{let i=t.children(n),a=zre(t,i);r=[...r,...a]}),r},"sorter"),Gre=o(t=>zre(t,t.children()),"sortNodesByHierarchy")});var Ure={};hr(Ure,{render:()=>yNe});var Vre,yNe,Hre=R(()=>{"use strict";Vd();Pv();ya();Q9();ri();$re();tL();X9();K9();ut();_d();_t();Vre=o(async(t,e,r,n,i,a)=>{V.warn("Graph in recursive render:XAX",zn(e),i);let s=e.graph().rankdir;V.trace("Dir in recursive render - dir:",s);let l=t.insert("g").attr("class","root");e.nodes()?V.info("Recursive render XXX",e.nodes()):V.info("No nodes found for",e),e.edges().length>0&&V.info("Recursive edges",e.edge(e.edges()[0]));let u=l.insert("g").attr("class","clusters"),h=l.insert("g").attr("class","edgePaths"),f=l.insert("g").attr("class","edgeLabels"),d=l.insert("g").attr("class","nodes");await Promise.all(e.nodes().map(async function(y){let v=e.node(y);if(i!==void 0){let x=JSON.parse(JSON.stringify(i.clusterData));V.trace(`Setting data for parent cluster XXX
+ Node.id = `,y,`
+ data=`,x.height,`
+Parent cluster`,i.height),e.setNode(i.id,x),e.parent(y)||(V.trace("Setting parent",y,i.id),e.setParent(y,i.id,x))}if(V.info("(Insert) Node XXX"+y+": "+JSON.stringify(e.node(y))),v?.clusterNode){V.info("Cluster identified XBX",y,v.width,e.node(y));let{ranksep:x,nodesep:b}=e.graph();v.graph.setGraph({...v.graph.graph(),ranksep:x+25,nodesep:b});let w=await Vre(d,v.graph,r,n,e.node(y),a),S=w.elem;ar(v,S),v.diff=w.diff||0,V.info("New compound node after recursive render XAX",y,"width",v.width,"height",v.height),lQ(S,v)}else e.children(y).length>0?(V.trace("Cluster - the non recursive path XBX",y,v.id,v,v.width,"Graph:",e),V.trace(Bm(v.id,e)),cr.set(v.id,{id:Bm(v.id,e),node:v})):(V.trace("Node - the non recursive path XAX",y,d,e.node(y),s),await rw(d,e.node(y),s))})),await o(async()=>{let y=e.edges().map(async function(v){let x=e.edge(v.v,v.w,v.name);V.info("Edge "+v.v+" -> "+v.w+": "+JSON.stringify(v)),V.info("Edge "+v.v+" -> "+v.w+": ",v," ",JSON.stringify(e.edge(v))),V.info("Fix",cr,"ids:",v.v,v.w,"Translating: ",cr.get(v.v),cr.get(v.w)),await Q5(f,x)});await Promise.all(y)},"processEdges")(),V.info("Graph before layout:",JSON.stringify(zn(e))),V.info("############################################# XXX"),V.info("### Layout ### XXX"),V.info("############################################# XXX"),lo(e),V.info("Graph after layout:",JSON.stringify(zn(e)));let m=0,{subGraphTitleTotalMargin:g}=io(a);return await Promise.all(Gre(e).map(async function(y){let v=e.node(y);if(V.info("Position XBX => "+y+": ("+v.x,","+v.y,") width: ",v.width," height: ",v.height),v?.clusterNode)v.y+=g,V.info("A tainted cluster node XBX1",y,v.id,v.width,v.height,v.x,v.y,e.parent(y)),cr.get(v.id).node=v,eL(v);else if(e.children(y).length>0){V.info("A pure cluster node XBX1",y,v.id,v.x,v.y,v.width,v.height,e.parent(y)),v.height+=g,e.node(v.parentId);let x=v?.padding/2||0,b=v?.labelBBox?.height||0,w=b-x||0;V.debug("OffsetY",w,"labelHeight",b,"halfPadding",x),await Y5(u,v),cr.get(v.id).node=v}else{let x=e.node(v.parentId);v.y+=g/2,V.info("A regular node XBX1 - using the padding",v.id,"parent",v.parentId,v.width,v.height,v.x,v.y,"offsetY",v.offsetY,"parent",x,x?.offsetY,v),eL(v)}})),e.edges().forEach(function(y){let v=e.edge(y);V.info("Edge "+y.v+" -> "+y.w+": "+JSON.stringify(v),v),v.points.forEach(S=>S.y+=g/2);let x=e.node(y.v);var b=e.node(y.w);let w=J5(h,v,cr,r,x,b,n);Z5(v,w)}),e.nodes().forEach(function(y){let v=e.node(y);V.info(y,v.type,v.diff),v.isGroup&&(m=v.diff)}),V.warn("Returning from recursive render XAX",l,m),{elem:l,diff:m}},"recursiveRender"),yNe=o(async(t,e)=>{let r=new lr({multigraph:!0,compound:!0}).setGraph({rankdir:t.direction,nodesep:t.config?.nodeSpacing||t.config?.flowchart?.nodeSpacing||t.nodeSpacing,ranksep:t.config?.rankSpacing||t.config?.flowchart?.rankSpacing||t.rankSpacing,marginx:8,marginy:8}).setDefaultEdgeLabel(function(){return{}}),n=e.select("g");ew(n,t.markers,t.type,t.diagramId),cQ(),lK(),rK(),Ire(),t.nodes.forEach(a=>{r.setNode(a.id,{...a}),a.parentId&&r.setParent(a.id,a.parentId)}),V.debug("Edges:",t.edges),t.edges.forEach(a=>{if(a.start===a.end){let s=a.start,l=s+"---"+s+"---1",u=s+"---"+s+"---2",h=r.node(s);r.setNode(l,{domId:l,id:l,parentId:h.parentId,labelStyle:"",label:"",padding:0,shape:"labelRect",style:"",width:10,height:10}),r.setParent(l,h.parentId),r.setNode(u,{domId:u,id:u,parentId:h.parentId,labelStyle:"",padding:0,shape:"labelRect",label:"",style:"",width:10,height:10}),r.setParent(u,h.parentId);let f=structuredClone(a),d=structuredClone(a),p=structuredClone(a);f.label="",f.arrowTypeEnd="none",f.id=s+"-cyclic-special-1",d.arrowTypeEnd="none",d.id=s+"-cyclic-special-mid",p.label="",h.isGroup&&(f.fromCluster=s,p.toCluster=s),p.id=s+"-cyclic-special-2",r.setEdge(s,l,f,s+"-cyclic-special-0"),r.setEdge(l,u,d,s+"-cyclic-special-1"),r.setEdge(u,s,p,s+"-cyc
/g),m=t.append("text").classed("er relationshipLabel",!0).attr("id",d).attr("x",f.x).attr("y",f.y).style("text-anchor","middle").style("dominant-baseline","middle").style("font-family",de().fontFamily).style("font-size",Ii.fontSize+"px");if(p.length==1)m.text(e.roleA);else{let y=-(p.length-1)*.5;p.forEach((v,x)=>{m.append("tspan").attr("x",f.x).attr("dy",`${x===0?y:1}em`).text(v)})}let g=m.node().getBBox();t.insert("rect","#"+d).classed("er relationshipLabelBox",!0).attr("x",f.x-g.width/2).attr("y",f.y-g.height/2).attr("width",g.width).attr("height",g.height)},"drawRelationshipFromLayout"),tMe=o(function(t,e,r,n){Ii=de().er,V.info("Drawing ER diagram");let i=de().securityLevel,a;i==="sandbox"&&(a=$e("#i"+e));let l=(i==="sandbox"?$e(a.nodes()[0].contentDocument.body):$e("body")).select(`[id='${e}']`);$o.insertMarkers(l,Ii);let u;u=new lr({multigraph:!0,directed:!0,compound:!1}).setGraph({rankdir:Ii.layoutDirection,marginx:20,marginy:20,nodesep:100,edgesep:100,ranksep:100}).setDefaultEdgeLabel(function(){return{}});let h=QNe(l,n.db.getEntities(),u),f=JNe(n.db.getRelationships(),u);lo(u),ZNe(l,u),f.forEach(function(y){eMe(l,y,u,h,n)});let d=Ii.diagramPadding;Lt.insertTitle(l,"entityTitleText",Ii.titleTopMargin,n.db.getDiagramTitle());let p=l.node().getBBox(),m=p.width+d*2,g=p.height+d*2;Sr(l,g,m,Ii.useMaxWidth),l.attr("viewBox",`${p.x-d} ${p.y-d} ${m} ${g}`)},"draw"),rMe="28e9f9db-3c8d-5aa5-9faf-44286ae5937c";o(nMe,"generateId");o(Tne,"strWithHyphen");Ene={setConf:jNe,draw:tMe}});var iMe,Sne,Ane=R(()=>{"use strict";iMe=o(t=>`
+ .entityBox {
+ fill: ${t.mainBkg};
+ stroke: ${t.nodeBorder};
+ }
+
+ .attributeBoxOdd {
+ fill: ${t.attributeBackgroundColorOdd};
+ stroke: ${t.nodeBorder};
+ }
+
+ .attributeBoxEven {
+ fill: ${t.attributeBackgroundColorEven};
+ stroke: ${t.nodeBorder};
+ }
+
+ .relationshipLabelBox {
+ fill: ${t.tertiaryColor};
+ opacity: 0.7;
+ background-color: ${t.tertiaryColor};
+ rect {
+ opacity: 0.5;
+ }
+ }
+
+ .relationshipLine {
+ stroke: ${t.lineColor};
+ }
+
+ .entityTitleText {
+ text-anchor: middle;
+ font-size: 18px;
+ fill: ${t.textColor};
+ }
+ #MD_PARENT_START {
+ fill: #f5f5f5 !important;
+ stroke: ${t.lineColor} !important;
+ stroke-width: 1;
+ }
+ #MD_PARENT_END {
+ fill: #f5f5f5 !important;
+ stroke: ${t.lineColor} !important;
+ stroke-width: 1;
+ }
+
+`,"getStyles"),Sne=iMe});var _ne={};hr(_ne,{diagram:()=>aMe});var aMe,Lne=R(()=>{"use strict";nne();sne();Cne();Ane();aMe={parser:rne,db:ane,renderer:Ene,styles:Sne}});function Xn(t){return typeof t=="object"&&t!==null&&typeof t.$type=="string"}function xa(t){return typeof t=="object"&&t!==null&&typeof t.$refText=="string"}function ED(t){return typeof t=="object"&&t!==null&&typeof t.name=="string"&&typeof t.type=="string"&&typeof t.path=="string"}function Wd(t){return typeof t=="object"&&t!==null&&Xn(t.container)&&xa(t.reference)&&typeof t.message=="string"}function co(t){return typeof t=="object"&&t!==null&&Array.isArray(t.content)}function ef(t){return typeof t=="object"&&t!==null&&typeof t.tokenType=="object"}function zv(t){return co(t)&&typeof t.fullText=="string"}var Yd,Vo=R(()=>{"use strict";o(Xn,"isAstNode");o(xa,"isReference");o(ED,"isAstNodeDescription");o(Wd,"isLinkingError");Yd=class{static{o(this,"AbstractAstReflection")}constructor(){this.subtypes={},this.allSubtypes={}}isInstance(e,r){return Xn(e)&&this.isSubtype(e.$type,r)}isSubtype(e,r){if(e===r)return!0;let n=this.subtypes[e];n||(n=this.subtypes[e]={});let i=n[r];if(i!==void 0)return i;{let a=this.computeIsSubtype(e,r);return n[r]=a,a}}getAllSubTypes(e){let r=this.allSubtypes[e];if(r)return r;{let n=this.getAllTypes(),i=[];for(let a of n)this.isSubtype(a,e)&&i.push(a);return this.allSubtypes[e]=i,i}}};o(co,"isCompositeCstNode");o(ef,"isLeafCstNode");o(zv,"isRootCstNode")});function cMe(t){return typeof t=="string"?t:typeof t>"u"?"undefined":typeof t.toString=="function"?t.toString():Object.prototype.toString.call(t)}function hT(t){return!!t&&typeof t[Symbol.iterator]=="function"}function Kr(...t){if(t.length===1){let e=t[0];if(e instanceof uo)return e;if(hT(e))return new uo(()=>e[Symbol.iterator](),r=>r.next());if(typeof e.length=="number")return new uo(()=>({index:0}),r=>r.index${l.name}<- can never be matched.
+Because it appears AFTER the Token Type ->${n.name}<-in the lexer's definition.
+See https://chevrotain.io/docs/guide/resolving_lexer_errors.html#UNREACHABLE`;e.push({message:u,type:Gn.UNREACHABLE_PATTERN,tokenTypes:[n,l]})}})}),e}function dIe(t,e){if(zo(e)){let r=e.exec(t);return r!==null&&r.index===0}else{if(wi(e))return e(t,0,[],{});if(Xe(e,"exec"))return e.exec(t,0,[],{});if(typeof e=="string")return e===t;throw Error("non exhaustive match")}}function pIe(t){return Za([".","\\","[","]","|","^","$","(",")","?","*","+","{"],r=>t.source.indexOf(r)!==-1)===void 0}function mie(t){let e=t.ignoreCase?"i":"";return new RegExp(`^(?:${t.source})`,e)}function gie(t){let e=t.ignoreCase?"iy":"y";return new RegExp(`${t.source}`,e)}function xie(t,e,r){let n=[];return Xe(t,Qm)||n.push({message:"A MultiMode Lexer cannot be initialized without a <"+Qm+`> property in its definition
+`,type:Gn.MULTI_MODE_LEXER_WITHOUT_DEFAULT_MODE}),Xe(t,GT)||n.push({message:"A MultiMode Lexer cannot be initialized without a <"+GT+`> property in its definition
+`,type:Gn.MULTI_MODE_LEXER_WITHOUT_MODES_PROPERTY}),Xe(t,GT)&&Xe(t,Qm)&&!Xe(t.modes,t.defaultMode)&&n.push({message:`A MultiMode Lexer cannot be initialized with a ${Qm}: <${t.defaultMode}>which does not exist
+`,type:Gn.MULTI_MODE_LEXER_DEFAULT_MODE_VALUE_DOES_NOT_EXIST}),Xe(t,GT)&&Ee(t.modes,(i,a)=>{Ee(i,(s,l)=>{if(er(s))n.push({message:`A Lexer cannot be initialized using an undefined Token Type. Mode:<${a}> at index: <${l}>
+`,type:Gn.LEXER_DEFINITION_CANNOT_CONTAIN_UNDEFINED});else if(Xe(s,"LONGER_ALT")){let u=wt(s.LONGER_ALT)?s.LONGER_ALT:[s.LONGER_ALT];Ee(u,h=>{!er(h)&&!Fn(i,h)&&n.push({message:`A MultiMode Lexer cannot be initialized with a longer_alt <${h.name}> on token <${s.name}> outside of mode <${a}>
+`,type:Gn.MULTI_MODE_LEXER_LONGER_ALT_NOT_IN_CURRENT_MODE})})}})}),n}function bie(t,e,r){let n=[],i=!1,a=wc(Gr(or(t.modes))),s=Kh(a,u=>u[a0]===ni.NA),l=Cie(r);return e&&Ee(s,u=>{let h=Eie(u,l);if(h!==!1){let d={message:gIe(u,h),type:h.issue,tokenType:u};n.push(d)}else Xe(u,"LINE_BREAKS")?u.LINE_BREAKS===!0&&(i=!0):zT(l,u.PATTERN)&&(i=!0)}),e&&!i&&n.push({message:`Warning: No LINE_BREAKS Found.
+ This Lexer has been defined to track line and column information,
+ But none of the Token Types can be identified as matching a line terminator.
+ See https://chevrotain.io/docs/guide/resolving_lexer_errors.html#LINE_BREAKS
+ for details.`,type:Gn.NO_LINE_BREAKS_FLAGS}),n}function wie(t){let e={},r=Dr(t);return Ee(r,n=>{let i=t[n];if(wt(i))e[n]=[];else throw Error("non exhaustive match")}),e}function Tie(t){let e=t.PATTERN;if(zo(e))return!1;if(wi(e))return!0;if(Xe(e,"exec"))return!0;if(di(e))return!1;throw Error("non exhaustive match")}function mIe(t){return di(t)&&t.length===1?t.charCodeAt(0):!1}function Eie(t,e){if(Xe(t,"LINE_BREAKS"))return!1;if(zo(t.PATTERN)){try{zT(e,t.PATTERN)}catch(r){return{issue:Gn.IDENTIFY_TERMINATOR,errMsg:r.message}}return!1}else{if(di(t.PATTERN))return!1;if(Tie(t))return{issue:Gn.CUSTOM_LINE_BREAK};throw Error("non exhaustive match")}}function gIe(t,e){if(e.issue===Gn.IDENTIFY_TERMINATOR)return`Warning: unable to identify line terminator usage in pattern.
+ The problem is in the <${t.name}> Token Type
+ Root cause: ${e.errMsg}.
+ For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#IDENTIFY_TERMINATOR`;if(e.issue===Gn.CUSTOM_LINE_BREAK)return`Warning: A Custom Token Pattern should specify the u.length){u=s,h=f,ie=ce;break}}}break}}if(u!==null){if(d=u.length,p=ie.group,p!==void 0&&(m=ie.tokenTypeIdx,g=this.createTokenInstance(u,T,m,ie.tokenType,M,N,d),this.handlePayload(g,h),p===!1?E=this.addToken(A,E,g):k[p].push(g)),e=this.chopInput(e,d),T=T+d,N=this.computeNewColumn(N,d),I===!0&&ie.canLineTerminator===!0){let q=0,K,se;C.lastIndex=0;do K=C.test(u),K===!0&&(se=C.lastIndex-1,q++);while(K===!0);q!==0&&(M=M+q,N=d-se,this.updateTokenEndLineColumnLocation(g,p,se,q,M,N,d))}this.handleModes(ie,Q,X,g)}else{let q=T,K=M,se=N,ce=j===!1;for(;ce===!1&&T{"use strict";Pt();i2();s0();o(zu,"tokenLabel");o(gN,"hasTokenLabel");EIe="parent",Rie="categories",Nie="label",Mie="group",Iie="push_mode",Oie="pop_mode",Pie="longer_alt",Bie="line_breaks",Fie="start_chars_hint";o(VT,"createToken");o(CIe,"createTokenInternal");fo=VT({name:"EOF",pattern:ni.NA});Fu([fo]);o(o0,"createTokenInstance");o(s2,"tokenMatcher")});var Gu,zie,Ol,Jm=R(()=>{"use strict";l0();Pt();ns();Gu={buildMismatchTokenMessage({expected:t,actual:e,previous:r,ruleName:n}){return`Expecting ${gN(t)?`--> ${zu(t)} <--`:`token of type --> ${t.name} <--`} but found --> '${e.image}' <--`},buildNotAllInputParsedMessage({firstRedundant:t,ruleName:e}){return"Redundant input, expecting EOF but found: "+t.image},buildNoViableAltMessage({expectedPathsPerAlt:t,actual:e,previous:r,customUserDescription:n,ruleName:i}){let a="Expecting: ",l=`
+but found: '`+na(e).image+"'";if(n)return a+n+l;{let u=Vr(t,(p,m)=>p.concat(m),[]),h=qe(u,p=>`[${qe(p,m=>zu(m)).join(", ")}]`),d=`one of these possible Token sequences:
+${qe(h,(p,m)=>` ${m+1}. ${p}`).join(`
+`)}`;return a+d+l}},buildEarlyExitMessage({expectedIterationPaths:t,actual:e,customUserDescription:r,ruleName:n}){let i="Expecting: ",s=`
+but found: '`+na(e).image+"'";if(r)return i+r+s;{let u=`expecting at least one iteration which starts with one of these possible Token sequences::
+ <${qe(t,h=>`[${qe(h,f=>zu(f)).join(",")}]`).join(" ,")}>`;return i+u+s}}};Object.freeze(Gu);zie={buildRuleNotFoundError(t,e){return"Invalid grammar, reference to a rule which is not defined: ->"+e.nonTerminalName+`<-
+inside top level rule: ->`+t.name+"<-"}},Ol={buildDuplicateFoundError(t,e){function r(f){return f instanceof fr?f.terminalType.name:f instanceof Zr?f.nonTerminalName:""}o(r,"getExtraProductionArgument");let n=t.name,i=na(e),a=i.idx,s=Rs(i),l=r(i),u=a>0,h=`->${s}${u?a:""}<- ${l?`with argument: ->${l}<-`:""}
+ appears more than once (${e.length} times) in the top level rule: ->${n}<-.
+ For further details see: https://chevrotain.io/docs/FAQ.html#NUMERICAL_SUFFIXES
+ `;return h=h.replace(/[ \t]+/g," "),h=h.replace(/\s\s+/g,`
+`),h},buildNamespaceConflictError(t){return`Namespace conflict found in grammar.
+The grammar has both a Terminal(Token) and a Non-Terminal(Rule) named: <${t.name}>.
+To resolve this make sure each Terminal and Non-Terminal names are unique
+This is easy to accomplish by using the convention that Terminal names start with an uppercase letter
+and Non-Terminal names start with a lower case letter.`},buildAlternationPrefixAmbiguityError(t){let e=qe(t.prefixPath,i=>zu(i)).join(", "),r=t.alternation.idx===0?"":t.alternation.idx;return`Ambiguous alternatives: <${t.ambiguityIndices.join(" ,")}> due to common lookahead prefix
+in
/gi):t).forEach(n=>{let i=document.createElementNS("http://www.w3.org/2000/svg","tspan");i.setAttributeNS("http://www.w3.org/XML/1998/namespace","xml:space","preserve"),i.setAttribute("dy","1em"),i.setAttribute("x","0"),i.setAttribute("class","row"),i.textContent=n.trim(),e.appendChild(i)}),e},"drawText"),Ile=o(t=>{let e,r,n;return Tr==="BT"?(r=o((i,a)=>i<=a,"comparisonFunc"),n=1/0):(r=o((i,a)=>i>=a,"comparisonFunc"),n=0),t.forEach(i=>{let a=Tr==="TB"||Tr=="BT"?Os.get(i)?.y:Os.get(i)?.x;a!==void 0&&r(a,n)&&(e=i,n=a)}),e},"findClosestParent"),fBe=o(t=>{let e="",r=1/0;return t.forEach(n=>{let i=Os.get(n).y;i<=r&&(e=n,r=i)}),e||void 0},"findClosestParentBT"),dBe=o((t,e,r)=>{let n=r,i=r,a=[];t.forEach(s=>{let l=e.get(s);if(!l)throw new Error(`Commit not found for key ${s}`);l.parents.length?(n=mBe(l),i=Math.max(n,i)):a.push(l),gBe(l,n)}),n=i,a.forEach(s=>{yBe(s,n,r)}),t.forEach(s=>{let l=e.get(s);if(l?.parents.length){let u=fBe(l.parents);n=Os.get(u).y-df,n<=i&&(i=n);let h=Is.get(l.branch).pos,f=n-ff;Os.set(l.id,{x:h,y:f})}})},"setParallelBTPos"),pBe=o(t=>{let e=Ile(t.parents.filter(n=>n!==null));if(!e)throw new Error(`Closest parent not found for commit ${t.id}`);let r=Os.get(e)?.y;if(r===void 0)throw new Error(`Closest parent position not found for commit ${t.id}`);return r},"findClosestParentPos"),mBe=o(t=>pBe(t)+df,"calculateCommitPosition"),gBe=o((t,e)=>{let r=Is.get(t.branch);if(!r)throw new Error(`Branch not found for commit ${t.id}`);let n=r.pos,i=e+ff;return Os.set(t.id,{x:n,y:i}),{x:n,y:i}},"setCommitPosition"),yBe=o((t,e,r)=>{let n=Is.get(t.branch);if(!n)throw new Error(`Branch not found for commit ${t.id}`);let i=e+r,a=n.pos;Os.set(t.id,{x:a,y:i})},"setRootPosition"),vBe=o((t,e,r,n,i,a)=>{if(a===Hr.HIGHLIGHT)t.append("rect").attr("x",r.x-10).attr("y",r.y-10).attr("width",20).attr("height",20).attr("class",`commit ${e.id} commit-highlight${i%E0} ${n}-outer`),t.append("rect").attr("x",r.x-6).attr("y",r.y-6).attr("width",12).attr("height",12).attr("class",`commit ${e.id} commit${i%E0} ${n}-inner`);else if(a===Hr.CHERRY_PICK)t.append("circle").attr("cx",r.x).attr("cy",r.y).attr("r",10).attr("class",`commit ${e.id} ${n}`),t.append("circle").attr("cx",r.x-3).attr("cy",r.y+2).attr("r",2.75).attr("fill","#fff").attr("class",`commit ${e.id} ${n}`),t.append("circle").attr("cx",r.x+3).attr("cy",r.y+2).attr("r",2.75).attr("fill","#fff").attr("class",`commit ${e.id} ${n}`),t.append("line").attr("x1",r.x+3).attr("y1",r.y+1).attr("x2",r.x).attr("y2",r.y-5).attr("stroke","#fff").attr("class",`commit ${e.id} ${n}`),t.append("line").attr("x1",r.x-3).attr("y1",r.y+1).attr("x2",r.x).attr("y2",r.y-5).attr("stroke","#fff").attr("class",`commit ${e.id} ${n}`);else{let s=t.append("circle");if(s.attr("cx",r.x),s.attr("cy",r.y),s.attr("r",e.type===Hr.MERGE?9:10),s.attr("class",`commit ${e.id} commit${i%E0}`),a===Hr.MERGE){let l=t.append("circle");l.attr("cx",r.x),l.attr("cy",r.y),l.attr("r",6),l.attr("class",`commit ${n} ${e.id} commit${i%E0}`)}a===Hr.REVERSE&&t.append("path").attr("d",`M ${r.x-5},${r.y-5}L${r.x+5},${r.y+5}M${r.x-5},${r.y+5}L${r.x+5},${r.y-5}`).attr("class",`commit ${n} ${e.id} commit${i%E0}`)}},"drawCommitBullet"),xBe=o((t,e,r,n)=>{if(e.type!==Hr.CHERRY_PICK&&(e.customId&&e.type===Hr.MERGE||e.type!==Hr.MERGE)&&Ko?.showCommitLabel){let i=t.append("g"),a=i.insert("rect").attr("class","commit-label-bkg"),s=i.append("text").attr("x",n).attr("y",r.y+25).attr("class","commit-label").text(e.id),l=s.node()?.getBBox();if(l&&(a.attr("x",r.posWithOffset-l.width/2-Hu).attr("y",r.y+13.5).attr("width",l.width+2*Hu).attr("height",l.height+2*Hu),Tr==="TB"||Tr==="BT"?(a.attr("x",r.x-(l.width+4*Oc+5)).attr("y",r.y-12),s.attr("x",r.x-(l.width+4*Oc)).attr("y",r.y+l.height-12)):s.attr("x",r.posWithOffset-l.width/2),Ko.rotateCommitLabel))if(Tr==="TB"||Tr==="BT")s.attr("transform","rotate(-45, "+r.x+", "+r.y+")"),a.attr("transform","rotate(-45, "+r.x+", "+r.y+")");else{let u=-7.5-(l.width+10)/25*9.5,h=10+l.width/25*8.5;i.attr("transform","translate("+u+", "+h+") rotate(-45, "+n+", "+r.y+")")}}},"drawCommitLabel"),bBe=o((t,e,r,n)=>{if(e.tags.length>0){let i=0,a=0,s=0,l=[];for(let u of e.tags.reverse()){let h=t.insert("polygon"),f=t.append("circle"),d=t.append("text").attr("y",r.y-16-i).attr("class","tag-label").text(u),p=d.node()?.getBBox();if(!p)throw new Error("Tag bbox not found");a=Math.max(a,p.width),s=Math.max(s,p.height),d.attr("x",r.posWithOffset-p.width/2),l.push({tag:d,hole:f,rect:h,yOffset:i}),i+=20}for(let{tag:u,hole:h,rect:f,yOffset:d}of l){let p=s/2,m=r.y-19.2-d;if(f.attr("class","tag-label-bkg").attr("points",`
+ ${n-a/2-Oc/2},${m+Hu}
+ ${n-a/2-Oc/2},${m-Hu}
+ ${r.posWithOffset-a/2-Oc},${m-p-Hu}
+ ${r.posWithOffset+a/2+Oc},${m-p-Hu}
+ ${r.posWithOffset+a/2+Oc},${m+p+Hu}
+ ${r.posWithOffset-a/2-Oc},${m+p+Hu}`),h.attr("cy",m).attr("cx",n-a/2+Oc/2).attr("r",1.5).attr("class","tag-hole"),Tr==="TB"||Tr==="BT"){let g=n+d;f.attr("class","tag-label-bkg").attr("points",`
+ ${r.x},${g+2}
+ ${r.x},${g-2}
+ ${r.x+ff},${g-p-2}
+ ${r.x+ff+a+4},${g-p-2}
+ ${r.x+ff+a+4},${g+p+2}
+ ${r.x+ff},${g+p+2}`).attr("transform","translate(12,12) rotate(45, "+r.x+","+n+")"),h.attr("cx",r.x+Oc/2).attr("cy",g).attr("transform","translate(12,12) rotate(45, "+r.x+","+n+")"),u.attr("x",r.x+5).attr("y",g+3).attr("transform","translate(14,14) rotate(45, "+r.x+","+n+")")}}}},"drawCommitTags"),wBe=o(t=>{switch(t.customType??t.type){case Hr.NORMAL:return"commit-normal";case Hr.REVERSE:return"commit-reverse";case Hr.HIGHLIGHT:return"commit-highlight";case Hr.MERGE:return"commit-merge";case Hr.CHERRY_PICK:return"commit-cherry-pick";default:return"commit-normal"}},"getCommitClassType"),TBe=o((t,e,r,n)=>{let i={x:0,y:0};if(t.parents.length>0){let a=Ile(t.parents);if(a){let s=n.get(a)??i;return e==="TB"?s.y+df:e==="BT"?(n.get(t.id)??i).y-df:s.x+df}}else return e==="TB"?tE:e==="BT"?(n.get(t.id)??i).y-df:0;return 0},"calculatePosition"),kBe=o((t,e,r)=>{let n=Tr==="BT"&&r?e:e+ff,i=Tr==="TB"||Tr==="BT"?n:Is.get(t.branch)?.pos,a=Tr==="TB"||Tr==="BT"?Is.get(t.branch)?.pos:n;if(a===void 0||i===void 0)throw new Error(`Position were undefined for commit ${t.id}`);return{x:a,y:i,posWithOffset:n}},"getCommitPosition"),Nle=o((t,e,r)=>{if(!Ko)throw new Error("GitGraph config not found");let n=t.append("g").attr("class","commit-bullets"),i=t.append("g").attr("class","commit-labels"),a=Tr==="TB"||Tr==="BT"?tE:0,s=[...e.keys()],l=Ko?.parallelCommits??!1,u=o((f,d)=>{let p=e.get(f)?.seq,m=e.get(d)?.seq;return p!==void 0&&m!==void 0?p-m:0},"sortKeys"),h=s.sort(u);Tr==="BT"&&(l&&dBe(h,e,a),h=h.reverse()),h.forEach(f=>{let d=e.get(f);if(!d)throw new Error(`Commit not found for key ${f}`);l&&(a=TBe(d,Tr,a,Os));let p=kBe(d,a,l);if(r){let m=wBe(d),g=d.customType??d.type,y=Is.get(d.branch)?.index??0;vBe(n,d,p,m,y,g),xBe(i,d,p,a),bBe(i,d,p,a)}Tr==="TB"||Tr==="BT"?Os.set(d.id,{x:p.x,y:p.posWithOffset}):Os.set(d.id,{x:p.posWithOffset,y:p.y}),a=Tr==="BT"&&l?a+df:a+df+ff,a>hf&&(hf=a)})},"drawCommits"),EBe=o((t,e,r,n,i)=>{let s=(Tr==="TB"||Tr==="BT"?r.x