Adapter

Status: Entwurf

Vorschläge von Claude – nicht verbindlich, bis Daniel sie freigibt.

Ein Adapter weiß, welche Objekte es in seinem Format gibt. Er macht aus dem Text einer Quelldatei einen Baum von Elementen, jedes mit einem strukturellen Selektor, einem Textbereich und dem Text, der für den Fingerprint genutzt wird.

Vorschlag: Eine kleine Adapter-Schnittstelle für alle Formate PROP_ADAPTER_API
status: draft
@dataclass(frozen=True)
class Element:
    selector: str            # "md:heading:Career/Advisor"
    kind: str                # "heading", "paragraph", "need", ...
    label: str               # short text for UIs and the CLI
    start: int               # character offsets in the source
    end: int
    line: int                # 1-based, for humans and editors
    text: str                # source text of the element (fingerprint input)
    children: tuple["Element", ...] = ()

class Adapter(Protocol):
    name: str                         # "md", "rst", "text"
    suffixes: tuple[str, ...]         # (".md",), (".rst",), (".txt",)
    def elements(self, source: str, path: str) -> list[Element]: ...

Adapter sind pur: kein Dateizugriff, kein Zustand. Dadurch lassen sie sich schnell mit Golden Files testen (Quelle rein, Elementliste als JSON raus), und die Sphinx-Erweiterung kann sie später wiederverwenden.

Strukturelle Selektoren

Ein Selektor ist ein String <adapter>:<kind>:<path>. Ihn sehen Nutzer in der CLI, und ihn speichert das Dateiformat.

Adapter

Selektor

Element

md

md:heading:Career/Advisor

der Abschnitt unter diesem Überschriftenpfad, mit seinem Inhalt

md

md:paragraph:Career/Advisor#2

der 2. Absatz in diesem Abschnitt

md

md:item:Open#3 · md:row:Formats#5 · md:code:Setup#1

Listenpunkt, Tabellenzeile, Codeblock – pro Abschnitt nummeriert

md

md:front:summary

ein Frontmatter-Feld

rst

rst:section:Install/Linux · rst:paragraph:Install/Linux#1

wie in Markdown

rst

sphinx-needs:REQ_042

ein Need, gefunden über seine ID – der robusteste Selektor, den es gibt

rst

sphinx-needs:REQ_042:status

eine Option eines Needs

rst

rst:directive:Install/Linux#note1

die erste note-Directive in diesem Abschnitt

text

text:paragraph#4 · text:line#17

reiner Text

html

html:heading:Services/Consulting · html:paragraph:Services/Consulting#2

eine gerenderte Webseite, im Browser gefunden (Web-Skript)

py

py:class:Engine · py:function:Engine.start · py:function:main

eine Klasse, Methode oder Funktion, benannt nach ihrem Pfad im Code (Python-Quelltext wird je Kl... (DEC_PYTHON_ADAPTER))

py

py:statement:imports · py:statement:API · py:statement:#2

Anweisungen außerhalb von Funktionen: der Importblock, eine Zuweisung nach Namen, jede andere Anweisung gezählt je Bereich

json

json: · json:/color/primary · json:/versions/0

das Dokument, ein Objekt-Mitglied, ein Array-Eintrag – ein JSON Pointer (JSON- und YAML-Dateien werd... (DEC_DATA_ADAPTERS))

yaml

yaml: · yaml:/jobs/build/steps/2

dasselbe für YAML

Vorschlag: Element-Selektoren sind Pfade aus Überschriften plus ein Zähler pro Abschnitt PROP_SELECTORS
status: draft

Überschriftenpfade überstehen das Verschieben ganzer Abschnitte und Änderungen in anderen Abschnitten. Der Zähler (#2) bricht, wenn darüber ein Absatz eingefügt wird – deshalb speichert jede Annotation zusätzlich ein Textzitat (Dateiformat), das die Engine als Nächstes probiert. Doppelte Überschriften in einem Pfad bekommen ein Suffix (Notes~2).

Markdown

  • Parser: markdown-it-py (CommonMark + Tabellen); Token-Maps liefern Zeilenbereiche, Offsets ergeben sich aus den Zeilenbereichen.

  • Elemente: Überschriften-Abschnitte, Absätze, Listenpunkte (verschachtelt), Tabellenzeilen, Codeblöcke, Frontmatter-Felder (YAML-Schlüssel auf oberster Ebene).

Als Erstes gebaut, für Host-Apps (In einem Host bekommt das O... (DEC_HOST_ELEMENTS)): deciwa.core.adapters.markdown liefert die Elemente als Dicts in der Form des Sphinx-Elementindex (selector, kind, label, line, end, text, fingerprint, parent) – Abschnitte, Absätze, Listenpunkte, Tabellenzeilen, Codeblöcke und Bilder. Frontmatter-Felder und Zeichen-Offsets fehlen noch. Der Text eines Elements ist das, was Lesende sehen (ohne Inline-Markup), wie in den Sphinx- und Browser-Adaptern; so wird ein in einer dargestellten Ansicht markiertes Zitat darin gefunden, und ein geändertes Linkziel allein macht ein Element nicht veraltet. Zeilen zählen ab 1 und schließen die Frontmatter ein. Die Engine nutzt den Adapter für jede Datei, die kein Build-Index kennt.

rST und Sphinx-Needs

Vorschlag: Der rST-Adapter parst mit docutils, ohne Sphinx auszuführen PROP_RST_DOCUTILS
status: draft
refined by: Q_NEED_TYPES

docutils merkt sich an jedem Knoten die Quellzeilen. Unbekannte Directives und Rollen (Sphinx, Sphinx-Needs, beliebige Erweiterungen) werden als generische Platzhalter registriert, sodass eine Datei ohne conf.py und ohne Import von Erweiterungen geparst wird. Need-Directives werden am Namen erkannt; ihre :id: ergibt den sphinx-needs:-Selektor.

Frage: Welche Directive-Namen zählen als Needs? Q_NEED_TYPES
status: open
to: Daniel

Vorschlag: aus needs.json lesen, wenn das Projekt eines baut, sonst aus .deciwa/config.toml (needs.directives = ["req", "spec", ...]), und auf die Standardwerte von Sphinx-Needs zurückfallen (req, spec, impl, test, need). OK, oder soll deciwa conf.py / ubproject.toml direkt lesen?

Python

Python-Quelltext wird je Kl... (DEC_PYTHON_ADAPTER) – deciwa.core.adapters.python liest .py- und .pyi-Dateien mit ast aus der Standardbibliothek:

  • Elemente: Klassen, Funktionen und Methoden, verschachtelt wie im Code (das Elternelement einer Methode ist ihre Klasse, das einer inneren Funktion die Funktion darum); Definitionen in if-, try- oder with-Blöcken zählen auf ihrer Ebene.

  • Anweisungen außerhalb von Funktionen – auf Modulebene und im Rumpf einer Klasse – sind Elemente der Art statement: der Docstring (py:statement:__doc__), ein Block von Importen (py:statement:imports), eine Zuweisung nach ihrem Namen (py:statement:API, py:statement:Engine.size), jede andere Anweisung gezählt je Bereich (py:statement:#1 – z. B. if __name__ == "__main__":; py:statement:Engine#2). Anweisungen in einer Funktion gehören zur Funktion.

  • Selektoren: py:<class|function>:<path> – py:function:Engine.start; eine zweite Definition mit demselben Pfad (ein Property-Setter, eine Neudefinition) bekommt ~2.

  • Text: der Quelltext der Definition, Dekoratoren eingeschlossen – jede Änderung an einer freigegebenen Funktion macht sie veraltet. Bezeichnung: ihre erste Zeile (def start(self) -> Handshake:).

  • Zeilen: line ist die erste Zeile (der erste Dekorator), end die letzte.

  • Eine Datei mit Syntaxfehler hat keine Elemente.

  • Die Elementarten class, function und statement lassen sich in den Review-Regeln nennen.

Noch nicht: Kommentare als Elemente; JS, TS und C (tree-sitter, als Nächstes).

HTML-Dateien

HTML-Dateien liest ein Adap... (DEC_HTML_ADAPTER) – deciwa.core.adapters.html liest .html- und .htm-Dateien mit html.parser aus der Standardbibliothek und folgt den Regeln des DOM-Scans im Browser (Das Web-Skript findet die E... (PROP_DOM_ADAPTER)): die Review-Wurzel (main, ein einzelnes article, sonst body), Abschnitte nach der Überschriftengliederung, Absätze, Listenpunkte, Tabellenzeilen, Codeblöcke, Hinweise und Bilder, gezählt je Abschnitt – dieselben html:-Selektoren. Jedes Element trägt seine Anfangszeile, seine Endzeile und sein tag. Ohne Browser gibt es kein Layout, deshalb zählen auch per CSS versteckte Elemente und kleine Icons.

Ein Host markiert jedes Start-Tag der dargestellten Seite mit seiner Quellzeile; das Overlay ordnet jedes Element dem Tag seiner Art auf seiner Zeile zu. Dargestellte Seite und Quelltextansicht teilen sich so ihre Elemente, und Feedback auf der Seite zeigt auf die Quellzeile. Eine unter ihrer Adresse gespeicherte Webseite (aus einem Browser) weicht dem Adapter, sobald ihre Quelldatei bekannt ist.

In einem Host werden die Zeilen jedes Elements zu einem Block der Code-Ansicht zusammengefasst (Host-Apps).

Datendateien (JSON, YAML)

JSON- und YAML-Dateien werd... (DEC_DATA_ADAPTERS) – deciwa.core.adapters.json liest .json-Dateien, deciwa.core.adapters.yaml liest .yaml- und .yml-Dateien. Beide liefern dieselben Elemente:

  • Elemente: das Dokument, jedes Objekt-Mitglied und jeder Array-Eintrag, Blätter eingeschlossen, verschachtelt wie in der Datei. Arten: object, array, value – sie können in den Review-Regeln genannt werden. Ein YAML-Mapping ist ein Objekt, eine Sequenz ein Array, ein Skalar ein Wert.

  • Selektoren: der Name des Adapters plus der JSON Pointer des Elements (RFC 6901): json: für das Dokument, json:/color/primary, json:/versions/0; ~ und / in einem Schlüssel werden zu ~0 und ~1. Jedes Element trägt außerdem seinen pointer, damit ein Host den Knoten in seiner eigenen Baumansicht findet (Host-Apps).

  • Text: der Wert in kanonischer Form – sortierte Schlüssel, keine Leerzeichen (json.dumps(value, sort_keys=True, ensure_ascii=False, separators=(",", ":"))). Neu formatieren, neu einrücken oder Schlüssel umsortieren behält den Review-Stand; ein geänderter Wert macht sein Element und alle Eltern veraltet.

  • Bezeichnung: der Schlüssel plus eine kurze Vorschau – primary: "#1f6feb", needs {312}, steps [4]; Array-Einträge nehmen ihren Index als Schlüssel.

  • Zeilen: line ist die Zeile des Schlüssels (Array-Einträge: ihre erste Zeile), end die letzte Zeile des Werts.

  • JSON: die Standardbibliothek liefert keine Positionen, deshalb liest ein kleiner Scanner die Datei einmal und merkt sich die Zeilen jedes Schlüssels und Werts. Nur Standard-JSON – kein JSON Lines, keine Kommentare, kein NaN.

  • YAML: der Safe Loader von PyYAML – der Knotenbaum liefert die Positionen, der Safe Constructor die Werte. Nur das erste Dokument wird gelesen. Schlüssel, die keine Strings sind, werden zu ihrer JSON-Form (1, true). Anker, Aliase und Merge-Schlüssel sind in den Werten aufgelöst; Mitglieder, die ein Mapping über einen Merge-Schlüssel (<<: *base) bekommt, und das Innere eines Alias werden dort reviewt, wo sie stehen, nicht als eigene Elemente.

  • Eine ungültige Datei hat keine Elemente.

Ein Array-Index verschiebt sich, wenn davor ein Eintrag eingefügt wird. Die Engine folgt dann dem reviewten Wert zu seinem neuen Index (State-Engine, Verschobene Elemente), so bleibt das Review beim Wert.

Reiner Text

Absätze sind durch Leerzeilen getrennte Blöcke; Zeilen sind eine zweite Ebene. Alles andere ist freie Textauswahl.

HTML im Browser

Vorschlag: Das Web-Skript findet die Elemente einer gerenderten Seite selbst PROP_DOM_ADAPTER
status: draft

Ein kleiner Adapter in JavaScript liest das DOM der Review-Wurzel und liefert dieselbe Elementliste wie die anderen Adapter:

  • Abschnitte aus der Überschriftengliederung h1–h6 (Webseiten verschachteln selten echte <section>-Elemente), benannt nach ihrem Überschriftenpfad,

  • Absätze, Listenpunkte, Tabellenzeilen (tr außerhalb von thead), Codeblöcke (pre), Hinweise (aside, [role=note]), Bilder (img, svg, figure, picture) – pro Abschnitt gezählt wie in Markdown,

  • ausgelassen: nav, header, footer, form, [hidden], [aria-hidden=true], script, style und alles, worauf data-ignore passt.

Der Fingerprint ist SHA-256 über den normalisierten Text, berechnet mit crypto.subtle – derselbe Wert wie model.fingerprint in Python, damit CLI und Browser sich bei veralteten Elementen einig sind. Das Offline-Overlay wechselt von seinem lokalen FNV-Hash zu diesem. Bei Bildern bilden src, alt und Bildunterschrift den Fingerprint.

Seiten mit Build-Hinweisen (#deciwa-page, wie die Sphinx-Erweiterung es schreibt) überspringen den Scan und nutzen die Hinweise.

Freie Textauswahl

Vorschlag: Eine freie Textauswahl ist ein Textzitat innerhalb ihres nächsten Elements PROP_FREE_TEXT
status: draft

Wird ein Wort markiert, speichert deciwa den strukturellen Selektor des Elements, das es enthält, plus einen TextQuoteSelector (exact, prefix, suffix) und einen TextPositionSelector relativ zu diesem Element. Das funktioniert in jedem Format, und der Fingerprint umfasst nur den markierten Text.