# Architektur-Übersicht: Adapter-Out, CLI & Bootstrap Diese Datei beschreibt die drei Module `pdf-umbenenner-adapter-out`, `pdf-umbenenner-adapter-in-cli` und `pdf-umbenenner-bootstrap`: ihren Zweck, ihre Paketstruktur, die wichtigsten Klassen und die Verdrahtungslogik beim Programmstart. Sie richtet sich an Entwickler, die in einem dieser Module arbeiten wollen und noch keinen Überblick über das Projekt haben. Domain- und Application-Schicht (Port-Verträge, fachliche Domänenobjekte, Use-Case-Interfaces) sind nicht Gegenstand dieses Dokuments – sie sind in `docs/architecture/domain-overview.md` beschrieben. GUI-interne Ports und die Struktur des GUI-Adapters finden sich in `docs/architecture/gui-overview.md`. Die hexagonale Abhängigkeitsrichtung ist strikt: Adapter kennen Domain und Application, nicht umgekehrt. Adapter dürfen außerdem nicht direkt voneinander abhängen. --- ## 1. Modulzweck ### pdf-umbenenner-adapter-out Enthält alle Outbound-Adapter-Implementierungen, also die konkreten technischen Lösungen für sämtliche Outbound-Ports der Application. Dazu gehören: Dateisystemzugriff, PDF-Textextraktion via PDFBox, SQLite-Persistenz (Schema, Repositories, Unit of Work), HTTP-Clients für zwei KI-Provider-Familien (OpenAI-kompatibel und Anthropic nativ), Properties-Konfiguration inklusive Legacy-Migration, dateibasierter Run-Lock sowie Systemuhr und SHA-256-Fingerprint. ### pdf-umbenenner-adapter-in-cli Schlanker Inbound-Adapter für den kopflosen Batch-Betrieb. Enthält genau eine Klasse (`SchedulerBatchCommand`), die den CLI-Einstiegspunkt bildet und ausschließlich über das Inbound-Port-Interface an die Application delegiert. Keine eigene Fachlogik. ### pdf-umbenenner-bootstrap Composition Root der Anwendung. Verantwortlich für: CLI-Argument-Parsing, Konfigurationsauflösung und -validierung, Aufbau des vollständigen Objektgraphen (manuell, ohne DI-Framework), Auswahl der aktiven KI-Adapter-Implementierung, Dispatch auf GUI- oder Headless-Pfad sowie Exit-Code-Ableitung. Bootstrap ist die einzige Stelle, an der alle Module zusammengeführt werden. --- ## 2. Paketstruktur ### pdf-umbenenner-adapter-out Wurzelpaket: `de.gecheckt.pdf.umbenenner.adapter.out` | Unterpaket | Inhalt | |-------------------------|-------------------------------------------------------------------------------------| | `.ai` | HTTP-Adapter für OpenAI-kompatible Schnittstelle und Anthropic Messages API | | `.clock` | Systemuhr-Adapter (`Instant.now()`) | | `.configuration` | Properties-Laden, Multi-Provider-Parsing/-Validierung, Legacy-Migration | | `.fingerprint` | SHA-256-Inhalts-Fingerprint | | `.lock` | Dateibasierter Run-Lock | | `.modelcatalog` | HTTP-Modellabruf für den GUI-Konfigurationseditor | | `.pathcheck` | Pfadprüfung für den GUI-Editor | | `.pdfextraction` | PDFBox-3.x-Adapter: Textextraktion und Seitenanzahl | | `.prompt` | Prompt-Template-Lader | | `.resourcecreation` | Anlegen von Ordnern und Dateien (korrigierende technische Tests) | | `.sourcedocument` | Quellordner-Scanner (nicht rekursiv) | | `.sqlite` | Schema-Initialisierung, Repositories, Unit of Work | | `.targetcopy` | Zielkopie via Temp-Datei und atomarem Move | | `.targetfolder` | Kollisionsfreier Zieldateiname, Umbenennung bestehender Zieldateien | | `.validation` | API-Key-Auflösung aus Umgebungsvariablen (GUI-Editor) | | `.bootstrap.validation` | `StartConfiguration`-Validierung vor Prozessstart | ### pdf-umbenenner-adapter-in-cli Wurzelpaket: `de.gecheckt.pdf.umbenenner.adapter.in.cli` Enthält ausschließlich `SchedulerBatchCommand` sowie die zugehörige `package-info.java`. ### pdf-umbenenner-bootstrap Wurzelpaket: `de.gecheckt.pdf.umbenenner.bootstrap` | Unterpaket | Inhalt | |---------------------|-------------------------------------------------------------------------------------------------------| | *(Wurzel)* | `PdfUmbenennerApplication` (main), `BootstrapRunner`, `AiProviderSelector` | | `.adapter` | Bootstrap-interne Adapter: `Log4jProcessingLogger`, `GuiConfigurationPropertiesWriter`, `AiModelCatalogDispatcher` | | `.singleinstance` | `SingleInstanceGuard` – Einzelinstanz-Schutz via Loopback-ServerSocket | | `.startup` | `StartupMode`, `StartupArguments`, `CliArgumentParser` | --- ## 3. Schlüsselklassen Die folgenden Klassen sind für das Verständnis der drei Module zentral. FQN-Kürzel: `...` steht jeweils für das Wurzelpaket des Moduls. ### Adapter-Out #### KI-Adapter - **`...ai.OpenAiHttpAdapter`** – implementiert `AiInvocationPort` für OpenAI-kompatible Endpunkte. POST `{baseUrl}/v1/chat/completions`, Bearer-Authentifizierung, extrahiert `choices[0].message.content`, klassifiziert HTTP-Fehler und Timeouts als `AiInvocationTechnicalFailure`. - **`...ai.AnthropicClaudeHttpAdapter`** – implementiert `AiInvocationPort` für die native Anthropic Messages API. POST `/v1/messages`, Header `x-api-key` und `anthropic-version`, konkateniert `text`-Content-Blöcke aus dem Antwort-Array. Beide Adapter liefern denselben Domain-Typ (`NamingProposal`) und enthalten keinerlei provider-spezifische Typen in öffentlichen Signaturen. Welche Implementierung aktiv ist, entscheidet ausschließlich der Bootstrap (→ `AiProviderSelector`). #### Modell-Katalog (GUI) - **`...modelcatalog.ClaudeModelCatalogAdapter`** – `AiModelCatalogPort` für Claude, GET `/v1/models` mit `x-api-key`. - **`...modelcatalog.OpenAiCompatibleModelCatalogAdapter`** – `AiModelCatalogPort` für OpenAI-kompatibel, GET `/v1/models` mit Bearer. #### PDF-Extraktion - **`...pdfextraction.PdfTextExtractionPortAdapter`** – PDFBox-3.x-Adapter. Alle technischen Fehler werden als `PdfExtractionTechnicalError` zurückgegeben; es werden keine Exceptions propagiert. #### SQLite - **`...sqlite.SqliteSchemaInitializationAdapter`** – Flyway-basierte Schema-Initialisierung mit `V1__initial_schema.sql`. Drei-Fall-Strategie: leere Datenbank (Flyway führt das Skript vollständig aus), bestehender Datenbestand ohne Flyway-History (Schema-Prüfung, datiertes Backup, dann Baseline-Eintrag ohne Skriptausführung), regulärer Folgestart mit Flyway-History (idempotenter Lauf). Foreign-Key-Durchsetzung via `SQLiteConfig.enforceForeignKeys(true)` auf DataSource-Ebene, sodass jede neue Verbindung automatisch `PRAGMA foreign_keys = ON` erhält. - **`...sqlite.SqliteUnitOfWorkAdapter`** – implementiert `UnitOfWorkPort`. Setzt `autoCommit=false`, führt atomare Commits durch, rollt bei Fehlern zurück. Die innere `TransactionOperations`-Implementierung wurde um `resetDocumentStatusForRetry(DocumentFingerprint)` erweitert: setzt feldgenau `overall_status = 'READY_FOR_AI'`, `content_error_count = 0`, `transient_error_count = 0`, `last_failure_instant = NULL`; alle anderen Felder und alle `processing_attempt`-Einträge bleiben unangetastet. - **`...sqlite.SqliteDocumentRecordRepositoryAdapter`** – Stammsatz pro SHA-256-Fingerprint (Gesamtstatus, Fehlerzähler, Zieldateiname usw.). - **`...sqlite.SqliteProcessingAttemptRepositoryAdapter`** – Versuchshistorie, referenziert über Fingerprint. Enthält u. a. Provider-Identifikator, Modellname, Prompt-Identifikator, KI-Rohantwort und finalen Zieldateinamen. - **`...sqlite.SqliteHistoryQueryAdapter`** – implementiert `HistoryQueryPort`. Kapselt alle lesenden Datenbankoperationen für den Historien-Tab: Übersicht (`loadOverview` mit Sortierung `updated_at DESC, fingerprint ASC`, LIMIT 501-Strategie, case-insensitive Freitextsuche via `LOWER()` mit Sonderzeichen-Escape für `%` und `_`), Stammsatz-Lookup (`findRecordByFingerprint`) und Versuchshistorie (`findAttemptsByFingerprint`). #### Konfiguration - **`...configuration.PropertiesConfigurationPortAdapter`** – implementiert `ConfigurationPort`. Lädt `config/application.properties` (oder einen `--config`-Override), parst via `MultiProviderConfigurationParser`, löst API-Keys aus Umgebungsvariablen (`OPENAI_COMPATIBLE_API_KEY`, `ANTHROPIC_API_KEY`). - **`...configuration.LegacyConfigurationMigrator`** – erkennt alte Flat-Key-Konfigurationen (Schlüssel wie `api.baseUrl`, `api.model`), legt eine `.bak`-Sicherung an und überführt den Inhalt in das aktuelle Multi-Provider-Schema. #### Prompt-Adapter - **`...prompt.FilesystemPromptPortAdapter`** – implementiert `PromptPort`. Lädt das Prompt-Template aus einer externen Datei und leitet den Identifikator aus dem Dateinamen ab. Die neue Methode `savePrompt(String content)` schreibt den Inhalt atomar: temporäre Datei im selben Verzeichnis anlegen (gleiche Partition), Inhalt in UTF-8 schreiben, dann `ATOMIC_MOVE` zur Zieldatei. Kein stiller Fallback bei `AtomicMoveNotSupportedException`. Der Pfad stammt aus der Adapter-internen Konfiguration, nicht aus dem Port-Aufruf. #### Laufzeitinfrastruktur - **`...lock.FilesystemRunLockPortAdapter`** – Lock-Datei mit PID-Inhalt. Wirft `RunLockUnavailableException`, wenn die Datei bereits vorhanden ist. Release löscht die Datei (best-effort). - **`...clock.SystemClockAdapter`** – delegiert an `Instant.now()`. - **`...fingerprint.Sha256FingerprintAdapter`** – SHA-256 über den Rohdatei-Inhalt. Fehler als `FingerprintTechnicalError`. #### Zieldatei - **`...targetcopy.FilesystemTargetFileCopyAdapter`** – kopiert die Quelldatei zunächst in eine `.tmp`-Datei, dann atomarer Move (Fallback: Standard-Move). Die Quelldatei wird in keinem Fall verändert. - **`...targetfolder.FilesystemTargetFolderAdapter`** – ermittelt einen kollisionsfreien Zieldateinamen mit `(1)`, `(2)`-Suffix. Erkennt inhaltsidentische Duplikate via SHA-256. #### Validierung vor Prozessstart - **`...bootstrap.validation.StartConfigurationValidator`** – validiert die geladene `StartConfiguration` auf Pflichtfelder, Wertebereiche, URI-Syntax und Pfadbedingungen. Wird im Bootstrap-Headless-Pfad unmittelbar nach dem Laden der Konfiguration aufgerufen. --- ### Adapter-In-CLI - **`...adapter.in.cli.SchedulerBatchCommand`** – einziger Inbound-Adapter für den Headless-Betrieb. Nimmt einen `BatchRunContext` entgegen, delegiert an `BatchRunProcessingUseCase.execute()` und gibt `BatchRunOutcome` zurück. Enthält keine eigene Fachlogik; die Verdrahtung mit dem Use-Case-Interface erfolgt ausschließlich im Bootstrap. --- ### Bootstrap - **`...bootstrap.PdfUmbenennerApplication`** – `main`-Methode. Parst CLI-Argumente via `CliArgumentParser`, bricht bei ungültiger Verwendung mit Exit-Code 1 ab, delegiert an `BootstrapRunner.run()` und ruft abschließend `System.exit()` mit dem zurückgegebenen Code auf. - **`...bootstrap.BootstrapRunner`** – Herzstück der Verdrahtung. Baut den Objektgraph für Headless- und GUI-Pfad, dispatcht über `StartupMode`, enthält `buildProductionBatchUseCase()` und `runHeadlessBatch()` als zentrale Kompositionsmethoden, liefert den Exit-Code zurück. - **`...bootstrap.AiProviderSelector`** – einzige Stelle, an der `AiProviderFamily` auf eine konkrete `AiInvocationPort`-Implementierung abgebildet wird: `OPENAI_COMPATIBLE` → `OpenAiHttpAdapter`, `CLAUDE` → `AnthropicClaudeHttpAdapter`. - **`...bootstrap.startup.CliArgumentParser`** – parst `--headless` und `--config ` zu einem typsicheren `StartupArgumentsParseResult` (sealed: `Valid` / `Invalid`). - **`...bootstrap.singleinstance.SingleInstanceGuard`** – bindet einen Loopback-ServerSocket auf Port 47832. Wirft `AnotherInstanceRunningException`, wenn der Port bereits belegt ist. Ein Shutdown-Hook gibt den Socket frei. - **`...bootstrap.adapter.AiModelCatalogDispatcher`** – Bootstrap-interner Dispatcher für die GUI. Routet `AiModelCatalogPort`-Aufrufe anhand des `providerIdentifier` an den Claude- oder OpenAI-kompatiblen Modell-Katalog-Adapter. Thread-safe. - **`...bootstrap.ApplicationVersionProvider`** – statische Hilfsklasse ohne Zustand. Liest `Implementation-Version` aus dem Paket-Manifest via `getClass().getPackage().getImplementationVersion()`. Fallback `"dev"` bei IDE-Start und ungepacktem Betrieb (kein Manifest-Eintrag vorhanden). Der aufgelöste Wert wird im GUI-Pfad in `GuiStartupContext.applicationVersion` eingesetzt. - **`...bootstrap.adapter.Log4jProcessingLogger`** – implementiert `ProcessingLogger` auf Basis von Log4j2. Unterdrückt sensitive KI-Inhalte, wenn `AiContentSensitivity.PROTECT_SENSITIVE_CONTENT` gesetzt ist. - **`...bootstrap.adapter.GuiConfigurationPropertiesWriter`** – schreibt die im GUI-Editor bearbeitete Konfiguration als normalisierte `application.properties` zurück auf das Dateisystem. --- ## 4. Verdrahtungslogik in Bootstrap Die folgende Sequenz beschreibt den Ablauf von `main()` bis zum Start des eigentlichen Adapters. Der Objektgraph wird ausschließlich durch manuelle `new`-Aufrufe aufgebaut; es wird kein DI-Framework verwendet. **Argument-Parsing** - `PdfUmbenennerApplication.main()` → `CliArgumentParser.parse(args)` - Ergebnis `Invalid` → Exit-Code 1, keine weiteren Schritte **Einzelinstanz-Schutz** - `BootstrapRunner.run()` → `SingleInstanceGuard.acquire()` - `AnotherInstanceRunningException` → Exit-Code 1; im GUI-Modus zusätzlich ein Swing-Warndialog **Modus-Dispatch** - `BootstrapRunner.run()` wertet `startupArguments.mode()` aus: - `HEADLESS` → `runHeadlessBatch()` - `GUI` → `startGuiMode()` **Konfigurationsauflösung (Headless-Pfad)** - Prüfung, ob `--config`-Datei existiert (Fehler → Exit-Code 1) - `LegacyConfigurationMigrator.migrateIfLegacy()` bei erkannter Legacy-Form - `PropertiesConfigurationPortAdapter` lädt und parst die Properties - `StartConfigurationValidator` validiert die geladene `StartConfiguration` - Validierungsfehler → Exit-Code 1 **KI-Provider-Auswahl** - Innerhalb von `buildProductionBatchUseCase()`: `multiProviderConfiguration().activeProviderFamily()` → `AiProviderSelector.select(family, providerConfig)` - Ergebnis: genau eine `AiInvocationPort`-Instanz **Objektgraph-Aufbau (Headless)** - Erzeugte Instanzen (Reihenfolge nach Abhängigkeit): `Sha256FingerprintAdapter`, `SqliteDocumentRecordRepositoryAdapter`, `SqliteProcessingAttemptRepositoryAdapter`, `SqliteUnitOfWorkAdapter`, `FilesystemTargetFolderAdapter`, `FilesystemTargetFileCopyAdapter`, `FilesystemPromptPortAdapter`, `SystemClockAdapter`, `SourceDocumentCandidatesPortAdapter`, `PdfTextExtractionPortAdapter`, `Log4jProcessingLogger` - Application-Services (`DocumentProcessingCoordinator`, `AiResponseValidator`, `AiNamingService`) werden verdrahtet und in `DefaultBatchRunProcessingUseCase` eingebettet **CLI-Adapter** - `BootstrapRunner` erzeugt `SchedulerBatchCommand` mit dem fertigen `BatchRunProcessingUseCase` **Exit-Code-Ableitung** - `BatchRunOutcome` → 0 (Lauf technisch erfolgreich) oder 1 (harter Bootstrap-/Konfigurationsfehler) - `PdfUmbenennerApplication` ruft `System.exit(exitCode)` auf **GUI-Pfad** - `startGuiMode()` baut via `buildGuiStartupContext()` einen `GuiStartupContext`: enthält `AiModelCatalogDispatcher`, `EnvironmentApiKeyResolutionAdapter`, `TechnicalTestOrchestrator`, `GuiConfigurationPropertiesWriter` - Bootstrap verdrahtet zusätzlich vier neue History-Use-Cases (`DefaultHistoryOverviewUseCase`, `DefaultHistoryDetailsUseCase`, `DefaultHistoryResetDocumentStatusUseCase`, `DefaultDeleteDocumentHistoryUseCase`) und den `DefaultPromptEditorUseCase` als anonyme Bridge-Implementierungen in den `GuiStartupContext` - `ApplicationVersionProvider.resolveVersion()` wird aufgerufen und der Wert in `GuiStartupContext.applicationVersion` gesetzt - Wenn eine Konfigurationsdatei beim Start bekannt ist, erzeugt Bootstrap zusätzlich einen vollständig verdrahteten `GuiPromptEditorPort` (kombiniert `FilesystemPromptPortAdapter` mit `DefaultPromptEditorUseCase`); ohne Konfiguration erhält der Context einen No-Op-Port - `GuiAdapter.start(context)` übernimmt; ab diesem Punkt liegt die Kontrolle beim GUI-Adapter - Im GUI-Pfad: keine SQLite-Schema-Initialisierung beim Start, kein Run-Lock-Erwerb, kein Batch-Use-Case; History-Operationen initialisieren die Schema-Verbindung ad-hoc pro Aufruf - GUI-interne Ports und deren Verbindung mit Outbound-Adaptern sind in `docs/architecture/gui-overview.md` beschrieben --- ## 5. Einstiegspunkte je Modul ### pdf-umbenenner-adapter-out 1. **`...ai.OpenAiHttpAdapter`** – zeigt das typische Adapter-Muster: Port-Interface implementieren, alle provider-spezifischen Details kapseln, `ProviderConfiguration` als einzige Konfigurationsquelle konsumieren. Danach `AnthropicClaudeHttpAdapter` zum Vergleich lesen. 2. **`...sqlite.SqliteSchemaInitializationAdapter`** – erklärt das Datenbankschema, das alle SQLite-Adapter voraussetzen. Hier sieht man, welche Felder in `document_record` und `processing_attempt` existieren und wie Schema-Evolution additiv umgesetzt ist. 3. **`...configuration.PropertiesConfigurationPortAdapter`** – Einstieg in die Konfigurationskette. Von hier aus `MultiProviderConfigurationParser` und `LegacyConfigurationMigrator` nachverfolgen. ### pdf-umbenenner-adapter-in-cli 1. **`...adapter.in.cli.SchedulerBatchCommand`** – komprimiertes Inbound-Adapter-Muster in einer einzigen Klasse. Zeigt, wie ein Inbound-Adapter ausschließlich über Port-Interfaces mit der Application kommuniziert. 2. **`package-info.java`** – beschreibt Abhängigkeitsrichtung und Verdrahtungsvertrag dieses Adapters. 3. **`SchedulerBatchCommandTest`** – zeigt, wie der Adapter ohne Bootstrap testbar ist. ### pdf-umbenenner-bootstrap 1. **`PdfUmbenennerApplication`** – Startpunkt; die kurze Kette von `main()` bis `System.exit()` gibt einen ersten Überblick über die gesamte Startsequenz. 2. **`BootstrapRunner`** – Herzstück; `buildProductionBatchUseCase()` zeigt, wie der vollständige Objektgraph manuell aufgebaut wird. `runHeadlessBatch()` zeigt den Headless-Kontrollfluss. 3. **`AiProviderSelector`** – kleinste Klasse mit größter Hebelwirkung: hier liegt die einzige Stelle, an der die Provider-Auswahl aus der Konfiguration auf eine konkrete `AiInvocationPort`-Implementierung trifft. --- *Port-Verträge und Domain-Typen: `docs/architecture/domain-overview.md`* *GUI-interne Ports und GUI-Adapter-Struktur: `docs/architecture/gui-overview.md`*