Files
2026-04-30 15:29:07 +02:00

357 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <Pfad>` 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`*