# AGENTS.md **Entwicklerrichtlinien für DocuMentor (KI-Coding-Agenten)** Diese Datei enthält wichtige Richtlinien für Coding-Agenten, die in diesem Repository arbeiten. ## Sprache **Alle Kommunikation, Code-Kommentare und UI-Texte auf Deutsch!** - Kommentare: Deutsch - Docstrings: Deutsch - Log-Meldungen: Deutsch - Variablennamen: Deutsch wo kontextuell passend - UI-Labels: Deutsch ## Build & Lint Kommandos ### Paketmanager: `uv` (NICHT pip oder poetry!) ```bash # Abhängigkeiten installieren uv sync # Anwendung starten uv run python src/main.py # Code-Style prüfen (Zeilenlänge: 120) uv run ruff check # Code automatisch formatieren uv run ruff format ``` ### Tests ausführen ```bash # Alle Hash-Tests uv run python test_hash_implementation.py # Duplikatserkennung testen uv run python test_xml_hash_duplicate_detection.py # Einzelnen Test ausführen (direkter Python-Aufruf) uv run python test_hash_implementation.py ``` **Hinweis:** Dieses Projekt verwendet KEINE pytest/unittest-Frameworks. Tests sind standalone Python-Skripte mit if __name__ == "__main__". ## Code-Style-Richtlinien ### Import-Organisation **Reihenfolge (keine Leerzeilen zwischen Gruppen):** ```python # 1. Standard Library import os import sys import logging from pathlib import Path from typing import Optional, TYPE_CHECKING # 2. Drittanbieter (Third-Party) from PySide6.QtCore import Qt, QThread, Signal from PySide6.QtWidgets import QDialog, QMainWindow from pydantic import BaseModel, Field # 3. Lokale Imports (IMMER absolute Imports, KEINE relativen .imports) from conf import app_settings, TreeNode, XslFile from ui.MainWindow import MainWindow from ui.JavaVmConfigDialog_ui import Ui_JavaVmConfigDialog ``` **Wichtig:** - Immer `from pathlib import Path` verwenden - NIEMALS String-Pfade verwenden, IMMER `Path`-Objekte - `TYPE_CHECKING` für zirkuläre Import-Vermeidung nutzen - Keine relativen Imports (`.` oder `..`) ### Type Annotations **Moderne Union-Syntax verwenden:** ```python # RICHTIG def transform(xml_path: Path, params: dict[str, str]) -> tuple[bool, str]: result: str | None = None files: list[Path] = [] # FALSCH def transform(xml_path, params): # Keine Annotations result: Optional[str] = None # Alte Union-Syntax files: List[Path] = [] # Großgeschriebene Types ``` **Pflicht:** - Alle Funktionsparameter annotieren - Alle Rückgabewerte annotieren - Moderne Syntax: `str | None` statt `Optional[str]` - Container: `list[str]`, `dict[str, str]`, `tuple[int, int]` ### Naming Conventions ```python # Klassen: PascalCase class SaxonWorkerPool: class TransformationJob: # Funktionen/Methoden: snake_case def transform_saxon(xml_file: Path) -> bool: def calculate_hash(content: bytes) -> str: # Private Methoden: _snake_case mit Unterstrich def _create_tree_item(self, node: TreeNode): def _load_project_data(self): # Variablen: snake_case xml_file_path = Path("test.xml") diff_pdf_count = 0 self.current_zoom = 100 # Konstanten: UPPER_CASE SAXON_WORKER_JAVA = """...""" MAX_RETRY_COUNT = 3 ``` ### Formatierung & Linting - **Zeilenlänge:** 120 Zeichen (via Ruff konfiguriert) - **Strings:** Bevorzugt Double-Quotes `"..."`, aber konsistent im File - **Trailing Commas:** Bei mehrzeiligen Strukturen verwenden - **Ruff:** Alle Warnings beheben vor Commit ### Error Handling **IMMER Logging statt print() verwenden:** ```python import logging logger = logging.getLogger(__name__) def transform(xml_path: Path) -> tuple[bool, str]: try: # Operation durchführen logger.info(f"Transformation gestartet: {xml_path}") result = do_transform(xml_path) logger.debug(f"Zwischenergebnis: {result}") return True, "Erfolg" except FileNotFoundError as e: error_msg = f"XML-Datei nicht gefunden: {xml_path}" logger.error(error_msg) return False, error_msg except Exception as e: error_msg = f"Fehler bei Transformation: {str(e)}" logger.exception(error_msg) # Mit Stack Trace return False, error_msg ``` **Pattern:** - `logger.debug()` für Debugging-Infos - `logger.info()` für normale Operationen - `logger.warning()` für Warnungen - `logger.error()` für Fehler ohne Stack Trace - `logger.exception()` für Fehler MIT Stack Trace - Fehlermeldungen auf Deutsch ### Docstrings **Google-Style auf Deutsch:** ```python def transform_xml_to_pdf(xml_path: Path, xsl_path: Path, output_dir: Path) -> tuple[bool, str]: """ Transformiert eine XML-Datei mit XSL zu PDF. Args: xml_path: Pfad zur XML-Eingabedatei xsl_path: Pfad zum XSL-Stylesheet output_dir: Zielverzeichnis für PDF-Ausgabe Returns: tuple[bool, str]: (Erfolg, Fehlermeldung oder Info-Text) Raises: FileNotFoundError: Wenn XML- oder XSL-Datei nicht existiert """ ``` **Modul-Docstrings am Dateianfang:** ```python """ Saxon Worker Pool - Persistente JVM-Prozesse für schnelle XSLT-Transformationen. Eliminiert JVM-Startup-Overhead durch Vorinitialisierung von N Worker-Prozessen. Verwendet multiprocessing.Queue für Thread-sichere Kommunikation. """ ``` ## Pydantic Models ### Definition ```python from pydantic import BaseModel, Field class Project(BaseModel): id: int = Field(..., description="Eindeutige Projekt-ID", gt=0) name: str = Field(..., description="Projekt-Name", min_length=1, max_length=255) project_dir: Path = Field(..., description="Pfad zum Projekt-Verzeichnis") # Helper-Methoden direkt im Model erlaubt def getJavaVm(self) -> str: global app_settings value = [x.version for x in app_settings.java_vms if x.id == self.java_vm_id] return value[0] if len(value) else "" ``` ### Settings speichern ```python from conf import app_settings, ProjectData # Globale Einstellungen app_settings.theme = "Fusion" app_settings.save() # WICHTIG: Nicht vergessen! # Projekteinstellungen project_data = ProjectData.readSettings(project_dir) project_data.nodes.append(new_node) project_data.writeSettings(project_dir) # WICHTIG: Persistieren! ``` ## PySide6 UI-Integration ### KRITISCH: UI-Dateien nicht manuell bearbeiten! ``` src/ui/ ├── MainWindow.ui # Qt Designer Datei (editieren erlaubt) ├── MainWinddow_ui.py # AUTO-GENERIERT (NICHT BEARBEITEN!) └── MainWindow.py # Implementierung (hier Code schreiben) ``` ### UI-Import-Pattern ```python # In src/ui/JavaVmConfigDialog.py from PySide6.QtWidgets import QDialog from ui.JavaVmConfigDialog_ui import Ui_JavaVmConfigDialog class JavaVmConfigDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) # UI einrichten self.ui = Ui_JavaVmConfigDialog() self.ui.setupUi(self) # Signale NACH setupUi() verbinden self.ui.browseButton.clicked.connect(self._browse_file) def _browse_file(self): # Widgets über self.ui.widgetName zugreifen current_path = self.ui.pathLineEdit.text() ... ``` **Wichtig:** - UI-Klassen NIEMALS direkt erben, nur als `self.ui` Member - Alle Widgets über `self.ui.widgetName` zugreifen - Signal-Verbindungen immer NACH `setupUi()` aufrufen ## Projektstruktur-Änderungen Beim Modifizieren der Baumstruktur (TreeNode, XslFile, XmlFile): ```python # 1. ProjectData modifizieren self.project_data.nodes.append(new_node) # 2. SOFORT persistieren self.project_data.writeSettings(self.project.project_dir) # 3. UI neu laden self._load_nodes_to_tree() ``` **Pattern:** Immer in dieser Reihenfolge: Modifizieren → Speichern → UI aktualisieren ## Wichtige Konventionen ### Pathlib IMMER verwenden ```python # RICHTIG from pathlib import Path xml_path = Path("data/test.xml") xml_path = Path.home() / ".config" / "app" / "config.json" xml_path = Path(os.path.expandvars("$HOME/data")).expanduser() if xml_path.exists(): content = xml_path.read_text(encoding="utf-8") # FALSCH xml_path = "data/test.xml" # String-Pfad xml_path = os.path.join("data", "test.xml") # os.path statt pathlib ``` ### Globale Singletons ```python # In conf.py am Modulende app_settings = AppSettings() # In anderen Modulen from conf import app_settings # Verwendung java_vm = [x for x in app_settings.java_vms if x.id == vm_id][0] ``` ### Thread-basierte Operationen ```python from PySide6.QtCore import QThread, Signal class HashCalculatorThread(QThread): # Signale für Thread-sichere Kommunikation progress = Signal(int) finished = Signal(dict) def __init__(self, files: list[Path]): super().__init__() self.files = files def run(self): for i, file_path in enumerate(self.files): hash_value = calculate_hash(file_path) self.progress.emit(i + 1) self.finished.emit(results) # Verwendung thread = HashCalculatorThread(xml_files) thread.progress.connect(self._on_progress) thread.finished.connect(self._on_finished) thread.start() # NICHT run() direkt aufrufen! ``` ## RAM-Optimierung Da DocuMentor permanent läuft, sparsam mit RAM umgehen: - Worker-Pools nach Verwendung herunterfahren - Große Datenstrukturen frühzeitig freigeben - Polars DataFrames statt Pandas (geringerer RAM-Verbrauch) - Lazy Loading wo möglich --- **Zusammenfassung:** Deutsch sprechen, pathlib verwenden, Typen annotieren, Ruff nutzen, UI-Dateien nicht anfassen!