# Ruff — die harte Teilmenge # # Diese Datei nennt die Regelfamilien, für die der Bestand NICHT geduldet wird. # Für sie gilt: jeder Treffer ist ein Fehler, sofort. Aufgenommen wird eine # Familie erst, wenn sie über server/ null Treffer hat. # # Aufruf, gehört vor jeden Commit, der Python-Code berührt: # # ruff check --config ruff-hart.toml server/ # # Rückgabewert 0 ist die Bedingung. Alles andere blockiert den Commit. # # WARUM EINE ZWEITE DATEI UND NICHT EIN EINTRAG IN ruff.toml: # Ruff kennt keine Schweregrade. Jede in ruff.toml selektierte Regel meldet # gleich laut, und der Gesamtlauf über server/ endet ohnehin mit einem # Rückgabewert ungleich null — am 30.07.2026 mit 2263 Treffern, die als # Bestand geduldet sind. Ein neu hinzukommender LOG-Treffer hübe diese Zahl # von 2263 auf 2264 und wäre damit von keinem Menschen und keinem # Rückgabewert zu unterscheiden. Ein zweiter Lauf, der sauber sein MUSS, ist # der einzige Weg, aus einer Regel eine Wand zu machen, ohne eine Datei mit # Vergleichszahlen zu pflegen, die selbst wieder gepflegt werden müsste. # # Diese Datei wird von Ruff NICHT automatisch gefunden — gesucht werden nur # ruff.toml, .ruff.toml und pyproject.toml. Sie wirkt ausschließlich über # --config und stört den normalen Lauf deshalb nicht. # # AUFNAHMEKRITERIUM, beide Teile sind Pflicht: # 1. Die Familie hat über server/ null Treffer. # 2. Keine ihrer Regeln steht im Preview-Status. Preview-Regeln können mit # der nächsten Werkzeugversion erscheinen oder verschwinden; eine Wand, # die sich mit dem Werkzeug bewegt, ist keine. # Dazu die Frage, die eine Null erst zu einem Befund macht: Kann das Werkzeug # einen Verstoß in diesem Bestand überhaupt sehen? Siehe die Reichweiten- # Prüfung bei LOG. # Erbt ruff.toml vollständig: Zielversion, Zeilenlänge, Ausnahmeblock und alle # Grenzwerte. Ersetzt davon nur die Regelauswahl. extend = "ruff.toml" [lint] select = [ # LOG (flake8-logging) — Fehlgebrauch der Logging-Bibliothek. # # Bestand am 30.07.2026, ruff 0.16.0, über server/: 0 Treffer. # Alle sieben Regeln der Familie sind stable, keine im Preview: # LOG001, LOG002, LOG004, LOG007, LOG009, LOG014, LOG015. # # Gegenprobe am 30.07.2026: Für jede der sieben Regeln wurde ein Verstoß # konstruiert und unter dieser Konfiguration gemeldet. Die Null sagt damit # "kein Verstoß vorhanden" und nicht "das Werkzeug sieht nicht hin". # # REICHWEITE, gemessen am 30.07.2026 — der Teil, der beinahe gefehlt hätte: # Die Regeln der Familie erkennen einen Logger an seinem NAMEN, nicht an # seiner Herkunft. Ein Name wird erkannt, wenn er "logger" enthält oder # genau "log" lautet, Groß- und Kleinschreibung egal. Isoliert nachgemessen: # logger, log, LOGGER, _logger, _llm_logger, logger_tokens -> erkannt # _log -> STUMM # Der Importstil ist dabei gleichgültig: "import logging" und # "from logging import getLogger" verhalten sich identisch. # # Im Bestand entstehen 156 Logger modulweit, keiner innerhalb einer # Funktion, keiner als Attribut, keiner als Parameter. 155 davon tragen # einen erkannten Namen. Der 156. hieß _log und war damit für die gesamte # Familie unsichtbar; er ist am 30.07.2026 umbenannt worden, weil eine Wand # vor einem Modul, in das das Werkzeug nicht hineinsieht, eine Zusicherung # ohne Deckung ist. # # Die Einstellung logger-objects hilft hier NICHT und ist deshalb nicht # gesetzt. Gemessen: Sie wirkt nur, wenn ein Logger aus einem anderen Modul # importiert wird; für den Gebrauch innerhalb des Moduls, das ihn anlegt, # bleibt sie wirkungslos. Der einzige Hebel dort ist der Name. "LOG", # F821 (Pyflakes) — Verwendung eines Namens, den es nicht gibt. # # Bestand am 31.07.2026, ruff 0.16.0, über server/: 0 Treffer. # F821 ist stable, nicht im Preview. # # ANLASS, und er ist der schärfste, den eine Wand haben kann: Die Regel # hatte am 30.07.2026 neun Treffer, und acht davon waren zwei echte # Laufzeitfehler in api/chat.py — je zwei Leser einer lokalen Variablen, # die bei einer Umstrukturierung mit ihrem Erzeuger verschwunden war. # Beide Pfade des Chat-Endpunkts starben daran, der synchrone wie der # streamende. Das Werkzeug hat das gemeldet, bevor es das erste Mal # zuschlug — die Meldung ging in der geduldeten Trefferzahl unter. # # Das ist der Fall, gegen den diese Datei gebaut ist: nicht eine Regel, # die Geschmack durchsetzt, sondern eine, die einen NameError vor der # Auslieferung findet. Sie duldet keinen Bestand, weil ein Bestand hier # bedeutet, dass Code ausgeliefert wird, der beim Betreten abstürzt. # # REICHWEITE, gemessen am 31.07.2026: # F821 ist blind gegenüber Namen, die zur Laufzeit entstehen — Stern- # Importe, exec(), Zuweisungen in globals(). Über server/ gezählt: # "from x import *" -> 0 Vorkommen # exec( / globals()[ -> 0 Vorkommen # Es gibt im Bestand also keine Stelle, an der ein Name auf einem Weg # entsteht, den die Regel nicht verfolgen kann. Die Null sagt damit # "kein undefinierter Name vorhanden". # # Positivkontrolle am 31.07.2026: Eine Funktion mit einem frei erfundenen # Namen wurde unter dieser Konfiguration gemeldet. # # ACHTUNG bei String-Annotationen: `redis_client: "redis.Redis"` ohne # Import ist zur Laufzeit harmlos, für F821 aber ein Treffer — zu Recht, # denn sobald jemand die Annotationen auflöst, kracht es. Der Bestand # annotiert deshalb direkt und importiert das Modul. "F821", # W191, W291, W292, W293, W605 (pycodestyle Warning) — Weissraum und # ungueltige Escape-Sequenzen. # # Bestand am 08.08.2026, ruff 0.16.0, ueber server/: 0 Treffer, nachdem # sechs bereinigt wurden (3x W292 fehlende Schlusszeile, 3x W293 Leerzeile # mit Leerzeichen, alle vier betroffenen Dateien maschinell behoben). # # WARUM NICHT DIE FAMILIE "W", obwohl sie bei null steht: **W391 ist im # Preview.** Aufnahmekriterium 2 schliesst das aus, und der Grund ist hier # greifbar — wuerde W391 mit einer spaeteren Werkzeugversion stable, waechse # die Wand ohne Zutun um eine Regel, gegen die der Bestand nie geprueft # wurde. Eine Wand, die sich mit dem Werkzeug bewegt, ist keine. Die # Familie steht deshalb einzeln aufgezaehlt, nicht als Kuerzel. # # W505 ist bewusst NICHT dabei, obwohl stable: Die Regel wirkt nur mit # gesetztem max-doc-length, und ruff.toml setzt es nicht (gemessen: 0 # Vorkommen). Sie waere eine Wand ohne Deckung — sie kann nie feuern und # sagt trotzdem null. # # REICHWEITE, gemessen am 08.08.2026: Fuer jede der fuenf Regeln wurde ein # Verstoss konstruiert und unter dieser Auswahl gemeldet — Tabulator- # Einrueckung, Leerzeichen am Zeilenende, fehlende Schlusszeile, Leerzeile # mit Leerzeichen, "\d" in einer gewoehnlichen Zeichenkette. Fuenf von # fuenf. Die Null sagt damit "kein Verstoss vorhanden". # # W605 ist die einzige der fuenf, die kein Stilbefund ist: Eine ungueltige # Escape-Sequenz in einer gewoehnlichen Zeichenkette ist heute eine # Warnung und in kuenftigen Python-Fassungen ein Fehler. "W191", "W291", "W292", "W293", "W605", # N (pep8-naming) — Namensformen. # # Bestand am 08.08.2026, ruff 0.16.0, ueber server/: 0 Treffer, nachdem # dreissig bereinigt wurden — 20x N802 (Endpunkt-Handler in PascalCase), # 6x N806 (Konstanten im Funktionsrumpf), 4x N818 (Ausnahmen ohne # Error-Suffix). Nulllinie 2177 -> 2147. # # Alle sechzehn Regeln sind stable, keine im Preview: N801-N807, N811-N818, # N999. NPY001-003 und NPY201 beginnen zwar mit N, gehoeren aber zur # Familie NPY und werden von diesem Kuerzel NICHT mitselektiert — gemessen # am 08.08.2026, die Statistik meldet sie unter "N" nie. # # REICHWEITE, gemessen am 08.08.2026: Fuer **jede** der sechzehn Regeln # wurde ein Verstoss konstruiert und gemeldet. Drei brauchten dabei einen # zweiten Anlauf, und der Grund gehoert dazu, weil er sonst beim naechsten # Mal wieder Zeit kostet: # N811 feuert nur bei einem echten Konstanten-Namen in der Quelle # (`from calendar import EPOCH as epoch`), nicht bei `sep as s`. # N817 feuert bei einem Akronym aus den Initialen (`TestCase as TC`); # ein Kuerzel wie `HTTPConnection as HC` faellt vorher auf N814. # N999 feuert nur INNERHALB eines Pakets — eine schlecht benannte Datei # ohne `__init__.py` daneben bleibt stumm. # # WAS DIESE WAND NICHT LEISTET, und das ist der Teil, der sonst spaeter # falsch erinnert wird: Sie prueft die **Form**, nicht die **Sprache**. # `12_NAMENSGEBUNG.md` §1 schreibt englische Bezeichner vor; eine neue # Funktion namens `haltung_berechnen` ist snake_case und passiert diese # Wand ohne Widerspruch. Sprachtrennung steht in §7a ausdruecklich als # nicht maschinell pruefbar. Die Wand faengt PascalCase-Funktionen, # Konstanten im Funktionsrumpf und Ausnahmen ohne Error-Suffix — mehr # nicht, und der Bestand an deutschen Bezeichnern bleibt fuer sie # unsichtbar. "N", # I001 (isort) — unsortierte Importe. # # Bestand am 25.08.2026, ruff 0.16.0, ueber server/: 0 Treffer, nachdem # 162 maschinell behoben wurden. I001 ist stable, nicht im Preview. # # WARUM NICHT DIE FAMILIE "I", obwohl sie bei null steht: I002 # (missing-required-import) wirkt nur mit gesetztem required-imports, und # ruff.toml setzt es nicht — gemessen mit --show-settings: # linter.isort.required_imports = []. Sie waere eine Wand ohne Deckung, die # nie feuern kann und trotzdem null sagt. Derselbe Fall wie W505. # # REICHWEITE, gemessen am 25.08.2026: Ein konstruierter Verstoss — zwei # Standardbibliotheks-Importe in falscher Reihenfolge in server/api/health.py # — wurde im echten Riegelpfad gemeldet, mit Rueckgabewert ungleich null. # Die Null sagt damit "kein Verstoss vorhanden". # # Was diese Wand NICHT leistet: Sie prueft die REIHENFOLGE, nicht die # SCHICHTGRENZEN. `15_ARCHITEKTUR.md` §1 verlangt eine Importwurzel; ein # Import quer durch die Schichten ist korrekt sortiert und passiert diese # Wand ohne Widerspruch. Dafuer braucht es den Import-Pruefer aus # `19_WERKZEUGE/der-satz.md`, den es noch nicht gibt. "I001", # B (flake8-bugbear) — haeufige Fallen. Aufgenommen sind 37 der 39 stabilen # Regeln, einzeln aufgezaehlt und NICHT als Kuerzel. # # Bestand am 25.08.2026, ruff 0.16.0, ueber server/: 0 Treffer, nachdem der # letzte behoben wurde (B905, `zip()` ohne `strict=` in einem Zeugen ueber # ast.Dict — dort paaren sich keys und values per Konstruktion, `strict=True` # macht daraus eine Zusicherung). # # WARUM NICHT DAS KUERZEL "B": Vier der 43 Regeln stehen im Preview — B043, # B901, B903, B909. Aufnahmekriterium 2 schliesst das aus; eine Wand, die # mit der naechsten Werkzeugversion um vier Regeln waechst, gegen die der # Bestand nie geprueft wurde, ist keine. Derselbe Grund wie bei "W". # # ZWEI STABILE REGELN SIND TROTZDEM DRAUSSEN, und der Grund ist der von # W505 — eine Wand ohne Deckung. Gemessen am 25.08.2026 an einem # konstruierten Verstoss je Regel: # B911 (batched-without-explicit-strict) stumm bei py312, meldet ab py313 # B912 (map-without-explicit-strict) stumm bei py312, meldet ab py314 # `itertools.batched(..., strict=)` gibt es erst ab Python 3.13, # `map(..., strict=)` erst ab 3.14. Bei target-version = "py312" koennen # beide nie feuern und wuerden trotzdem null sagen. Sie gehoeren hier hinein, # sobald die Container-Version steigt — und nicht frueher. # # REICHWEITE, gemessen am 25.08.2026: Fuer **jede** der 39 stabilen Regeln # wurde ein Verstoss konstruiert und unter py312 geprueft. 37 von 39 melden. # Drei brauchten einen zweiten Anlauf, und der Grund gehoert dazu: # B005 feuert NUR, wenn das strip-Argument ein Zeichen WIEDERHOLT. # `s.strip("xyz")` und `"text".strip("prefix_")` bleiben stumm, # `s.strip("aab")` und `s.rstrip("//")` melden. Ein mehrzeichiger # strip-Aufruf ohne Dublette ist fuer diese Wand unsichtbar. # B008 feuert nicht bei jedem Aufruf im Vorgabewert — `os.getpid()` blieb # stumm, ein Aufruf mit veraenderlicher Rueckgabe meldet. # B026 braucht ein Schluesselwort-Argument VOR dem `*` (`f(kw=1, *werte)`); # `f(*werte, **kw)` ist die erlaubte Form und bleibt stumm. # # Positivkontrolle im echten Riegelpfad: Eine Probedatei mit allen 39 # Verstoessen lag unter server/, der Lauf endete mit Rueckgabewert 1; nach # ihrer Entfernung mit 0. "B002", "B003", "B004", "B005", "B006", "B007", "B008", "B009", "B010", "B011", "B012", "B013", "B014", "B015", "B016", "B017", "B018", "B019", "B020", "B021", "B022", "B023", "B024", "B025", "B026", "B027", "B028", "B029", "B030", "B031", "B032", "B033", "B034", "B035", "B039", "B904", "B905", ]