1
0

Umsetzung von M1

This commit is contained in:
2026-04-20 10:11:19 +02:00
parent cd6e5221aa
commit b5044f62a9
59 changed files with 5891 additions and 884 deletions
+2
View File
@@ -3,3 +3,5 @@
.settings/ .settings/
target/ target/
.claude/settings.local.json .claude/settings.local.json
logs/
dependency-reduced-pom.xml
+86 -56
View File
@@ -1,18 +1,28 @@
---
model: sonnet
---
# AP05 Befundmodell mit Spec-/Diagnose-Trennung # AP05 Befundmodell mit Spec-/Diagnose-Trennung
> **Meilenstein:** M1
> **Vorgänger:** AP03, AP04 ✅
> **Nachfolger:** AP06, AP07, AP09
> **Grundlage:** `docs/specs/technik-und-architektur.md` v5, §§ „Ergebnis- und Befundmodell", „Befundarten", „Gültigkeitsentscheidung und Exit-Codes"
> **Entscheidungsprotokoll:** `docs/arbeitspakete/m1/E00-entscheidungsprotokoll.md`
## Ziel ## Ziel
Im Paket `domain` ein **stabiles Befundmodell** einführen, das von Anfang an zwischen **Spec-Urteil** (verbindliches Prüfurteil gemäß Spezifikation) und **diagnostischer Weiteranalyse** (zusätzliche, das Spec-Urteil nicht verändernde Hinweise) unterscheidet. Dieses Modell ist das **Herzstück** für alle nachfolgenden Meilensteine. Im Paket `domain` ein **stabiles Befundmodell** einführen, das von Anfang an zwischen **Spec-Urteil** (verbindliches Prüfurteil gemäß Spezifikation) und **diagnostischer Weiteranalyse** (zusätzliche, das Spec-Urteil nicht verändernde Hinweise) unterscheidet. Dieses Modell ist das **Herzstück** für alle nachfolgenden Meilensteine.
Der bestehende Typ `de.gecheckt.asv.validation.model.ValidationResult` wird nicht einfach gelöscht, sondern **ersetzt** durch ein sauber geschnittenes Domain-Modell. Der alte Typ wird im Rahmen von AP09 eingefroren. Der bestehende Typ `de.gecheckt.asv.validation.model.ValidationResult` wird nicht geändert — er wird in AP09 eingefroren.
## Voraussetzungen ## Voraussetzungen
- AP03 (Paketstruktur) - AP03 (Paketstruktur vorhanden)
- AP04 (Logging-Adapter)
## Scope IN ## Scope IN
Folgende Typen im Paket `de.gecheckt.asv.domain.finding` (oder ähnliches Unterpaket von `domain`): Folgende Typen im Paket `de.gecheckt.asv.domain.finding`:
### `Severity` (Enum) ### `Severity` (Enum)
- `ERROR` - `ERROR`
@@ -21,88 +31,108 @@ Folgende Typen im Paket `de.gecheckt.asv.domain.finding` (oder ähnliches Unterp
### `FindingKind` (Enum) ### `FindingKind` (Enum)
- `SPEC` — Befund ist Teil des Spec-Urteils, beeinflusst `Verdict` - `SPEC` — Befund ist Teil des Spec-Urteils, beeinflusst `Verdict`
- `DIAGNOSTIC` — zusätzliche Weiteranalyse, beeinflusst `Verdict` **nicht** - `DIAGNOSTIC` — zusätzliche Weiteranalyse, beeinflusst `Verdict` **niemals**
### `FindingLayer` (Enum) ### `FindingLayer` (Enum)
- `ARTIFACT` — äußeres Artefakt / Dateiebene - `ARTIFACT` — äußeres Artefakt / Dateiebene
- `TECHNICAL_STRUCTURE` — Service-Segmente, KKS, Transport - `TECHNICAL_STRUCTURE` — Service-Segmente, KKS, Transport
- `DOMAIN_MODEL` — kanonisches Fachmodell (ASVREC/ASVFEH) - `DOMAIN_MODEL` — kanonisches Fachmodell (ASVREC/ASVFEH)
### `Finding` (Record) ### `Finding` (Record oder unveränderliche Klasse)
Alle Pflichtfelder laut `technik-und-architektur.md` §„Befundarten":
```java ```java
public record Finding( public record Finding(
FindingKind kind, // SPEC oder DIAGNOSTIC FindingKind kind, // SPEC oder DIAGNOSTIC
Severity severity, // ERROR/WARNING/HINT Severity severity, // ERROR/WARNING/HINT
FindingLayer layer, // ARTIFACT/TECHNICAL_STRUCTURE/DOMAIN_MODEL FindingLayer layer, // ARTIFACT/TECHNICAL_STRUCTURE/DOMAIN_MODEL
String ruleId, // interne Regel-ID, optional null String ruleId, // interne Regel-ID, nullable
String officialErrorCode, // offizieller Fehlercode, optional null String officialErrorCode, // offizieller Spec-Fehlercode, nullable
String segmentType, // z.B. "UNB", optional String segmentType, // z.B. "UNB", nullable
Integer segmentIndex, // optional Integer segmentIndex, // nullable
String fieldId, // z.B. "UNB_0020", optional String fieldId, // z.B. "UNB_0020", nullable
String rawValue, // Rohwert, optional String rawValue, // Rohwert, nullable
Integer position, // Byte-/Zeichenposition, optional Integer position, // Byte-/Zeichenposition, nullable
String messageReference, // UNH 0062 bei Nachrichtenbezug, optional String messageReference, // UNH 0062 bei Nachrichtenbezug, nullable
String germanMessage // deutscher Befundtext String germanMessage // deutscher Befundtext, nicht nullable
) {} ) {}
``` ```
Records mit vielen optionalen Feldern sind unschön — als Alternative ist eine reguläre Klasse mit Builder erlaubt, solange sie unveränderlich (`final`) ist und die gleichen Felder trägt. **Wichtig ist: unveränderlich und mit allen Metadaten aus `technik-und-architektur.md` Abschnitt „Befundarten".**
Records mit vielen optionalen Feldern sind unschön — als Alternative ist eine reguläre unveränderliche Klasse mit Builder erlaubt, solange alle Felder vorhanden sind.
### `Verdict` (Enum) ### `Verdict` (Enum)
- `VALID` — keine Spec-ERROR-Befunde - `VALID` — keine SPEC-ERROR-Befunde
- `INVALID` — mindestens ein Spec-ERROR-Befund - `INVALID` — mindestens ein SPEC-ERROR-Befund
- `OPERATIONAL_ERROR` — Bedienfehler (Exit-Code 2) - `OPERATIONAL_ERROR` — Bedienfehler (Exit-Code 2), wird in AP08 genutzt
### `ValidationReport` (Klasse) ### `ValidationReport` (unveränderliche Klasse)
- unveränderlich
- enthält:
- `List<Finding> findings` (alle Befunde, SPEC und DIAGNOSTIC gemischt, Reihenfolge erhalten)
- Methode `Verdict computeVerdict()` — berücksichtigt **nur** `kind == SPEC` und `severity == ERROR`
- Methode `List<Finding> specFindings()` — filtert auf `kind == SPEC`
- Methode `List<Finding> diagnosticFindings()` — filtert auf `kind == DIAGNOSTIC`
- Methode `boolean hasSpecErrors()`
- Metadaten: `String fileName`, `Instant timestamp`
- **invariant:** eine `DIAGNOSTIC`-Severity `ERROR` darf das Verdict **niemals** auf `INVALID` setzen. Dies ist per Unit-Test abzusichern.
### Unit-Tests ```java
- `ValidationReport` ohne Befunde → `Verdict.VALID` public final class ValidationReport {
- `ValidationReport` mit einem SPEC-ERROR → `Verdict.INVALID` // Metadaten
- `ValidationReport` mit einem DIAGNOSTIC-ERROR → `Verdict.VALID` (dieser Test ist kritisch!) String fileName;
- `ValidationReport` mit SPEC-WARNING → `Verdict.VALID` (nur ERROR zählt) Instant timestamp;
- `specFindings()` / `diagnosticFindings()` filtern korrekt // Befunde
- Unveränderlichkeit: `findings`-Liste ist nicht modifizierbar List<Finding> findings; // unveränderlich
// Kern-Methoden
Verdict computeVerdict(); // NUR SPEC+ERROR zählt
boolean hasSpecErrors();
List<Finding> specFindings();
List<Finding> diagnosticFindings();
// Factory für Bedienfehler (AP08)
static ValidationReport operationalError(String fileName, String ruleId, String message);
}
```
**Invariante:** `computeVerdict()` berücksichtigt **ausschließlich** Findings mit `kind == SPEC` und `severity == ERROR`. Ein `DIAGNOSTIC`-ERROR darf das Verdict **niemals** auf `INVALID` setzen. Dies ist per Unit-Test abzusichern.
### Unit-Tests (Mindestanforderung)
1. Leerer Report → `Verdict.VALID`
2. Ein SPEC-ERROR → `Verdict.INVALID`
3. **Ein DIAGNOSTIC-ERROR → `Verdict.VALID`** (dieser Test ist kritisch und muss explizit vorhanden sein)
4. SPEC-WARNING → `Verdict.VALID` (nur ERROR zählt)
5. `specFindings()` / `diagnosticFindings()` filtern korrekt
6. `findings`-Liste ist nicht von außen modifizierbar
7. `operationalError(...)``Verdict.OPERATIONAL_ERROR`
## Scope OUT ## Scope OUT
- Integration des neuen Modells in den bestehenden Lauf (kommt in AP06) - Integration des neuen Modells in den bestehenden Lauf (AP06)
- Löschen oder Umbenennen der alten `validation.model.ValidationResult`-Klasse (das ist Teil von AP09) - Löschen oder Umbenennen der alten `ValidationResult`-Klasse (AP09)
- Berichtserzeugung (Text-Rendering), Bericht-Format, Konsolenausgabe (kommt in AP07) - Berichtserzeugung, Textrendering, Konsolenausgabe (AP07)
- Architekturtest (kommt in AP10) - Architekturtest (AP10)
## Schritte ## Schritte
1. Branch `m1/ap05-befundmodell` 1. Paket `de.gecheckt.asv.domain.finding` anlegen
2. Paket `de.gecheckt.asv.domain.finding` anlegen 2. Enums `Severity`, `FindingKind`, `FindingLayer` implementieren
3. Enums und Klassen implementieren 3. `Finding` implementieren
4. Unit-Tests schreiben, mindestens die sechs oben genannten 4. `Verdict` implementieren
5. `mvn clean verify` grün bekommen 5. `ValidationReport` implementieren
6. Commit `M1-AP05: Befundmodell mit Spec-/Diagnose-Trennung` 6. Unit-Tests schreiben — mindestens die sieben oben genannten
7. Abschlussbericht schreiben 7. `mvn clean verify` grün bekommen
8. Abschlussbericht schreiben
## Abnahmekriterien ## Abnahmekriterien
- Paket `domain.finding` enthält alle oben genannten Typen - [ ] Paket `domain.finding` enthält alle genannten Typen
- **Der Test „DIAGNOSTIC-ERROR ergibt VALID" ist grün** und wird im Bericht explizit genannt - [ ] **Test „DIAGNOSTIC-ERROR ergibt VALID" ist grün** und im Bericht explizit genannt
- `ValidationReport.findings` ist unveränderlich (Test vorhanden) - [ ] `ValidationReport.findings` ist unveränderlich (Test vorhanden)
- alle Metadaten-Felder aus `technik-und-architektur.md` Abschnitt „Befundarten" sind im `Finding`-Typ vorhanden - [ ] Alle Metadatenfelder aus `technik-und-architektur.md` §„Befundarten" sind im `Finding`-Typ vorhanden
- `mvn clean verify` ist grün - [ ] `operationalError(...)` Factory-Methode existiert
- keine Änderung an `validation.model.ValidationResult` (Altmodell) - [ ] Keine Änderung an `ValidationResult` (Altmodell)
- Abschlussbericht liegt vor - [ ] `mvn clean verify` grün
- [ ] Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP05-bericht.md`
## Rest-Risiken und offene Punkte ## Rest-Risiken und offene Punkte
- Wir haben jetzt zwei parallele Ergebnis-Typen: den alten `ValidationResult` und den neuen `ValidationReport`. Das ist **Absicht** bis AP09, wo die alte Logik sauber eingefroren wird. - Zwei parallele Ergebnistypen (`ValidationResult` alt, `ValidationReport` neu) sind bis AP09 Absicht.
- Das Befundmodell ist bewusst **keine** Hierarchie (Datei → Schicht → Nachricht → Befund), sondern eine flache Liste mit Metadaten. Die hierarchische Berichtserzeugung passiert später auf Basis dieser Metadaten in M9. Für M1 genügt die flache Struktur. - Das Befundmodell ist bewusst eine **flache Liste** mit Metadaten, keine Hierarchie. Die hierarchische Berichtserzeugung kommt in M9.
## Bericht ## Bericht
`docs/arbeitspakete/m1/berichte/AP05-bericht.md` nach `templates/ap-bericht.md`. `docs/arbeitspakete/m1/berichte/AP05-bericht.md` nach `docs/arbeitspakete/m1/templates/ap-bericht.md`.
+128 -55
View File
@@ -1,13 +1,22 @@
---
model: sonnet
---
# AP06 Bootstrap und CLI-Adapter # AP06 Bootstrap und CLI-Adapter
> **Meilenstein:** M1
> **Vorgänger:** AP05 ✅ erforderlich
> **Nachfolger:** AP07, AP09
> **Grundlage:** `docs/specs/technik-und-architektur.md` v5, §§ „CLI-Zuschnitt", „Laufzeit- und Betriebsmodell", „Gültigkeitsentscheidung und Exit-Codes", „Zeichensätze"
> **Entscheidungsprotokoll:** `docs/arbeitspakete/m1/E00-entscheidungsprotokoll.md` (E-05 fat JAR)
## Ziel ## Ziel
Die bestehende `AsvValidatorApplication` wird in zwei klar getrennte Verantwortlichkeiten zerlegt: Die bestehende `AsvValidatorApplication` wird in zwei klar getrennte Verantwortlichkeiten zerlegt:
1. **Bootstrap** (`de.gecheckt.asv.bootstrap.Main`) — verdrahtet die Komponenten manuell per Constructor Injection und ist der einzige `public static void main`. 1. **Bootstrap** (`de.gecheckt.asv.bootstrap.Main`) — verdrahtet alle Komponenten manuell per Constructor Injection, ist der einzige `public static void main`
2. **CLI-Adapter** (`de.gecheckt.asv.adapter.in.cli.CliRunner` oder ähnlich) — nimmt CLI-Argumente entgegen, ruft die Application-Schicht auf, übersetzt das Ergebnis in einen Exit-Code. 2. **CLI-Adapter** (`de.gecheckt.asv.adapter.in.cli.CliRunner`) — nimmt CLI-Argumente entgegen, ruft Application-Schicht auf, übersetzt Ergebnis in Exit-Code
Zusätzlich werden die **Exit-Codes spec-konform** auf `0/1/2` umgestellt. Zusätzlich werden Exit-Codes spec-konform auf `0/1/2` umgestellt, ISO-8859-15 als Eingabe-Encoding eingeführt, und das Uber-JAR via `maven-shade-plugin` gebaut.
## Voraussetzungen ## Voraussetzungen
@@ -15,82 +24,146 @@ Zusätzlich werden die **Exit-Codes spec-konform** auf `0/1/2` umgestellt.
## Scope IN ## Scope IN
### Bootstrap ### Bootstrap (`de.gecheckt.asv.bootstrap.Main`)
- Klasse `de.gecheckt.asv.bootstrap.Main` mit `public static void main(String[] args)`
- verdrahtet manuell: - `public static void main(String[] args)`
- Logging-Konfigurator - Verdrahtet manuell per Constructor Injection:
- CLI-Runner - `LoggingConfigurator`
- (Application-Service — Platzhalter, wird in AP09 feingeschnitten) - `CliRunner`
- ruft `CliRunner.run(args)` auf, gibt den zurückgegebenen Exit-Code an `System.exit` weiter - Application-Service (in M1 noch Dummy-Pfad — Datei lesen, leeren `ValidationReport` zurückgeben)
- Ruft `CliRunner.run(args)` auf und gibt den Exit-Code an `System.exit` weiter
- Log4j2-Typen dürfen **nur** hier und in `adapter.out.logging` sichtbar sein
### CLI-Adapter (`de.gecheckt.asv.adapter.in.cli.CliRunner`)
### CLI-Adapter
- Klasse `CliRunner` im Paket `adapter.in.cli`
- Methode `int run(String[] args)` - Methode `int run(String[] args)`
- akzeptiert **genau ein Positionsargument**: den Pfad zur Eingabedatei - Akzeptiert **genau ein Positionsargument**: Pfad zur Eingabedatei
- bei 0 oder ≥2 Argumenten → Exit-Code `2`, Minimalbericht-Vorbereitung (vollständige Minimalbericht-Logik kommt in AP08) - Bei 0 oder ≥ 2 Argumenten → Exit-Code `2`, kurze deutsche Fehlermeldung auf STDERR (vollständiger Minimalbericht kommt in AP08)
- bei nicht existierender, nicht lesbarer oder nicht regulärer Eingabedatei → Exit-Code `2` - Bei nicht existierender, nicht lesbarer oder nicht regulärer Eingabedatei → Exit-Code `2`
- bei erfolgreichem Lauf ohne Spec-Fehler → Exit-Code `0` - Bei erfolgreichem Lauf ohne Spec-Fehler → Exit-Code `0`
- bei erfolgreichem Lauf mit Spec-Fehlern → Exit-Code `1` - Bei erfolgreichem Lauf mit Spec-Fehlern → Exit-Code `1`
### Exit-Code-Konstanten ### Exit-Code-Konstanten
- Konstanten **nur noch in einer Klasse** (z.B. `ExitCode` im Paket `adapter.in.cli`)
- Werte: Konstanten **nur noch in einer Klasse** (z.B. `ExitCode` im Paket `adapter.in.cli`):
- `0` = gültig, keine Fehler-Befunde
- `1` = ungültig, mindestens ein Spec-Fehler ```java
- `2` = Bedienfehler public final class ExitCode {
- Die alten Konstanten (`EXIT_CODE_INVALID_ARGUMENTS=1`, `EXIT_CODE_FILE_ERROR=2`, `EXIT_CODE_VALIDATION_ERRORS=3`) werden **gelöscht** public static final int VALID = 0;
public static final int INVALID = 1;
public static final int OPERATIONAL_ERROR = 2;
private ExitCode() {}
}
```
Die alten Konstanten (`EXIT_CODE_INVALID_ARGUMENTS=1`, `EXIT_CODE_FILE_ERROR=2`, `EXIT_CODE_VALIDATION_ERRORS=3`) werden **gelöscht**.
### Verdrahtung mit Befundmodell (AP05) ### Verdrahtung mit Befundmodell (AP05)
- `CliRunner` gibt am Ende einen `ValidationReport` aus AP05 weiter oder erzeugt selbst einen Minimal-`ValidationReport` mit einem `Finding` des Kinds `SPEC`, Severity `ERROR`, Layer `ARTIFACT` im Bedienfehlerfall
- das eigentliche Einlesen und Verarbeiten der Datei kann für M1 noch ein **Dummy-Pfad** sein: die Datei wird gelesen, die Bytes gezählt, ein leerer `ValidationReport` mit `fileName` und `timestamp` zurückgegeben. Echte Validierung gehört nicht in M1. - `CliRunner` nutzt `ValidationReport.computeVerdict()` zur Exit-Code-Ableitung
- Im Bedienfehlerfall: `ValidationReport.operationalError(...)` → Exit-Code `2`
- Der eigentliche M1-Dummy-Pfad: Datei wird gelesen (ISO-8859-15), Bytes gezählt, leerer `ValidationReport` mit `fileName` und `timestamp` zurückgegeben. **Keine echte Validierung in M1.**
### Zeichensatz-Korrektur ### Zeichensatz-Korrektur
- beim Einlesen der Eingabedatei wird **ISO 8859-15** verwendet, nicht UTF-8. Das ist eine harte Spec-Vorgabe aus `fachliche-anforderungen.md` §5.1 und muss ab jetzt dauerhaft so bleiben.
```java Beim Einlesen der Eingabedatei wird **ISO-8859-15** verwendet:
Files.readString(path, StandardCharsets.ISO_8859_1); // nicht ideal — besser Charset.forName("ISO-8859-15")
``` ```java
Achtung: Java kennt `StandardCharsets.ISO_8859_1`, aber **nicht** `ISO_8859_15`. Daher `Charset.forName("ISO-8859-15")` verwenden. // Korrekt:
Charset iso = Charset.forName("ISO-8859-15");
// NICHT: StandardCharsets.UTF_8
// NICHT: Charset.defaultCharset()
```
JDK-21-Verfügbarkeit per Test belegen: Eine Datei mit Byte `0xA4` ergibt beim Einlesen das Euro-Zeichen `€`, weil `0xA4` in ISO-8859-15 auf Euro-Zeichen liegt.
### Uber-JAR via `maven-shade-plugin`
`maven-jar-plugin`-Platzhalter aus AP02 ersetzen durch `maven-shade-plugin`:
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version><!-- aktuell stabil --></version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<transformers>
<transformer implementation=
"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>de.gecheckt.asv.bootstrap.Main</mainClass>
</transformer>
<!-- Log4j2 PluginCache zusammenführen -->
<transformer implementation=
"org.apache.maven.plugins.shade.resource.Log4j2PluginsCacheFileTransformer"/>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
```
`java -jar target/asv-format-validator-*.jar <datei>` muss ohne `-cp` funktionieren.
### `.gitignore` — `logs/` ergänzen
`logs/` zum `.gitignore` hinzufügen (gemäß E-04 aus Entscheidungsprotokoll). Der statische Dateipfad aus AP04 wird nach AP07 durch dynamische Logdatei ersetzt.
## Scope OUT ## Scope OUT
- Berichtdatei und Log-Datei im Eingabeverzeichnis (AP07) - Berichtdatei und Log-Datei im Eingabeverzeichnis (AP07)
- vollständiger Minimalbericht bei Exit-Code `2` (AP08) - Vollständiger Minimalbericht bei Exit-Code `2` (AP08)
- Entkopplung der Altlogik (AP09) - Entkopplung der Altlogik (AP09)
- Architekturtest (AP10) - Architekturtest (AP10)
## Schritte ## Schritte
1. Branch `m1/ap06-bootstrap-cli` 1. `de.gecheckt.asv.bootstrap.Main` anlegen
2. `de.gecheckt.asv.bootstrap.Main` anlegen mit `public static void main` 2. `CliRunner` in `adapter.in.cli` anlegen
3. `CliRunner` in `adapter.in.cli` anlegen 3. `ExitCode`-Konstantenklasse anlegen
4. `ExitCode`-Konstantenklasse anlegen 4. `AsvValidatorApplication` schrittweise entkernen: Code wandert nach `CliRunner` und `Main`
5. `AsvValidatorApplication` schrittweise entkernen: Code wandert nach `CliRunner` und `Main` 5. Einlese-Encoding auf `Charset.forName("ISO-8859-15")` umstellen
6. Einlese-Encoding auf `Charset.forName("ISO-8859-15")` umstellen 6. `maven-shade-plugin` in `pom.xml` einbinden, `maven-jar-plugin`-Platzhalter entfernen
7. `maven-jar-plugin` in `pom.xml` auf `de.gecheckt.asv.bootstrap.Main` setzen (Platzhalter aus AP02 konkretisieren) 7. `logs/` in `.gitignore` ergänzen
8. Alle Tests, die auf `AsvValidatorApplication` direkt zeigen, auf `CliRunner` umziehen 8. Alle Tests die auf `AsvValidatorApplication` direkt zeigen auf `CliRunner` umziehen
9. `mvn clean package` laufen lassen, das erzeugte JAR manuell mit `java -jar target/asv-format-validator-*.jar <test-datei>` prüfen 9. `mvn clean package` erzeugtes JAR mit `java -jar target/asv-format-validator-*.jar <testdatei>` manuell prüfen
10. `mvn clean verify` grün bekommen 10. `mvn clean verify` grün bekommen
11. Commit `M1-AP06: Bootstrap, CLI-Adapter, Exit-Codes 0/1/2, ISO 8859-15` 11. Abschlussbericht schreiben
12. Abschlussbericht schreiben
## Abnahmekriterien ## Abnahmekriterien
- `de.gecheckt.asv.bootstrap.Main` existiert und ist `Main-Class` des JAR - [ ] `de.gecheckt.asv.bootstrap.Main` existiert und ist `Main-Class` des Uber-JAR
- `CliRunner` ist der einzige Ort mit CLI-Argument-Parsing - [ ] `CliRunner` ist der einzige Ort mit CLI-Argument-Parsing
- Exit-Codes `0`, `1`, `2` sind definiert und spec-konform eingesetzt - [ ] Exit-Codes `0`, `1`, `2` sind definiert und spec-konform eingesetzt, kein Exit-Code `3` mehr erreichbar
- **Test:** Aufruf ohne Argument → Exit-Code `2` - [ ] Test: Aufruf ohne Argument → Exit-Code `2`
- **Test:** Aufruf mit nicht existierender Datei → Exit-Code `2` - [ ] Test: Aufruf mit nicht existierender Datei → Exit-Code `2`
- **Test:** Aufruf mit leerer, lesbarer Datei → Exit-Code `0` (Dummy-Pfad, leerer `ValidationReport`) - [ ] Test: Aufruf mit leerer, lesbarer Datei → Exit-Code `0`
- Einlese-Encoding ist ISO 8859-15 (per Test belegt: eine Datei mit Byte `0xA4` ergibt beim Einlesen ``, weil `0xA4` in ISO 8859-15 auf Euro-Zeichen liegt) - [ ] Einlese-Encoding ist ISO-8859-15 (per Test belegt: Byte `0xA4``€`)
- ausführbares JAR unter `target/` ist manuell startbar - [ ] `java -jar target/asv-format-validator-*.jar <datei>` startet ohne `-cp`
- `mvn clean verify` ist grün - [ ] `logs/` in `.gitignore`
- Abschlussbericht liegt vor - [ ] Keine Log4j2-Typen außerhalb von `bootstrap` und `adapter.out.logging`
- [ ] `mvn clean verify` grün
- [ ] Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP06-bericht.md`
## Rest-Risiken und offene Punkte ## Rest-Risiken und offene Punkte
- Der Dummy-Pfad (Datei wird gelesen, leerer Report zurückgegeben) ist bewusst dünn. Die Einbindung der alten Parser-Logik passiert in AP09. - Der Dummy-Pfad (Datei lesen, leerer Report) ist bewusst dünn. Echte Parser-/Validator-Einbindung kommt in M3+.
- Es ist verlockend, schon hier in AP06 die bestehende Parser-/Validator-Kette mit dem neuen Modell zu verknüpfen. **Nicht tun.** AP06 soll nur die äußere Hülle geradeziehen. - Nicht versuchen, in AP06 schon die alte Parser-/Validator-Kette mit dem neuen Modell zu verknüpfen — das ist AP09-Scope.
- `maven-shade-plugin` mit Log4j2 benötigt den `Log4j2PluginsCacheFileTransformer`, sonst werden Log4j2-Plugins nicht korrekt geladen.
## Bericht ## Bericht
`docs/arbeitspakete/m1/berichte/AP06-bericht.md` nach `templates/ap-bericht.md`. `docs/arbeitspakete/m1/berichte/AP06-bericht.md` nach `docs/arbeitspakete/m1/templates/ap-bericht.md`.
+82 -46
View File
@@ -1,12 +1,21 @@
---
model: sonnet
---
# AP07 Ausgabeartefakte: Berichtdatei und Log-Datei mit Suffix-Logik # AP07 Ausgabeartefakte: Berichtdatei und Log-Datei mit Suffix-Logik
> **Meilenstein:** M1
> **Vorgänger:** AP04, AP05, AP06 ✅ erforderlich
> **Nachfolger:** AP08, AP10
> **Grundlage:** `docs/specs/technik-und-architektur.md` v5, §§ „Ausgabeartefakte", „Zeichensätze", „Logging und Berichtserzeugung"
> **Entscheidungsprotokoll:** `docs/arbeitspakete/m1/E00-entscheidungsprotokoll.md` (E-04 logs/-Verzeichnis)
## Ziel ## Ziel
Pro Lauf werden **zwei Ausgabedateien** im **Verzeichnis der Eingabedatei** erzeugt: Pro Lauf werden **zwei Ausgabedateien** im **Verzeichnis der Eingabedatei** erzeugt:
- eine **Berichtdatei** `<basename>.txt` - eine **Berichtdatei** `<basename>.txt`
- eine **Log-Datei** `<basename>.log` - eine **Log-Datei** `<basename>.log`
Beide in **UTF-8**. Zusätzlich wird der Bericht weiterhin in die **Konsole** geschrieben. Wenn bereits gleichnamige Dateien existieren, werden neue mit laufendem Suffix `_v1`, `_v2`, … erzeugt, **pro Eingabedatei-Basisname**. Beide in **UTF-8**. Der Bericht wird zusätzlich in die **Konsole** geschrieben. Bei bereits vorhandenen Dateien gleichen Namens werden neue mit laufendem Suffix `_v1`, `_v2`, … erzeugt.
## Voraussetzungen ## Voraussetzungen
@@ -14,69 +23,96 @@ Beide in **UTF-8**. Zusätzlich wird der Bericht weiterhin in die **Konsole** ge
## Scope IN ## Scope IN
### Berichtdatei ### `SuffixResolver` im Paket `adapter.out.filesystem`
- Klasse `ReportFileWriter` oder ähnlich im Paket `adapter.out.reporting`
- Eingabe: `ValidationReport` (aus AP05) und Eingabedatei-Pfad
- Ausgabe: eine UTF-8-Textdatei im **selben Verzeichnis** wie die Eingabedatei
- Dateiname: `<basename-der-eingabedatei>.txt`, bei Konflikt `<basename>_v1.txt`, `<basename>_v2.txt`, …
- Inhalt für M1: **einfach gehalten**. Pro `Finding` eine Zeile mit den wichtigsten Metadaten (Severity, Kind, Layer, Feld-ID, deutsche Nachricht). Kopfzeile mit Dateiname, Zeitstempel, Verdict. Die fein strukturierte hierarchische Darstellung kommt erst in M9.
### Log-Datei ```java
- Wiederverwendung des `LoggingConfigurator` aus AP04 public class SuffixResolver {
- Methode `configureLogFile(Path logFile)` wird im Bootstrap **vor** dem CLI-Runner aufgerufen /**
- Log-Datei liegt im **selben Verzeichnis** wie die Eingabedatei * Ermittelt den ersten freien Dateipfad für den gegebenen Basisnamen
- Dateiname: `<basename>.log`, Suffix-Logik analog zur Berichtdatei * und die gegebene Extension im Zielverzeichnis.
- Log4j2 wird programmatisch umkonfiguriert: der File-Appender schreibt nach dem neuen Pfad. Die statische `log4j2.xml` aus AP04 ist nur der Fallback für „kein Eingabeargument". * Probiert: <baseName>.<ext>, dann <baseName>_v1.<ext>, <baseName>_v2.<ext>, ...
*/
public Path resolveNextFreePath(Path directory, String baseName, String extension) { ... }
}
```
### Suffix-Logik - Suffix-Zählung ist **pro Extension unabhängig**`.txt` und `.log` haben getrennte Zähler
- eigene kleine Utility-Klasse `SuffixResolver` im Paket `adapter.out.filesystem` - Unit-Tests mindestens für: keine Datei vorhanden, `.txt` vorhanden, `.txt` + `_v1` vorhanden
- Methode `Path resolveNextFreePath(Path baseDirectory, String baseName, String extension)`:
- probiert `<baseName>.<ext>`, dann `<baseName>_v1.<ext>`, `<baseName>_v2.<ext>`, … ### `ReportFileWriter` im Paket `adapter.out.reporting`
- gibt den ersten freien Pfad zurück
- wird sowohl für die Berichtdatei als auch für die Log-Datei verwendet - Eingabe: `ValidationReport` (AP05) + Eingabedatei-Pfad
- **Hinweis:** Die Zählung ist pro Basisname unabhängig. Wenn für `test.auf.txt` schon `test.auf_v1.txt` existiert, aber für `test.auf.log` noch keine `_v1`, kann das vorkommen — die Suffixe müssen **nicht synchron** sein. - Ausgabe: UTF-8-Textdatei im Verzeichnis der Eingabedatei
- Dateiname via `SuffixResolver`
- **Berichtinhalt für M1** (absichtlich minimal, wird in M9 ausgebaut):
- Kopfzeile: Zeitstempel, Eingabedatei, Verdict
- Pro `Finding`: eine Zeile mit Severity, Kind, Layer, Feld-ID, deutscher Meldung
- Fußzeile: Hinweis auf bewusst nicht geprüfte Bereiche
Alle Texte auf **Deutsch**, Encoding **UTF-8** explizit — kein Plattform-Default.
### `LoggingConfigurator.configureLogFile(Path)` in `adapter.out.logging`
- Methode wird im Bootstrap **vor** dem ersten fachlichen Log-Aufruf aufgerufen
- Konfiguriert Log4j2-File-Appender programmatisch auf den gewünschten Pfad
- Dateiname via `SuffixResolver` (analog zur Berichtdatei, eigene Zählung)
- Log4j2-Typen (`LoggerContext`, `Appender` etc.) bleiben **ausschließlich** in `adapter.out.logging` und `bootstrap`
- Statischer `logs/`-Pfad aus `log4j2.xml` (AP04) wird entfernt oder auf Fallback-Default gesetzt, der nur greift wenn `configureLogFile` nicht aufgerufen wurde (z.B. für Testläufe)
- **Fallback:** Falls programmatische Log4j2-Umkonfiguration sich als instabil erweist, ist eine System-Property-basierte Konfiguration (`-Dasv.log.file=...`) ein zulässiger Ausweg — Entscheidung im Bericht dokumentieren
### Integration in `bootstrap.Main`
Reihenfolge vor dem Validierungslauf:
1. Eingabedatei-Pfad bestimmen
2. Basisnamen und Zielverzeichnis ableiten
3. `SuffixResolver` für `.log` aufrufen
4. `LoggingConfigurator.configureLogFile(logPath)` aufrufen → ab jetzt gehen Logs in die Datei
5. Validierungslauf starten
6. `ReportFileWriter` schreiben
7. Konsolenausgabe (identisch zum Berichtinhalt)
### Konsolenausgabe ### Konsolenausgabe
- bleibt erhalten, schreibt denselben Bericht-Text wie die Berichtdatei
- Konsolenausgabe erfolgt **nach** der Berichtdatei-Erstellung, damit ein IO-Fehler beim Schreiben der Berichtdatei nicht die Konsolenausgabe verhindert - Schreibt denselben Text wie die Berichtdatei
- Erfolgt **nach** der Berichtdatei-Erstellung, damit ein IO-Fehler beim Schreiben die Konsolenausgabe nicht verhindert
## Scope OUT ## Scope OUT
- hierarchische Berichtsgliederung (M9) - Hierarchische Berichtsgliederung (M9)
- Einfärbung / ANSI-Codes in der Konsole - ANSI-Farben / Einfärbung in der Konsole
- Log-Rotation - Log-Rotation
- Minimalbericht bei Exit-Code 2 (AP08) - Minimalbericht bei Exit-Code `2` (AP08)
## Schritte ## Schritte
1. Branch `m1/ap07-ausgabeartefakte` 1. `SuffixResolver` implementieren inkl. Unit-Tests
2. `SuffixResolver` implementieren inkl. Unit-Tests für: keine Datei vorhanden, `.txt` vorhanden, `.txt` + `_v1` vorhanden, mehrere Lücken 2. `ReportFileWriter` implementieren mit einfachem Zeilenformat für M1
3. `ReportFileWriter` implementieren mit einfachem Zeilenformat für M1 3. `LoggingConfigurator.configureLogFile(Path)` implementieren
4. `LoggingConfigurator.configureLogFile(Path)` implementieren (programmatische Log4j2-Reconfiguration) 4. Bootstrap erweitern: Log-Datei-Pfad bestimmen → `LoggingConfigurator` aufrufen
5. Bootstrap erweitern: vor CLI-Lauf → Log-Datei-Pfad bestimmen → `LoggingConfigurator` aufrufen 5. `CliRunner` erweitern: nach Lauf → Berichtdatei schreiben → Konsolenausgabe
6. CLI-Runner erweitert: nach Lauf → Berichtdatei schreiben → Konsolenausgabe erzeugen 6. End-to-End-Test: beide Ausgabedateien entstehen, Suffix-Logik funktioniert, UTF-8
7. End-to-End-Test mit Dummy-Eingabedatei: beide Ausgabedateien entstehen, Suffix-Logik funktioniert 7. `mvn clean verify` grün bekommen
8. `mvn clean verify` grün 8. Abschlussbericht schreiben
9. Commit `M1-AP07: Berichtdatei und Log-Datei im Eingabeverzeichnis mit Suffix-Logik`
10. Abschlussbericht schreiben
## Abnahmekriterien ## Abnahmekriterien
- nach einem Lauf mit Eingabedatei `foo/bar.auf` existieren `foo/bar.auf.txt` und `foo/bar.auf.log` - [ ] Nach Lauf mit `foo/bar.auf` entstehen `foo/bar.auf.txt` und `foo/bar.auf.log`
- zweiter Lauf mit derselben Eingabedatei erzeugt `foo/bar.auf_v1.txt` und `foo/bar.auf_v1.log` - [ ] Zweiter Lauf mit derselben Datei → `foo/bar.auf_v1.txt` und `foo/bar.auf_v1.log`
- dritter Lauf → `_v2` usw. - [ ] Dritter Lauf → `_v2` usw.
- beide Ausgabedateien sind UTF-8 - [ ] Beide Ausgabedateien sind UTF-8
- Konsolenausgabe ist identisch zum Inhalt der Berichtdatei - [ ] Konsolenausgabe ist identisch zum Inhalt der Berichtdatei
- `SuffixResolver` hat mindestens vier Unit-Tests - [ ] `SuffixResolver` hat mindestens drei Unit-Tests
- `mvn clean verify` ist grün - [ ] Log4j2-Typen sind außerhalb von `adapter.out.logging` und `bootstrap` nicht sichtbar
- Abschlussbericht liegt vor - [ ] Statischer `logs/`-Pfad aus `log4j2.xml` entfernt oder auf Fallback-Default gesetzt
- [ ] `mvn clean verify` grün
- [ ] Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP07-bericht.md`
## Rest-Risiken und offene Punkte ## Rest-Risiken und offene Punkte
- **Race Conditions** bei gleichzeitigen Läufen auf derselben Eingabedatei: laut `technik-und-architektur.md` bewusst nicht behandelt (kein Mehrbenutzerbetrieb in V1). - Race Conditions bei gleichzeitigen Läufen auf derselben Eingabedatei: laut `technik-und-architektur.md` bewusst nicht behandelt (kein Mehrbenutzerbetrieb in V1).
- Die programmatische Log4j2-Reconfiguration ist technisch nicht ganz ohne. Falls sie sich als zu instabil erweist, ist eine **sys-property-basierte** Konfiguration der Log-Datei (`-Dasv.log.file=...`) ein zulässiger Fallback. Entscheidung im Bericht dokumentieren.
- Das Bericht-Dateiformat ist in M1 absichtlich primitiv. In M9 wird es durch die finale hierarchische Struktur ersetzt. - Das Bericht-Dateiformat ist in M1 absichtlich primitiv. In M9 wird es durch die finale hierarchische Struktur ersetzt.
- Fallback bei instabiler programmatischer Log4j2-Umkonfiguration im Bericht dokumentieren.
## Bericht ## Bericht
`docs/arbeitspakete/m1/berichte/AP07-bericht.md` nach `templates/ap-bericht.md`. `docs/arbeitspakete/m1/berichte/AP07-bericht.md` nach `docs/arbeitspakete/m1/templates/ap-bericht.md`.
+50 -44
View File
@@ -1,8 +1,16 @@
---
model: sonnet
---
# AP08 Minimalbericht bei Bedienfehlern (Exit-Code 2) # AP08 Minimalbericht bei Bedienfehlern (Exit-Code 2)
> **Meilenstein:** M1
> **Vorgänger:** AP06, AP07 ✅ erforderlich
> **Nachfolger:** AP10, AP11
> **Grundlage:** `docs/specs/technik-und-architektur.md` v5, §§ „Gültigkeitsentscheidung und Exit-Codes", „Laufzeit- und Betriebsmodell"
## Ziel ## Ziel
Auch bei **Bedien- oder Zugriffsfehlern** (Exit-Code `2`) soll ein **Minimalbericht** entstehen, der den Fehler nachvollziehbar beschreibt. Das verlangt `technik-und-architektur.md` ausdrücklich: „Auch bei Exit-Code 2 soll, soweit technisch möglich, ein Minimalbericht erzeugt werden, der den Bedien- oder Zugriffsfehler nachvollziehbar beschreibt." Auch bei **Bedien- oder Zugriffsfehlern** (Exit-Code `2`) entsteht ein **Minimalbericht**, der den Fehler nachvollziehbar beschreibt. `technik-und-architektur.md` verlangt das ausdrücklich: „Auch bei Exit-Code 2 soll, soweit technisch möglich, ein Minimalbericht erzeugt werden."
## Voraussetzungen ## Voraussetzungen
@@ -10,71 +18,69 @@ Auch bei **Bedien- oder Zugriffsfehlern** (Exit-Code `2`) soll ein **Minimalberi
## Scope IN ## Scope IN
### Bedienfehler-Fälle ### Bedienfehler-Fälle (alle müssen Exit-Code `2` + Minimalbericht ergeben)
Alle diese Fälle müssen in Exit-Code `2` mit Minimalbericht resultieren:
1. **Kein Argument übergeben** → Minimalbericht auf Konsole (Eingabeverzeichnis unbekannt, also keine Dateiausgabe möglich) 1. **Kein Argument** → nur Konsolenausgabe (Eingabeverzeichnis unbekannt)
2. **Mehr als ein Argument**Minimalbericht auf Konsole 2. **Mehr als ein Argument**nur Konsolenausgabe
3. **Eingabedatei existiert nicht**Minimalbericht auf Konsole und falls möglich in das übergeordnete Verzeichnis (dort wo die Datei hätte liegen sollen), sonst nur Konsole 3. **Eingabedatei existiert nicht**Konsolenausgabe + Berichtdatei im übergeordneten Verzeichnis, sofern dieses schreibbar ist
4. **Eingabepfad ist kein regulärer Dateityp** (z.B. Verzeichnis) → Minimalbericht auf Konsole, keine Dateiausgabe 4. **Eingabepfad ist kein regulärer Dateityp** (z.B. Verzeichnis) → nur Konsolenausgabe
5. **Eingabedatei ist nicht lesbar** (Permissions) → Minimalbericht auf Konsole, Dateiausgabe wird versucht wenn Zielverzeichnis schreibbar ist, sonst nur Konsole 5. **Eingabedatei ist nicht lesbar** (Berechtigungen) → Konsolenausgabe + Berichtdatei sofern Zielverzeichnis schreibbar
### Minimalbericht-Inhalt ### Minimalbericht-Inhalt
Der Minimalbericht ist ein **`ValidationReport`** (aus AP05) mit:
- `fileName` = übergebener Pfad (oder Platzhalter `<kein Argument>` bzw. `<mehrere Argumente>`) Ein `ValidationReport` (AP05) via `ValidationReport.operationalError(...)` mit:
- `fileName` = übergebener Pfad (oder `<kein Argument>` / `<mehrere Argumente>`)
- `timestamp` = jetzt - `timestamp` = jetzt
- genau ein `Finding`: - Genau ein `Finding`:
- `kind = SPEC` - `kind = SPEC` → Verdict wird `OPERATIONAL_ERROR`
- `severity = ERROR` - `severity = ERROR`
- `layer = ARTIFACT` - `layer = ARTIFACT`
- `ruleId = "OPERATIONAL-<fallkennung>"` (z.B. `OPERATIONAL-MISSING-ARG`, `OPERATIONAL-FILE-NOT-FOUND`, `OPERATIONAL-NOT-READABLE`, `OPERATIONAL-NOT-REGULAR`, `OPERATIONAL-TOO-MANY-ARGS`) - `ruleId` = z.B. `OPERATIONAL-MISSING-ARG`, `OPERATIONAL-FILE-NOT-FOUND`, `OPERATIONAL-NOT-READABLE`, `OPERATIONAL-NOT-REGULAR`, `OPERATIONAL-TOO-MANY-ARGS`
- `germanMessage` = kurzer, verständlicher deutscher Text - `germanMessage` = kurzer verständlicher deutscher Text
- **wichtig:** das Verdict dieses Reports ist **nicht** `INVALID`, sondern `OPERATIONAL_ERROR`. Die `Verdict`-Ableitungslogik muss in AP05 bereits den Fall „nur OPERATIONAL-Findings" erkennen können — falls das in AP05 noch nicht vorgesehen wurde, muss `ValidationReport` hier minimal erweitert werden.
- Alternative, sauberere Modellierung: ein zusätzlicher Konstruktor oder Factory-Methode `ValidationReport.operationalError(String fileName, String ruleId, String message)`, der den Verdict-Status explizit auf `OPERATIONAL_ERROR` setzt.
### Dateiausgabe bei Exit-Code 2 ### Wichtige Regeln
- wenn das Zielverzeichnis ermittelbar und schreibbar ist: Berichtdatei wird gemäß AP07-Logik erzeugt
- wenn nicht: **nur Konsolenausgabe**, keine Fehlermeldung („Bericht konnte nicht geschrieben werden"), weil das den Benutzer doppelt verunsichert
- eine kurze Hinweiszeile auf der Konsole ist okay: „Bericht konnte nicht in das Verzeichnis geschrieben werden, siehe Konsolenausgabe oben."
### Logging - **Kein Stack-Trace für den Nutzer** — technische Details gehören ins Log, nicht in den Bericht
- der Minimalbericht wird zusätzlich **geloggt** (`logger.error(...)`) — damit in der Log-Datei (sofern erzeugt) dokumentiert ist, was schiefging - Wenn Zielverzeichnis nicht schreibbar: **nur Konsolenausgabe**, kein Fehler-auf-Fehler
- bei Fall 1 und 2 (keine Dateipfadinformation) ist die Log-Datei nicht sinnvoll zu platzieren → Fallback auf `log4j2.xml`-Default aus AP04 - Eine kurze Hinweiszeile auf Konsole ist okay: „Bericht konnte nicht in das Verzeichnis geschrieben werden."
- Der Bedienfehler wird zusätzlich geloggt (`logger.error(...)`)
### Tests
- Unit-Test für alle fünf Fälle: Exit-Code `2` + korrekter `ruleId`-Wert
- Negativ-Test: kein Stack-Trace in der Ausgabe
- Test: Fall 3 (Datei nicht vorhanden) → Berichtdatei im übergeordneten Verzeichnis, wenn schreibbar
## Scope OUT ## Scope OUT
- Unterscheidung zwischen verschiedenen IO-Exceptions im Detail (`FileSystemException`, `AccessDeniedException`, …) — ein einheitlicher Fall „nicht lesbar" reicht - Unterscheidung feingranularer IO-Exceptions (`AccessDeniedException`, `FileSystemException` etc.) — ein einheitlicher Fall „nicht lesbar" reicht
- Internationalisierung - Internationalisierung
- Exit-Codes jenseits von `0/1/2` - Exit-Codes jenseits von `0/1/2`
- Behandlung von `OutOfMemoryError`, `StackOverflowError` etc.
## Schritte ## Schritte
1. Branch `m1/ap08-minimalbericht` 1. `ValidationReport.operationalError(...)` prüfen — falls noch nicht vollständig in AP05 implementiert, hier ergänzen
2. Factory-Methode `ValidationReport.operationalError(...)` in AP05-Modell ergänzen (falls noch nicht vorhanden) 2. `CliRunner` um alle fünf Bedienfehler-Fälle erweitern
3. `CliRunner` um die fünf Bedienfehler-Fälle erweitern; pro Fall wird der passende Minimalbericht erzeugt 3. `ReportFileWriter`: im `OPERATIONAL_ERROR`-Fall weichere IO-Fehlerbehandlung (kein `RuntimeException`, stattdessen Konsolenhinweis)
4. `ReportFileWriter`: im OPERATIONAL-Fall weichere IO-Fehlerbehandlung (keine `RuntimeException`, stattdessen Konsolenhinweis) 4. Unit-Tests für alle fünf Fälle
5. Unit-Tests für alle fünf Fälle 5. `mvn clean verify` grün bekommen
6. End-to-End-Test: `java -jar ... /pfad/zu/nichtvorhandener/datei.auf` erzeugt auf Konsole einen Minimalbericht und Exit-Code `2` 6. Abschlussbericht schreiben
7. `mvn clean verify` grün
8. Commit `M1-AP08: Minimalbericht bei Exit-Code 2`
9. Abschlussbericht schreiben
## Abnahmekriterien ## Abnahmekriterien
- alle fünf Bedienfehler-Fälle erzeugen einen Minimalbericht (per Unit-Test belegt) - [ ] Alle fünf Bedienfehler-Fälle erzeugen Exit-Code `2` (per Unit-Test belegt)
- Exit-Code in allen fünf Fällen ist `2` - [ ] Fall „kein Argument" → **nur** Konsolenausgabe, keine Dateiausgabe
- Im Fall „Eingabedatei existiert nicht" wird der Minimalbericht in das übergeordnete Verzeichnis geschrieben, sofern dieses schreibbar ist - [ ] Fall „Datei nicht vorhanden" → Berichtdatei im übergeordneten Verzeichnis, sofern schreibbar
- Im Fall „kein Argument" wird der Minimalbericht **nur** auf Konsole ausgegeben (keine Dateiausgabe) - [ ] `Verdict.OPERATIONAL_ERROR` ist in mindestens einem Test verifiziert
- `Verdict.OPERATIONAL_ERROR` ist in mindestens einem Test verifiziert - [ ] Kein Stack-Trace in STDERR (Negativ-Test vorhanden)
- `mvn clean verify` ist grün - [ ] `mvn clean verify` grün
- Abschlussbericht liegt vor - [ ] Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP08-bericht.md`
## Rest-Risiken und offene Punkte ## Rest-Risiken und offene Punkte
- Im Fall „Eingabedatei existiert nicht, Zielverzeichnis aber schon" ist der Bericht-Basisname der **angegebene** Dateiname (obwohl die Datei nicht existiert). Das ist okay — der Benutzer findet den Bericht dort, wo er die Datei erwartet hätte. - Im Fall „Datei nicht vorhanden, Zielverzeichnis aber existiert" ist der Basisname der **angegebene** Dateiname — der Nutzer findet den Bericht dort, wo er die Datei erwartet hätte.
- Die Unterscheidung zwischen `OPERATIONAL_ERROR` und `INVALID` im Verdict ist wichtig für spätere Reporting-Logik. Falls sich herausstellt, dass `OPERATIONAL_ERROR` als separater Verdict-Wert zu Komplikationen führt, kann alternativ ein Boolean-Flag `isOperational` auf dem Report verwendet werden. Entscheidung im Bericht dokumentieren. - Falls `Verdict.OPERATIONAL_ERROR` als separater Wert zu Komplikationen führt: Boolean-Flag `isOperational` auf dem Report ist zulässiger Ausweg — Entscheidung im Bericht dokumentieren.
## Bericht ## Bericht
`docs/arbeitspakete/m1/berichte/AP08-bericht.md` nach `templates/ap-bericht.md`. `docs/arbeitspakete/m1/berichte/AP08-bericht.md` nach `docs/arbeitspakete/m1/templates/ap-bericht.md`.
@@ -1,86 +1,121 @@
# AP09 Altlogik aus M1 entkoppeln (Parser/Validator einfrieren) ---
model: sonnet
---
# AP09 Altlogik einfrieren (Preview-Code deaktivieren)
> **Meilenstein:** M1
> **Vorgänger:** AP03, AP05, AP06 ✅ erforderlich
> **Nachfolger:** AP10, AP11
> **Grundlage:** AP00-ist-analyse.md §§ 7, 8
> **Entscheidungsprotokoll:** `docs/arbeitspakete/m1/E00-entscheidungsprotokoll.md` (E-01 Option b, E-03 leere Testklasse)
## Ziel ## Ziel
Die **bereits vorhandene Parser- und Validator-Logik** aus der früheren Implementierung (vor dieser M1-Planung) wird **nicht gelöscht**, aber sauber **entkoppelt** vom aktiven M1-Lauf. Sie wird als „Vorbau für M3 und folgende" explizit markiert und ist während M1 **nicht** Bestandteil der aktiven Verarbeitungskette. Die bestehende Preview-Parser- und Validator-Logik (`DefaultStructureValidator`, `DefaultFieldValidator`, `DefaultInputFileValidator`) wird **nicht gelöscht und nicht verschoben**, sondern im Bootstrap **nicht mehr verdrahtet**. Ein M1-Lauf erzeugt keine fachlichen ASVREC-/ASVFEH-Befunde mehr. Die Klassen bleiben physisch an ihrem Ort als M3-Vorbau.
Hintergrund: Der ursprüngliche Stand im Repository enthält bereits `DefaultInputFileParser`, `DefaultSegmentLineTokenizer`, `DefaultStructureValidator`, `DefaultFieldValidator`, `DefaultInputFileValidator` und `validation.model.ValidationResult`. Das ist wertvoll und darf nicht verloren gehen — gehört aber fachlich in M3 (Parser), M5 (Feldregeln) und M6 (Beziehungen), nicht in M1. ## Hintergrund und Entscheidung
Laut AP00-Ist-Analyse läuft `DefaultStructureValidator` (19 ASVREC/ASVFEH-Regeln) aktiv im produktiven Lauf mit — was dem M1-Ziel „noch keine ASV-Fachvalidierung" widerspricht.
**Entscheidung E-01: Option (b)** — Klassen bleiben in ihren Paketen, werden aber im Bootstrap nicht mehr verdrahtet. Für M3 kann die Verdrahtung direkt wieder aktiviert werden.
Kein Paketumzug, keine Umbenennung, kein `@Deprecated`.
## Voraussetzungen ## Voraussetzungen
- AP03 (Migration), AP05 (neues Befundmodell), AP06 (neuer Bootstrap/CLI) - AP03 (Paketstruktur)
- AP05 (neues Befundmodell)
- AP06 (neuer Bootstrap/CLI)
## Scope IN ## Scope IN
### Einfrieren statt Löschen ### 1. Bootstrap-Verdrahtung anpassen
- Die bestehenden Klassen bleiben **vollständig erhalten**, inklusive Tests
- Sie werden in ein klar erkennbares Unterpaket verschoben, z.B.:
```
de.gecheckt.asv.legacy.parser
de.gecheckt.asv.legacy.validation
de.gecheckt.asv.legacy.model
```
oder alternativ:
```
de.gecheckt.asv.application.preview
```
Die Entscheidung zwischen `legacy` und `preview` wird im Bericht dokumentiert und begründet. Empfehlung: **`preview`**, weil „legacy" suggeriert, dass etwas alt und zu entsorgen ist — tatsächlich wird der Code in M3/M5/M6 weiterverwendet.
- Jedes Paket bekommt ein `package-info.java` mit deutlichem Hinweis:
```
Diese Klassen stammen aus einer früheren Implementierung und sind
für die Meilensteine M3 bis M6 vorgesehen. Sie sind in M1 nicht Teil
der aktiven Validierungskette. Änderungen an diesen Klassen während
M1 sind zu vermeiden.
```
### Entkopplung vom Lauf In `bootstrap.Main`: `DefaultInputFileValidator` erhält statt `DefaultStructureValidator` und `DefaultFieldValidator` jeweils eine **Null-Implementation** — eine leere Implementierung der jeweiligen Interfaces, die keine Befunde produziert.
- `CliRunner` und `Bootstrap` dürfen die Preview-Klassen **nicht** aufrufen
- der aktive M1-Lauf verwendet ausschließlich den Dummy-Pfad aus AP06 (Datei einlesen, leeren `ValidationReport` erzeugen)
- die alten Tests der Preview-Klassen laufen **weiterhin grün mit**, damit der Code nicht verrottet
- `validation.model.ValidationResult` (alt) bleibt im Preview-Paket und wird **nicht** mit dem neuen `ValidationReport` aus AP05 verwechselt
### Saubere Kennzeichnung Die Null-Implementierungen können als benannte Klassen in `bootstrap` oder `application` angelegt werden:
- In `README.md` des Repos (falls vorhanden, sonst anlegen) ein kurzer Abschnitt „Preview-Code" mit Verweis auf `docs/arbeitspakete/m1/AP09-altlogik-einfrieren.md`
- Kein `@Deprecated`! Deprecated würde bedeuten „wird entfernt" — das Gegenteil ist der Fall. ```java
/** M1-Platzhalter. Ab M3 durch DefaultStructureValidator ersetzen. */
public final class NoOpStructureValidator implements StructureValidator {
@Override
public List<Finding> validate(ValidationContext ctx) {
return List.of(); // bewusst leer in M1
}
}
```
Analog für `NoOpFieldValidator`.
### 2. Einfriermarker als JavaDoc-Kommentar
`DefaultStructureValidator` und `DefaultFieldValidator` erhalten folgenden JavaDoc-Kommentar — keine andere Änderung:
```java
/**
* M3-Vorbau. In M1 bewusst nicht im produktiven Lauf verdrahtet.
* Wird ab M3 wieder aktiviert und gegen die finalen Regelklassifikationen
* (V1-V/T/N/K) aus fachliche-anforderungen.md bewertet.
*
* @see docs/arbeitspakete/m1/E00-entscheidungsprotokoll.md E-01
*/
```
### 3. Abnahmetest
Ein Integrationstest prüft, dass ein Lauf mit einer beliebigen Testdatei **keine** fachlichen Findings mit Bezug auf ASVREC-/ASVFEH-Segmentregeln erzeugt.
### 4. Aufräumen
- `DefaultStructureValidatorTestAdditional` (leere Testklasse ohne `@Test`-Methoden) löschen (gemäß E-03)
- Sicherstellen dass `logs/` in `.gitignore` steht (falls AP06 das noch nicht erledigt hat)
- Aktive Tests von `DefaultStructureValidator` und `DefaultFieldValidator` bleiben **erhalten und grün** — sie testen den M3-Vorbau
### 5. Grep-Nachweis im Bericht
```bash
grep -rn "DefaultStructureValidator\|DefaultFieldValidator" \
src/main/java/de/gecheckt/asv/adapter \
src/main/java/de/gecheckt/asv/bootstrap
```
Muss **leer** sein — der aktive Code darf diese Klassen nicht mehr referenzieren.
## Scope OUT ## Scope OUT
- Weiterentwicklung der Preview-Klassen - Paketumzug der Preview-Klassen (explizit **nicht** — Entscheidung E-01 Option b)
- Änderung der Preview-Tests (außer notwendige Import-Anpassungen durch den Package-Umzug) - Inhaltliche Änderung an `DefaultStructureValidator` oder `DefaultFieldValidator`
- Integration der Preview-Klassen in die neue `domain.finding`-Struktur (das ist explizit M3+) - Fachliche Neubewertung der 19 Preview-Regeln (das ist M3)
- Löschung von Preview-Klassen, auch wenn sie wie Duplikate wirken - Löschen von Preview-Klassen
## Schritte ## Schritte
1. Branch `m1/ap09-preview-einfrieren` 1. `NoOpStructureValidator` und `NoOpFieldValidator` anlegen
2. Zielpaket wählen (`preview` empfohlen) und im Bericht begründen 2. `bootstrap.Main` umverdrahten: Preview-Validatoren durch NoOp-Implementierungen ersetzen
3. Alle Parser-Klassen verschieben 3. JavaDoc-Einfriermarker in `DefaultStructureValidator` und `DefaultFieldValidator` ergänzen
4. Alle Validator-Klassen verschieben 4. `DefaultStructureValidatorTestAdditional` löschen
5. `validation.model.ValidationResult` und `validation.model.*` mit verschieben 5. Grep-Nachweis ausführen und im Bericht dokumentieren
6. Tests entsprechend verschieben; Imports anpassen 6. Integrationstest: Lauf erzeugt keine ASVREC-/ASVFEH-Segmentbefunde
7. `CliRunner`/`Bootstrap` auf Preview-Imports prüfen — **darf keine haben**, sonst entkoppeln 7. `mvn clean verify` grün — alle bisherigen Tests der Preview-Klassen müssen weiterhin grün sein
8. `package-info.java` mit Warnhinweis in jedem Preview-Unterpaket anlegen 8. Abschlussbericht schreiben
9. README-Abschnitt „Preview-Code" ergänzen
10. `mvn clean verify` grün bekommen (alle Tests der Preview-Klassen laufen weiter mit)
11. Commit `M1-AP09: Alt-Parser und Alt-Validator nach preview-Paket, vom M1-Lauf entkoppelt`
12. Abschlussbericht schreiben
## Abnahmekriterien ## Abnahmekriterien
- alle ursprünglich vorhandenen Parser- und Validator-Klassen liegen im Preview-Paket - [ ] `NoOpStructureValidator` und `NoOpFieldValidator` existieren
- alle zugehörigen Tests laufen weiterhin grün - [ ] `bootstrap.Main` verdrahtet keine Preview-Validatoren mehr
- `grep -rn "de.gecheckt.asv.preview" src/main/java/de/gecheckt/asv/adapter src/main/java/de/gecheckt/asv/bootstrap src/main/java/de/gecheckt/asv/application` ist **leer** (keine Referenzen aus dem aktiven Code) - [ ] Grep auf `DefaultStructureValidator`/`DefaultFieldValidator` in `adapter` und `bootstrap` ist leer (Nachweis im Bericht)
- `package-info.java` mit Warnhinweis in jedem Preview-Unterpaket - [ ] Einfriermarker-JavaDoc in beiden Preview-Klassen vorhanden
- README enthält Abschnitt „Preview-Code" - [ ] `DefaultStructureValidatorTestAdditional` ist gelöscht
- keine Klasse wurde gelöscht (`git log --diff-filter=D` für diesen Commit zeigt nur Verschiebungen) - [ ] Bestehende Tests der Preview-Klassen laufen weiterhin grün
- `mvn clean verify` ist grün - [ ] Integrationstest: Lauf mit Testdatei erzeugt keine ASVREC-/ASVFEH-Segmentbefunde
- Abschlussbericht liegt vor - [ ] `mvn clean verify` grün
- [ ] Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP09-bericht.md`
## Rest-Risiken und offene Punkte ## Rest-Risiken und offene Punkte
- Bei Wiederaufnahme in M3 wird zu klären sein, wie der Preview-Code an das neue Befundmodell angebunden wird. Das ist explizit M3-Aufgabe, nicht M1. - Bei Wiederaufnahme in M3: jede der 19 Preview-Regeln ist neu gegen V1-V/T/N/K-Klassifikation zu bewerten. Explizit M3-Aufgabe.
- Falls die Preview-Tests beim Package-Umzug brechen (wegen relativer Ressourcenpfade o.ä.), müssen sie einmalig angepasst werden. Das ist kein Scope-Verstoß, sondern Teil des Umzugs. - Falls Preview-Tests beim Einfrieren brechen (z.B. wegen Interface-Änderungen durch AP05): einmalige Anpassung der Testimports ist kein Scope-Verstoß, sondern Teil der Konsolidierung.
## Bericht ## Bericht
`docs/arbeitspakete/m1/berichte/AP09-bericht.md` nach `templates/ap-bericht.md`. `docs/arbeitspakete/m1/berichte/AP09-bericht.md` nach `docs/arbeitspakete/m1/templates/ap-bericht.md`.
+85 -41
View File
@@ -1,26 +1,42 @@
---
model: sonnet
---
# AP10 Architekturtest # AP10 Architekturtest
> **Meilenstein:** M1
> **Vorgänger:** AP04, AP09 ✅ erforderlich (alle AP05AP09 sollten abgeschlossen sein)
> **Nachfolger:** AP11
> **Grundlage:** `docs/specs/technik-und-architektur.md` v5, §§ „Architekturprinzipien", „Test- und Qualitätsanforderungen"
> **Entscheidungsprotokoll:** `docs/arbeitspakete/m1/E00-entscheidungsprotokoll.md` (E-02 Test-Log, E-03 leere Testklasse)
## Ziel ## Ziel
Ein **automatisierter Architekturtest** stellt sicher, dass die in M1 etablierten Strukturregeln auch in Zukunft eingehalten werden. Insbesondere darf: Automatisierte Architekturtests sichern die in M1 etablierten Strukturregeln dauerhaft ab. Zusätzlich wird das Build-Rauschen durch ERROR-Log-Zeilen in Negativ-Tests beseitigt.
1. die **Log4j2-Bindung** nur in `adapter.out.logging` und `bootstrap` sichtbar sein
2. das `domain`-Paket **nichts** aus Adaptern oder Infrastruktur importieren
3. das `application`-Paket **nichts** aus konkreten Adaptern importieren, sondern nur aus `domain` und eigenen Ports
4. **Preview-Code** nicht aus dem aktiven Code (Bootstrap, CLI-Adapter, Application) referenziert werden
## Voraussetzungen ## Voraussetzungen
- AP04 (Logging-Adapter), AP09 (Preview eingefroren) - AP04 (Logging-Adapter etabliert)
- AP09 (Preview-Code eingefroren)
- Idealerweise alle AP05AP09 abgeschlossen
## Scope IN ## Scope IN
### Technische Umsetzung ### 1. ArchUnit als Test-Dependency
- **ArchUnit** (`com.tngtech.archunit:archunit-junit5`) als Test-Dependency aufnehmen
- neue Test-Klasse `ArchitectureTest` im Paket `de.gecheckt.asv` im Testbereich
- vier Tests:
### Test 1: Log4j2-Sichtbarkeit ```xml
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version><!-- aktuell stabile Version --></version>
<scope>test</scope>
</dependency>
```
### 2. Architekturtest-Klasse
Neue Testklasse `de.gecheckt.asv.ArchitectureTest` im Testbereich. Mindestens vier Regeln:
**Regel A — Log4j2-Sichtbarkeit:**
```java ```java
@ArchTest @ArchTest
static final ArchRule log4j2_nur_in_logging_adapter_und_bootstrap = static final ArchRule log4j2_nur_in_logging_adapter_und_bootstrap =
@@ -33,7 +49,7 @@ static final ArchRule log4j2_nur_in_logging_adapter_und_bootstrap =
.because("Log4j2 darf nur im Logging-Adapter und im Bootstrap sichtbar sein."); .because("Log4j2 darf nur im Logging-Adapter und im Bootstrap sichtbar sein.");
``` ```
### Test 2: Domain ist frei **Regel B — Domain-Reinheit:**
```java ```java
@ArchTest @ArchTest
static final ArchRule domain_hat_keine_adapter_abhaengigkeit = static final ArchRule domain_hat_keine_adapter_abhaengigkeit =
@@ -42,11 +58,10 @@ static final ArchRule domain_hat_keine_adapter_abhaengigkeit =
.should().dependOnClassesThat() .should().dependOnClassesThat()
.resideInAnyPackage( .resideInAnyPackage(
"de.gecheckt.asv.adapter..", "de.gecheckt.asv.adapter..",
"de.gecheckt.asv.bootstrap..", "de.gecheckt.asv.bootstrap..");
"de.gecheckt.asv.preview..");
``` ```
### Test 3: Application ist frei von konkreten Adaptern **Regel C — Application-Reinheit:**
```java ```java
@ArchTest @ArchTest
static final ArchRule application_kennt_keine_adapter_implementierungen = static final ArchRule application_kennt_keine_adapter_implementierungen =
@@ -58,53 +73,82 @@ static final ArchRule application_kennt_keine_adapter_implementierungen =
"de.gecheckt.asv.bootstrap.."); "de.gecheckt.asv.bootstrap..");
``` ```
### Test 4: Preview wird nicht referenziert **Regel D — Preview-Isolation:**
```java ```java
@ArchTest @ArchTest
static final ArchRule preview_wird_nicht_aus_aktivem_code_referenziert = static final ArchRule preview_wird_nicht_aus_aktivem_code_referenziert =
noClasses() noClasses()
.that().resideInAnyPackage( .that().resideInAnyPackage(
"de.gecheckt.asv.adapter..", "de.gecheckt.asv.adapter..",
"de.gecheckt.asv.application..", "de.gecheckt.asv.bootstrap..")
"de.gecheckt.asv.bootstrap..",
"de.gecheckt.asv.domain..")
.should().dependOnClassesThat() .should().dependOnClassesThat()
.resideInAPackage("de.gecheckt.asv.preview..") .haveSimpleNameContaining("DefaultStructureValidator")
.because("Preview-Code ist aus M1-Sicht eingefroren und wird erst ab M3 aktiv verwendet."); .orShould().dependOnClassesThat()
.haveSimpleNameContaining("DefaultFieldValidator")
.because("Preview-Validatoren sind in M1 eingefroren und werden erst ab M3 aktiv verwendet.");
``` ```
### Zusätzlich: Paketstruktur-Check **Wichtig:** Wenn beim ersten Lauf Regeln rot sind, **müssen die Verstöße behoben werden** — die Regeln werden nicht abgeschwächt.
- Prüfen, dass die Soll-Pakete aus `technik-und-architektur.md` tatsächlich existieren (als ArchUnit-Regel oder einfacher Dateisystem-Test)
### 3. Test-Log-Konfiguration (E-02)
`src/test/resources/log4j2-test.xml` anlegen:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_ERR">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="WARN">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
```
Log4j2 bevorzugt `log4j2-test.xml` im Test-Classpath gegenüber `log4j2.xml`. Das unterdrückt die erwartete ERROR-Zeile aus dem CLI-Negativ-Test.
### 4. Aufräumen
- Prüfen ob `DefaultStructureValidatorTestAdditional` bereits in AP09 gelöscht wurde — falls nicht, hier löschen
- Sicherstellen dass keine weiteren leeren Testklassen im Projekt existieren
## Scope OUT ## Scope OUT
- komplexere Regeln wie „keine zyklischen Abhängigkeiten zwischen Paketen" — wäre schön, ist aber für M1 zu weitgehend - Komplexe Regeln wie zyklische Abhängigkeiten für M1 zu weitgehend
- Regeln zu Klassenbenennung - Regeln zu Klassenbenennung oder `public`-Sichtbarkeit
- Regeln zu `public`-Sichtbarkeit - Coverage- und Mutation-Schwellwerte (kommen erst in M9)
- Tests für Preview-internen Aufbau - Neue Produktionsklassen
## Schritte ## Schritte
1. Branch `m1/ap10-architekturtest` 1. ArchUnit in `pom.xml` als Test-Dependency aufnehmen
2. ArchUnit in `pom.xml` als Test-Dependency aufnehmen 2. `ArchitectureTest`-Klasse mit den vier Regeln implementieren
3. `ArchitectureTest`-Klasse im Testbereich anlegen 3. `log4j2-test.xml` unter `src/test/resources/` anlegen
4. Die vier Regeln implementieren 4. `mvn clean verify` ausführen — alle vier ArchUnit-Regeln müssen grün sein
5. `mvn clean verify` laufen lassen — die Tests müssen **grün** sein. Falls rot: **das heißt, eine frühere M1-Phase hat die Regel verletzt**. Die Verletzung muss gefunden und behoben werden (kein Entschärfen der Regel!). 5. Falls Regeln rot: Verstöße identifizieren, beheben, erneut testen
6. Commit `M1-AP10: Architekturtest für Log4j2-Sichtbarkeit, Paketabhängigkeiten, Preview-Isolation` 6. Im Bericht dokumentieren ob beim ersten Lauf Regeln rot waren und wie behoben
7. Abschlussbericht schreiben 7. Abschlussbericht schreiben
## Abnahmekriterien ## Abnahmekriterien
- ArchUnit ist als Test-Dependency eingebunden - [ ] ArchUnit als Test-Dependency in `pom.xml`
- vier Architektur-Regeln sind implementiert und grün - [ ] Vier Architekturregeln AD implementiert und grün
- `mvn clean verify` ist grün - [ ] `log4j2-test.xml` unter `src/test/resources/` vorhanden
- Abschlussbericht liegt vor und dokumentiert, ob beim ersten Lauf Regeln rot waren und wenn ja, wie sie behoben wurden - [ ] Keine leeren Testklassen im Projekt
- [ ] `mvn clean verify` grün, kein unerwartetes Log-Rauschen
- [ ] Bericht dokumentiert ob erste Ausführung Regeln rot hatte und wie behoben
- [ ] Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP10-bericht.md`
## Rest-Risiken und offene Punkte ## Rest-Risiken und offene Punkte
- ArchUnit hat beim ersten Einsatz manchmal Überraschungen mit transitiven Abhängigkeiten (Regel greift auch auf Framework-Klassen, die nicht gemeint waren). In dem Fall: Regel präzisieren, **nicht** ausschalten. - ArchUnit hat beim ersten Einsatz manchmal Überraschungen mit transitiven Abhängigkeiten (Regel greift auch auf Framework-Klassen). In dem Fall: Regel präzisieren, **nicht** ausschalten.
- Die Regel „Log4j2-Sichtbarkeit" schließt auch den Bootstrap mit ein. Wenn der Bootstrap später (M8) Krypto-Typen referenziert, müssen analoge Regeln ergänzt werden — aber das ist M8-Thema. - Regel D (Preview-Isolation) ist auf Klassennamen-Basis formuliert, weil kein separates Paket existiert (Option b). Falls das zu fragil ist, kann alternativ auf Package-Ebene mit einem `preview`-Paket geprüft werden — aber nur wenn AP09 entsprechend umgebaut wurde.
## Bericht ## Bericht
`docs/arbeitspakete/m1/berichte/AP10-bericht.md` nach `templates/ap-bericht.md`. `docs/arbeitspakete/m1/berichte/AP10-bericht.md` nach `docs/arbeitspakete/m1/templates/ap-bericht.md`.
+74 -59
View File
@@ -1,92 +1,107 @@
---
model: sonnet
---
# AP11 M1-Abnahme # AP11 M1-Abnahme
> **Meilenstein:** M1
> **Vorgänger:** AP01AP10 alle ✅ erforderlich
> **Nachfolger:** M2
> **Grundlage:** `docs/specs/meilensteine.md` v3, M1-Abnahmekriterien
## Ziel ## Ziel
Der letzte Schritt in M1: Alles wird gegen die Meilenstein-Abnahmekriterien aus `docs/specs/meilensteine.md` v3 **geprüft**, ein **End-to-End-Lauf** mit einer Minimal-Eingabedatei wird durchgeführt, und alle AP-Berichte werden in einem **konsolidierten M1-Abschlussbericht** zusammengeführt. M1 wird formal abgenommen. Alle Abnahmekriterien aus `meilensteine.md` sind erfüllt und nachweisbar. Das Projekt ist bereit für M2.
## Voraussetzungen ## Voraussetzungen
- AP01 bis AP10 abgeschlossen und grün - AP01AP10 abgeschlossen und grün
- alle AP-Berichte liegen in `docs/arbeitspakete/m1/berichte/` vor - Alle AP-Berichte liegen in `docs/arbeitspakete/m1/berichte/` vor
## Scope IN ## Scope IN
### End-to-End-Lauf ### 1. Test-Artefakt anlegen
1. **Minimaldatei erstellen**: eine einfache Dummy-Textdatei im ISO-8859-15-Encoding, z.B. `test-artefakte/m1/minimal.txt` mit ein paar Zeilen Inhalt. Keine echten ASV-Daten, kein gültiges EDIFACT — dies ist nur ein Lauftest, kein Fachtest.
2. **JAR bauen**: `mvn clean package`
3. **Lauf 1**: `java -jar target/asv-format-validator-0.0.1-SNAPSHOT.jar test-artefakte/m1/minimal.txt`
- **Erwartung:** Exit-Code `0` (Dummy-Pfad, leerer Report), Berichtdatei `minimal.txt.txt` und Log-Datei `minimal.txt.log` entstehen, Konsolenausgabe vorhanden
4. **Lauf 2**: identischer Aufruf
- **Erwartung:** `minimal.txt_v1.txt` und `minimal.txt_v1.log` entstehen
5. **Lauf 3**: `java -jar ... nicht-vorhanden.txt`
- **Erwartung:** Exit-Code `2`, Minimalbericht auf Konsole, gegebenenfalls Berichtdatei im übergeordneten Verzeichnis wenn schreibbar
6. **Lauf 4**: `java -jar ...` (ohne Argument)
- **Erwartung:** Exit-Code `2`, Minimalbericht **nur** auf Konsole
7. **Lauf 5**: `java -jar ... datei1.txt datei2.txt`
- **Erwartung:** Exit-Code `2`, Minimalbericht auf Konsole
Alle fünf Läufe werden im M1-Abschlussbericht dokumentiert (Befehl, Exit-Code, relevante Ausgabe). `test-artefakte/m1/minimal.txt` anlegen — eine einfache ISO-8859-15-kompatible Textdatei mit 35 Zeilen Dummy-Inhalt. Keine echten ASV-Daten, kein gültiges EDIFACT — reiner Lauftest.
### Meilenstein-Abnahmeprüfung ### 2. End-to-End-Läufe durchführen
Jeder Abnahmepunkt aus `docs/specs/meilensteine.md` v3 Abschnitt „Abnahme von M1" wird mit einem konkreten Nachweis verknüpft:
| M1-Abnahmekriterium | Nachweis | Status | `mvn clean package` ausführen, dann alle fünf Läufe:
| Lauf | Befehl | Erwarteter Exit-Code | Erwartetes Verhalten |
|---|---|---|---|
| 1 | `java -jar target/asv-format-validator-*.jar test-artefakte/m1/minimal.txt` | `0` | `.txt` und `.log` entstehen im Verzeichnis |
| 2 | identischer Aufruf wie Lauf 1 | `0` | `_v1.txt` und `_v1.log` entstehen |
| 3 | `java -jar ... nicht-vorhanden.txt` | `2` | Minimalbericht auf Konsole |
| 4 | `java -jar ...` (ohne Argument) | `2` | Minimalbericht nur auf Konsole |
| 5 | `java -jar ... datei1.txt datei2.txt` | `2` | Minimalbericht nur auf Konsole |
Alle fünf Läufe werden im M1-Abschlussbericht dokumentiert (Befehl, tatsächlicher Exit-Code, relevante Ausgabe).
### 3. Meilenstein-Abnahmeprüfung
Jeden Punkt aus `docs/specs/meilensteine.md` v3 §„Abnahme von M1" mit konkretem Nachweis verknüpfen:
| Kriterium | Nachweis | Status |
|---|---|---| |---|---|---|
| Anwendung ist als JAR unter Windows mit Java 21 startbar | Lauf 1, JAR-Pfad | ✅ | | Anwendung als JAR unter Windows mit Java 21 startbar | Lauf 1 | |
| falsches oder fehlendes Argument → Exit-Code `2` mit Minimalbericht | Lauf 3, 4, 5 | | | Fehlendes/falsches Argument → Exit-Code `2` mit Minimalbericht | Lauf 3, 4, 5 | |
| Bericht- und Log-Datei werden im Eingabeverzeichnis mit korrekter Suffix-Logik erzeugt | Lauf 1 + Lauf 2 | | | Bericht- und Log-Datei im Eingabeverzeichnis mit korrekter Suffix-Logik | Lauf 1 + 2 | |
| Log4j2-Bindung ist außerhalb von Bootstrap und Logging-Adapter nicht sichtbar | Architekturtest AP10, Test 1 | ✅ | | Log4j2-Bindung außerhalb Bootstrap/Logging-Adapter nicht sichtbar | Architekturtest AP10 Regel A | |
| Befundmodell unterscheidet Spec-Urteil und diagnostische Weiteranalyse | Unit-Test AP05 | | | Befundmodell trennt Spec-Urteil und diagnostische Weiteranalyse | Unit-Test AP05 | |
| Build und Tests sind grün | `mvn clean verify` | | | Build und Tests grün | `mvn clean verify` | |
### M1-Abschlussbericht ### 4. Konsolidierter M1-Abschlussbericht
- Datei: `docs/arbeitspakete/m1/berichte/M1-abschlussbericht.md`
- Inhalt:
- **Zusammenfassung**: Was ist M1 geworden, in zwei Absätzen
- **AP-Übersicht**: Tabelle mit allen 11 APs, Status, Commit-Hashes, Abschlussdatum
- **Meilenstein-Abnahmetabelle** (siehe oben)
- **End-to-End-Protokoll**: die fünf Läufe mit Befehl, Exit-Code, Zusammenfassung der Ausgabe
- **Quality-Metriken** (Coverage, Testanzahl — **keine harten Gates in M1**, nur Informationswert)
- **Rest-Risiken und übertragene Punkte** aus allen AP-Berichten konsolidiert
- **Empfehlungen für M2**: Was sollte M2 beachten? Was ist aus M1-Sicht offen geblieben?
- **Commit-Graph-Snapshot**: `git log --oneline --graph main` für den M1-Zeitraum
- Freigabe-Vermerk am Ende: „M1 ist abnahmebereit" oder „M1 ist mit folgenden Einschränkungen abnahmebereit: ..."
### Tagging Datei: `docs/arbeitspakete/m1/berichte/M1-abschlussbericht.md`
- Git-Tag `m1-done` auf dem letzten AP11-Commit setzen
- Tag-Message: „Meilenstein 1 abgeschlossen, siehe docs/arbeitspakete/m1/berichte/M1-abschlussbericht.md" Pflichtabschnitte:
- **Zusammenfassung** — Was ist M1 geworden (max. zwei Absätze)
- **AP-Übersicht** — Tabelle mit allen 11 APs, Status, letzter Commit
- **Meilenstein-Abnahmetabelle** (siehe oben, vollständig ausgefüllt)
- **End-to-End-Protokoll** — alle fünf Läufe mit Befehl, Exit-Code, Ausgabe-Zusammenfassung
- **Quality-Metriken** — Testanzahl, Coverage-Wert (informativ, keine Gate-Prüfung in M1)
- **Rest-Risiken** — konsolidiert aus allen AP-Berichten
- **Empfehlungen für M2** — was M2 beachten sollte
- **Freigabe-Vermerk** — „M1 ist abnahmebereit" oder „M1 ist mit folgenden Einschränkungen abnahmebereit: ..."
### 5. Git-Tag setzen
```bash
git tag -a m1-done -m "Meilenstein 1 abgeschlossen, siehe docs/arbeitspakete/m1/berichte/M1-abschlussbericht.md"
```
## Scope OUT ## Scope OUT
- Vorgriffe auf M2-Themen (Dateinamensschemata, globale Rahmenregeln, ISO-8859-15 über Dateinamen hinaus — außer dem Einlese-Encoding aus AP06, das bleibt) - Vorgriffe auf M2-Themen
- Release-Builds, Signierung, Publizierung - Release-Builds, Signierung, Publizierung
- Externe Reviews (die kommen vom Rezensenten der Arbeitspakete, nicht aus diesem AP) - Inhaltliche Berichtsvertiefung über M1-Minimum hinaus
## Schritte ## Schritte
1. Branch `m1/ap11-abnahme` 1. `test-artefakte/m1/minimal.txt` anlegen
2. Test-Artefakt `test-artefakte/m1/minimal.txt` anlegen (ISO-8859-15, 35 Zeilen Dummy-Inhalt) 2. `mvn clean package`
3. `mvn clean package` ausführen 3. Alle fünf Läufe durchführen und protokollieren
4. Die fünf Läufe durchführen und protokollieren 4. `mvn clean verify` ein letztes Mal
5. Konsolidierten M1-Abschlussbericht schreiben 5. Konsolidierten M1-Abschlussbericht schreiben
6. `mvn clean verify` ein letztes Mal laufen lassen 6. Git-Tag `m1-done` setzen
7. Commit `M1-AP11: M1-Abnahme, End-to-End-Protokoll, konsolidierter Abschlussbericht`
8. Git-Tag `m1-done` setzen: `git tag -a m1-done -m "Meilenstein 1 abgeschlossen"`
## Abnahmekriterien ## Abnahmekriterien
- `test-artefakte/m1/minimal.txt` existiert - [ ] `test-artefakte/m1/minimal.txt` existiert
- alle fünf Läufe sind protokolliert - [ ] Alle fünf Läufe sind protokolliert
- M1-Abschlussbericht existiert und enthält alle oben genannten Abschnitte - [ ] `M1-abschlussbericht.md` existiert mit allen Pflichtabschnitten
- Meilenstein-Abnahmetabelle ist vollständig und jede Zeile hat einen konkreten Nachweis - [ ] Meilenstein-Abnahmetabelle vollständig, jede Zeile mit konkretem Nachweis
- `mvn clean verify` ist grün - [ ] Kein Exit-Code `3` mehr erreichbar
- Git-Tag `m1-done` ist gesetzt - [ ] `mvn clean verify` grün
- der Freigabe-Vermerk am Ende des Abschlussberichts ist explizit - [ ] Git-Tag `m1-done` gesetzt
- [ ] Freigabe-Vermerk ist explizit
- [ ] Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP11-bericht.md`
## Rest-Risiken und offene Punkte ## Rest-Risiken und offene Punkte
- Dieser AP ist ein reines Zusammenfassungs-AP. Wenn hier Abnahmekriterien nicht erfüllt sind, zeigt das, dass ein früheres AP unvollständig war. In dem Fall: **zurück zum betroffenen AP**, nachbessern, dann AP11 wiederholen. Keine Abkürzungen. - Wenn hier Abnahmekriterien nicht erfüllt sind, zeigt das, dass ein früheres AP unvollständig war. In dem Fall: **zurück zum betroffenen AP**, nachbessern, dann AP11 wiederholen. Keine Abkürzungen.
## Bericht ## Bericht
`docs/arbeitspakete/m1/berichte/AP11-bericht.md` nach `templates/ap-bericht.md` **zusätzlich** zum konsolidierten `M1-abschlussbericht.md`. `docs/arbeitspakete/m1/berichte/AP11-bericht.md` nach `docs/arbeitspakete/m1/templates/ap-bericht.md`.
**Zusätzlich** konsolidierter `docs/arbeitspakete/m1/berichte/M1-abschlussbericht.md`.
@@ -0,0 +1,57 @@
# M1-Orchestrierung: AP05 bis AP11
Dieser Prompt wird mit `claude --model opusplan` gestartet.
---
Lies vor dem Start vollständig und in dieser Reihenfolge:
1. `CLAUDE.md`
2. `docs/specs/technik-und-architektur.md` (Überblick — nicht jede Zeile)
3. `docs/specs/meilensteine.md` (nur M1-Abschnitt)
4. `docs/arbeitspakete/m1/E00-entscheidungsprotokoll.md`
5. `docs/arbeitspakete/m1/README.md`
6. Alle AP05- bis AP11-Dateien in `docs/arbeitspakete/m1/`
---
Du bist der Lead-Orchestrator für Meilenstein M1. Deine Aufgabe ist es,
die Arbeitspakete AP05 bis AP11 sequenziell zu orchestrieren.
Jede AP-Datei enthält `model: sonnet` im Frontmatter — nutze dieses Modell
für die Subagenten die die Implementierung übernehmen.
## Vorgehen
Für jedes Arbeitspaket in der unten angegebenen Reihenfolge:
1. Lies das AP-Dokument vollständig
2. Starte einen Subagenten (Task-Tool) mit dem Auftrag, genau dieses AP
umzusetzen — Subagent-Modell: sonnet
3. Der Subagent folgt den Schritten im AP-Dokument und endet mit
`mvn clean verify` und dem Abschlussbericht
4. Du prüfst nach Abschluss des Subagenten:
- Ist `mvn clean verify` grün?
- Liegt der Abschlussbericht unter `docs/arbeitspakete/m1/berichte/`?
- Sind alle Abnahmekriterien des APs erfüllt?
5. Nur wenn alle drei Punkte erfüllt: weiter zum nächsten AP
6. Wenn ein AP fehlschlägt: analysiere die Ursache, starte einen
Korrektur-Subagenten, prüfe erneut
## Reihenfolge
AP05 → AP06 → AP09 → AP07 → AP08 → AP10 → AP11
## Harte Regeln (aus CLAUDE.md — gelten für dich und alle Subagenten)
- Kein `git commit`, kein `git add`, kein `git push`
- Kein Schreiben außerhalb des jeweiligen AP-Scopes
- Abschlussbericht pro AP ist Pflicht
- `mvn clean verify` muss nach jedem AP grün sein
## Abschluss
Nach AP11: melde „M1-Orchestrierung abgeschlossen" mit einer Zusammenfassung
welche APs in wie vielen Versuchen abgeschlossen wurden.
Fange jetzt an.
@@ -0,0 +1,328 @@
# AP00 — Ist-Analyse Meilenstein M1
> **Bezug:** M1 aus `docs/specs/meilensteine.md` v3, technischer Soll-Rahmen aus `docs/specs/technik-und-architektur.md` v5
> **Bearbeiter:** Claude Code (claude-opus-4-7), Ist-Analyse auf Auftrag, keine Code-Änderungen
> **Datum:** 2026-04-20
> **Grundlage:** HEAD auf `main` nach den Commits AP01AP04 (letzter: `cd6e522 docs: Review-Korrekturen aus Peer-Review anwenden`)
> **Status:** Analyse abgeschlossen, reine Dokumentation
---
## 1. Zusammenfassung
Der aktuelle M1-Stand ist zu rund einem Drittel umgesetzt. Die **Build-Infrastruktur** (AP02), die **hexagonale Paketstruktur** (AP03) und die **SLF4J-Logging-Fassade** (AP04) sind fertig, `mvn clean verify` ist grün (147 Tests, 0 Fehler). Offen bleiben die fachlich tragenden Teile von M1: das **Befundmodell mit Spec-/Diagnose-Trennung** (AP05), das korrekte **Exit-Code-Modell `0/1/2`** (aktuell 4 Codes `0/1/2/3` mit vertauschter Semantik), der **CLI-/Bootstrap-Zuschnitt** (AP06), die **Ausgabeartefakte** (`_v1/_v2`-Suffixlogik, `.txt`/`.log`, AP07), der **Minimalbericht** bei Bedienfehlern (AP08), das **Einfrieren der Altlogik** (AP09) sowie der **Architekturtest** (AP10). Das Eingabeencoding ist aktuell hartkodiert `UTF-8` statt `ISO-8859-15`, das Hauptartefakt-JAR referenziert eine noch nicht existierende `bootstrap.Main`-Klasse, und der Preview-`DefaultStructureValidator` (19 ASVREC/ASVFEH-Regeln) läuft im produktiven Lauf mit und erzeugt fachliche Befunde, die in M1 noch nicht vorgesehen sind.
---
## 2. Projektstruktur
### 2.1 Verzeichnisbaum (Wurzel)
```
asv-format-validator/
├── .editorconfig, .gitattributes, .gitignore
├── CLAUDE.md, README.md, Spec.docx
├── pom.xml
├── Apply-ReviewPatches.ps1
├── docs/
│ ├── arbeitspakete/m1/{APxx.md, berichte/, templates/}
│ └── specs/{fachliche-anforderungen.md, technik-und-architektur.md, meilensteine.md}
├── logs/ # wurde vom Log4j2-File-Appender im Vorlauf angelegt
├── src/
│ ├── main/java/...
│ ├── main/resources/log4j2.xml
│ ├── test/java/...
│ └── test/resources/*.asv # Parser-Testartefakte
└── target/ # Maven Build-Output
```
### 2.2 Maven-Module
Ein einziges Modul: `de.gecheckt:asv-format-validator:0.0.1-SNAPSHOT`. Keine Submodule, keine Multi-Module-Struktur — wie vom Soll (M1) gefordert (evolutionär, kein Modulschnitt).
### 2.3 Java-Pakete (Ist-Stand)
```
de.gecheckt.asv
├── domain (package-info.java)
│ └── model
│ ├── Field (record)
│ ├── Segment (record)
│ ├── Message (record)
│ └── InputFile (record)
├── application (package-info.java)
│ ├── InputFileValidator (Interface)
│ ├── DefaultInputFileValidator (Orchestrator)
│ ├── model
│ │ ├── ValidationSeverity (enum INFO/WARNING/ERROR)
│ │ ├── ValidationError (record, 9 Felder)
│ │ └── ValidationResult (final class)
│ ├── field
│ │ ├── FieldValidator (Interface)
│ │ └── DefaultFieldValidator (Preview-Fachlogik)
│ └── structure
│ ├── StructureValidator (Interface)
│ └── DefaultStructureValidator (Preview, 19 ASVREC/ASVFEH-Regeln)
├── adapter
│ ├── in
│ │ └── cli (package-info.java)
│ │ └── AsvValidatorApplication (main, run, parseFile, printUsage)
│ └── out
│ ├── filesystem (package-info.java leer, AP07-Vorbehalt)
│ ├── parsing (package-info.java)
│ │ ├── InputFileParser (Interface)
│ │ ├── DefaultInputFileParser (ein Message pro Datei)
│ │ ├── SegmentLineTokenizer (Interface)
│ │ ├── DefaultSegmentLineTokenizer (hartes '+')
│ │ └── InputFileParseException
│ ├── reporting (package-info.java)
│ │ └── ValidationResultPrinter (Konsole)
│ └── logging (package-info.java)
│ └── LoggingConfigurator (Stub, AP07-Vorbehalt)
└── bootstrap (package-info.java leer, AP06-Vorbehalt)
```
Noch **nicht** angelegt: `adapter.out.crypto` (planmäßig erst in M8).
### 2.4 Konfigurationsdateien
| Datei | Zweck | Status |
|---|---|---|
| `pom.xml` | Maven-Build, Dependencies, Plugins | AP02-gehärtet (SLF4J, JaCoCo, PIT-Profil, Mockito-Agent, `maven-jar-plugin` mit Platzhalter-Main-Class) |
| `src/main/resources/log4j2.xml` | Log4j2-Konfiguration (Console→STDERR, File→`logs/asv-format-validator.log`) | AP04-gesetzt |
| `.editorconfig` | Encoding UTF-8, LF, 4 Spaces | AP02 |
| `.gitattributes` | LF-Policy | AP02 |
| `.classpath`, `.project`, `.settings/` | Eclipse-Projektfiles | Repo-Altbestand, unverändert |
### 2.5 Testklassen
21 Testklassen, 147 Tests gesamt (bestätigt durch `mvn clean verify`):
- `domain/model/*` — 4 Testklassen, 38 Tests (Record-Konstruktoren, Null-Guards)
- `adapter/out/parsing/*` — 2 Testklassen, 11 Tests
- `adapter/out/reporting/*` — 1 Testklasse, 3 Tests
- `adapter/in/cli/*` — 2 Testklassen, 6 Tests (`AsvValidatorApplicationTest`, `…AdditionalTest`)
- `application/*` — 1 Testklasse, 5 Tests (Orchestrator)
- `application/field/*` — 1 Testklasse, 9 Tests
- `application/model/*` — 2 Testklassen, 5 Tests
- `application/structure/*` — 8 Testklassen, 70 Tests (Preview-Regelabdeckung)
Eine Testklasse fällt auf: `DefaultStructureValidatorTestAdditional` enthält keine `@Test`-Methoden (seit AP01 dokumentiert, Aufräumen ist AP10 zugeordnet).
---
## 3. M1-Abnahmekriterien — Status je Punkt
Bewertung je Kriterium gegen den tatsächlichen Code (nicht gegen die bisherigen Berichte).
### 3.1 „Anwendung ist als JAR unter Windows mit Java 21 startbar"
**🔶 TEILWEISE.**
Der `pom.xml` konfiguriert zwar `maven-jar-plugin` mit `<mainClass>de.gecheckt.asv.bootstrap.Main</mainClass>` ([pom.xml:126](pom.xml#L126)), doch diese Klasse existiert noch nicht (Paket `bootstrap` enthält ausschließlich `package-info.java`). Ein `mvn clean package` würde ein JAR mit ungültigem Manifest-Eintrag erzeugen; `java -jar asv-format-validator.jar <datei>` schlägt mit `Error: Could not find or load main class de.gecheckt.asv.bootstrap.Main` fehl. Der aktuelle tatsächliche Einstiegspunkt ist [`de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication#main`](src/main/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplication.java:81) — der wird aber vom Manifest nicht referenziert. AP06 muss die `bootstrap.Main`-Klasse anlegen.
### 3.2 „Eingabedatei wird technisch entgegengenommen; falsches oder fehlendes Argument führt zu Exit-Code `2` mit Minimalbericht"
**❌ FEHLT** (semantisch falsch und ohne Minimalbericht).
Kritische Abweichungen ([AsvValidatorApplication.java:36-39](src/main/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplication.java:36)):
```java
private static final int EXIT_CODE_SUCCESS = 0;
private static final int EXIT_CODE_INVALID_ARGUMENTS = 1; // Soll: 2
private static final int EXIT_CODE_FILE_ERROR = 2; // Soll: 2 (gleicher Wert, aber semantisch mit invalid args zu vereinen)
private static final int EXIT_CODE_VALIDATION_ERRORS = 3; // Soll: 1
```
Konsequenzen gegen den Soll-Rahmen:
- Fehlendes Argument → Exit `1` (sollte `2`) — `AsvValidatorApplication.java:97`
- Validierungsfehler → Exit `3` (sollte `1`) — `AsvValidatorApplication.java:113`
- Es existiert **vier** Exit-Codes statt der in Spec und `technik-und-architektur.md` vorgeschriebenen **drei** (`0/1/2`).
- Ein **Minimalbericht** bei Bedienfehlern wird nicht erzeugt. Bei fehlenden Argumenten ruft `printUsage()` nur `System.out.println(…)` auf; bei nicht lesbarer Datei wird `System.err.println("Fehler beim …")` geschrieben. Beide Wege erzeugen weder Berichtdatei noch strukturierten Text.
### 3.3 „Bericht- und Log-Datei werden im Eingabeverzeichnis mit korrekter Suffix-Logik erzeugt (`_v1`, `_v2`, …)"
**❌ FEHLT** vollständig.
- Keine Berichtdatei-Erzeugung im Code. `ValidationResultPrinter#printToConsole` schreibt ausschließlich auf `System.out` ([ValidationResultPrinter.java:22](src/main/java/de/gecheckt/asv/adapter/out/reporting/ValidationResultPrinter.java:22)).
- Die Log-Datei wird statisch nach `logs/asv-format-validator.log` relativ zum Arbeitsverzeichnis geschrieben ([log4j2.xml:7](src/main/resources/log4j2.xml:7)), nicht in das Verzeichnis der Eingabedatei.
- Keine Suffix-Logik `_v1/_v2/…` vorhanden (Grep `_v1` im Hauptcode: keine Treffer).
- `adapter.out.filesystem` ist leer. `LoggingConfigurator#configureLogFile(Path)` ist ein Stub (`// TODO: dynamische Log-Datei-Umleitung in AP07`).
### 3.4 „Log4j2-Bindung ist außerhalb von Bootstrap und Logging-Adapter nicht sichtbar"
**✅ ERFÜLLT.**
Grep über `src/`: `org.apache.logging.log4j` erscheint in **keinem einzigen** Produktions- oder Testcode-Import (`Grep org.apache.logging.log4j` → keine Treffer). Die Log4j2-Abhängigkeit wird ausschließlich per Runtime-Binding (`log4j-slf4j2-impl`, Scope `runtime`) gebunden. `LoggingConfigurator` liegt korrekt im erlaubten Paket, importiert aktuell aber noch keine Log4j2-Typen (Stub-Zustand). Das in AP10 vorgesehene Architekturgate kann diese Regel formal sichern.
### 3.5 „Befundmodell trennt Spec-Urteil und Diagnose strukturell"
**❌ FEHLT** vollständig.
Der bestehende `ValidationError`-Record ([ValidationError.java:20](src/main/java/de/gecheckt/asv/application/model/ValidationError.java:20)) hat neun Felder (`errorCode`, `description`, `severity`, `segmentName`, `segmentPosition`, `fieldName`, `fieldPosition`, `actualValue`, `expectedRule`). Keines davon trägt die Unterscheidung zwischen verbindlichem **Spec-Urteil** und zusätzlicher **diagnostischer Weiteranalyse** — beides wandert undifferenziert in eine gemeinsame `List<ValidationError>` in `ValidationResult`. Zentrale Soll-Metadaten (Artefaktschicht, Prüfstufe, Prüfbereich, Rohwert, Position, Nachrichtenbezug, optionaler offizieller Fehlercode) fehlen ebenfalls. Das ist AP05-Scope.
### 3.6 „Build und Tests sind grün"
**✅ ERFÜLLT.**
`mvn clean verify``BUILD SUCCESS`, `Tests run: 147, Failures: 0, Errors: 0, Skipped: 0` (lokal reproduziert am 2026-04-20). JaCoCo-Report wird erzeugt, keine Schwellwerte (kommt erst M9). Warnungen: nur die bekannte HotSpot-Sharing-Warnung aus AP02 (kosmetisch). Bei den CLI-Tests wird eine erwartete `ERROR`-Zeile „Fehler beim Lesen der Datei: File does not exist: /non/existent/file.txt" ausgegeben — Test-Nebeneffekt, nicht fehlerhaft.
### 3.7 Ergebnis-Tabelle
| Abnahmekriterium | Status |
|---|---|
| JAR unter Windows mit Java 21 startbar | 🔶 |
| Eingabedatei/Exit-Code 2/Minimalbericht | ❌ |
| Bericht/Log im Eingabeverzeichnis mit Suffix-Logik | ❌ |
| Log4j2-Bindung gekapselt | ✅ |
| Befundmodell Spec-/Diagnose-Trennung | ❌ |
| Build und Tests grün | ✅ |
Zwei von sechs erfüllt, einer teilweise, drei fehlend.
---
## 4. Soll-Ist-Vergleich Paketstruktur
| Soll-Paket | Ist-Stand | Bewertung |
|---|---|---|
| `domain` | Unterpaket `domain.model` mit 4 Records (`Field`, `Segment`, `Message`, `InputFile`); `package-info.java` auf `domain`-Ebene vorhanden | **vorhanden, aber flach** — noch kein Befundmodell, noch keine fachliche Schichtung |
| `application` | Orchestrator `DefaultInputFileValidator` + `application.model.*` + `application.field.*` + `application.structure.*` | **vorhanden, mischt Orchestrierung und Preview-Fachregeln** — AP09 muss entscheiden, wie die Preview-Teile markiert/abgetrennt werden |
| `adapter.in.cli` | `AsvValidatorApplication` (main + run + parseFile + printUsage) | **vorhanden, aber überladen** — enthält Bootstrap-Wiring, Dateisystemzugriff, CLI-Parsing und Fehlerbehandlung in einer Klasse |
| `adapter.out.filesystem` | leer (nur `package-info.java`) | **angelegt, unbefüllt** — AP07-Vorbehalt |
| `adapter.out.parsing` | 5 Klassen (Parser/Tokenizer + Exception) | **vorhanden** — Preview-Parser mit hartem `+` als Separator |
| `adapter.out.crypto` | nicht angelegt | **fehlt planmäßig** — ist M8-Scope, in M1 nicht gefordert |
| `adapter.out.reporting` | `ValidationResultPrinter` | **vorhanden** — nur Konsolenformat, kein Dateioutput |
| `adapter.out.logging` | `LoggingConfigurator` (Stub) | **angelegt, unbefüllt** — dynamische Log-Datei-Umleitung ist AP07 |
| `bootstrap` | leer (nur `package-info.java`) | **angelegt, unbefüllt**`Main` ist AP06-Scope |
Fazit: Die Paketstruktur ist aus AP03 heraus formal vollständig. Die Inhalte sind asymmetrisch — das Parser-, Validator- und Modellpaket ist „voll" (z.T. mit Preview-Code), das Bootstrap- und das Filesystem-Paket sind leer.
---
## 5. Architekturprinzipien — Befunde
### 5.1 Keine Log4j2-Typen außerhalb `adapter.out.logging`/`bootstrap`
**✅ eingehalten.** Grep `org.apache.logging.log4j` über `src/` liefert keine Treffer. Nach AP04 ist die einzig erlaubte Anlaufstelle `LoggingConfigurator` (der aktuell selbst keine Log4j2-Typen importiert, da er nur Stub ist).
### 5.2 Keine Infrastrukturabhängigkeiten in `domain` oder `application`
**⚠️ größtenteils eingehalten, mit einem Rest-Risiko.**
- `domain.*` importiert nur `java.util.*` und `java.util.Optional`**sauber**.
- `application.*` importiert keine CLI-, Filesystem-, Logging- oder Parser-Typen. Grep bestätigt: kein `java.nio.file`, kein `System.out/err`, kein `org.slf4j` in `application/`.
- **Rest-Risiko:** Die Klassen `application.structure.DefaultStructureValidator` (19 Regeln, u.a. zu `UNH`/`UNT`, `ASVREC`/`ASVFEH`, Rechnungskennzeichen, FHL) und `application.field.DefaultFieldValidator` enthalten bereits fachliche ASV-Regelkenntnis. Formal gehören solche Regeln ins Domänen- bzw. Regelpaket, nicht in die „Application"-Orchestrierungsschicht. Da AP09 vorsieht, diese Altlogik explizit als Vorbau einzufrieren und erst in M3+ wieder aufzunehmen, ist das derzeit akzeptierte Schulden — aber es verletzt den Soll-Zuschnitt leise (Fachregeln in `application/*` statt in einer eigenständigen Regelschicht).
### 5.3 Manuelle Constructor Injection, kein DI-Framework
**✅ eingehalten.** `AsvValidatorApplication` besitzt einen Default-Konstruktor, der alle Komponenten manuell per `new` verdrahtet ([AsvValidatorApplication.java:50-60](src/main/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplication.java:50)), und einen Test-Konstruktor mit Constructor Injection ([AsvValidatorApplication.java:70](src/main/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplication.java:70)). `DefaultInputFileValidator` nimmt `StructureValidator` und `FieldValidator` per Constructor entgegen ([DefaultInputFileValidator.java:33](src/main/java/de/gecheckt/asv/application/DefaultInputFileValidator.java:33)). Kein Spring, kein Quarkus, kein CDI, kein Guice in den Dependencies.
Anmerkung: Die Wiring-Logik liegt derzeit im CLI-Adapter selbst; der Soll-Zuschnitt will sie in `bootstrap` sehen (AP06).
### 5.4 Befundmodell unterscheidet Spec-Urteil und Diagnose
**❌ nicht eingehalten.** Siehe 3.5. Das aktuelle `ValidationResult` kennt nur `ERROR`/`WARNING`/`INFO`, keine Dimension „spec-verbindlich vs. diagnostisch".
### 5.5 Exit-Codes `0/1/2` korrekt verdrahtet
**❌ nicht eingehalten.** Siehe 3.2. Ist-Stand ist `0/1/2/3` mit teilweise invertierter Semantik. Konkrete Stellen: `AsvValidatorApplication.java:36-39` (Konstanten), `:97` (falscher Code bei fehlendem Argument), `:113` (falscher Code bei Validierungsfehler).
### 5.6 Weitere prinzipielle Befunde
- **Eingabe-Encoding ISO-8859-15 (`technik-und-architektur.md` §„Zeichensätze"):** Das Ist liest mit `StandardCharsets.UTF_8` ([AsvValidatorApplication.java:152](src/main/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplication.java:152)). `java.nio.charset.StandardCharsets` bietet kein ISO-8859-15, man muss `Charset.forName("ISO-8859-15")` nutzen. Aktuelle Fehlkodierung verfälscht alle Umlaute (ä=0xE4 statt UTF-8-Doppelbyte) und jeden Euro-Sonderfall (`€`).
- **Schreiben statt Speichern der Bericht-/Log-Datei:** Die Bericht- und Log-Dateien müssten laut Soll in UTF-8 in das Verzeichnis der Eingabedatei geschrieben werden. Keine dieser Anforderungen ist umgesetzt.
- **EDIFACT-Konformität des Tokenizers:** `DefaultSegmentLineTokenizer` trennt starr an `+` ([Zeile 13](src/main/java/de/gecheckt/asv/adapter/out/parsing/DefaultSegmentLineTokenizer.java:13)). Das UNA-Segment und die Gruppenelementtrennung `:` werden nicht ausgewertet. Nicht M1-Scope (gehört zu M3), aber Rest-Risiko bei Tests und als Vorbau.
- **Parser-Vereinfachung:** `DefaultInputFileParser` erzeugt pro Datei genau eine `Message` ([Zeile 54](src/main/java/de/gecheckt/asv/adapter/out/parsing/DefaultInputFileParser.java:54)), unabhängig von UNH/UNT-Gruppen. Preview-Verhalten.
---
## 6. Qualitätsstatus (Build/Test)
> Hinweis zu den widersprüchlichen Vorgaben des Auftrags: Die Einleitung spricht von „machst kein mvn", Abschnitt 5 fordert jedoch explizit `mvn clean verify`, der abschließende Teil präzisiert auf „kein `mvn package`". Der Autor dieses Berichts interpretiert den spezifischen Auftrag in Abschnitt 5 als maßgeblich; `mvn clean verify` wurde einmalig ausgeführt. Kein `mvn package`, kein `git`, kein Schreiben außerhalb dieses Berichts.
- **`mvn clean verify`:** ✅ `BUILD SUCCESS` (lokal, 2026-04-20).
- **Tests:** 147 gesamt, 0 Failures, 0 Errors, 0 Skipped. Alle Testklassen der hexagonalen Struktur laufen durch.
- **Compile-/Javac-Warnungen:** keine über die aus AP02 bekannten hinaus.
- **JVM-Warnungen zur Laufzeit:**
- `Java HotSpot(TM) 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended` — kosmetisch, stammt vom JaCoCo-Agent, bekannt seit AP02.
- Eine erwartete `ERROR`-Log-Zeile aus dem CLI-Test (nicht-existente Datei) ist sichtbar, wird aber nicht als Testfehler gewertet. Sie wäre zum M1-Ende über ein Logback-Test-Filter oder eine angepasste Test-Log-Konfiguration in AP10 unterdrückbar — kein Blocker.
- **Mutation Testing:** In AP02 einmalig ausgeführt (249 Mutationen, 83 % getötet), Schwellwerte kommen erst M9.
- **Coverage:** JaCoCo-Report unter `target/site/jacoco/`; keine Schwellwerte aktiv.
---
## 7. Preview-Code / Altlogik
### 7.1 Identifikation
Als **Preview-Code** der Sondierungsphase (README, Abschnitt „Preview-Code"; AP01 Klassifikation) gelten:
| Klasse | Paket | M1-Bewertung | Verdrahtung im Lauf |
|---|---|---|---|
| `DefaultInputFileParser` | `adapter.out.parsing` | Behalten, für M3 überarbeiten (Trennzeichen, Multi-Message) | aktiv in `AsvValidatorApplication` |
| `DefaultSegmentLineTokenizer` | `adapter.out.parsing` | Behalten, für M3 überarbeiten (UNA) | aktiv |
| `InputFileParser`/`SegmentLineTokenizer`/`InputFileParseException` | `adapter.out.parsing` | Behalten als stabile Ports | aktiv |
| `DefaultFieldValidator` | `application.field` | **einzufrieren (AP09)** — keine M1-Weiterentwicklung | aktiv über `DefaultInputFileValidator` |
| `DefaultStructureValidator` (19 ASVREC/ASVFEH-Regeln) | `application.structure` | **einzufrieren (AP09)** — 19 fachliche Regeln, M3+-Vorbau | aktiv über `DefaultInputFileValidator` |
| `ValidationError`, `ValidationResult`, `ValidationSeverity` | `application.model` | umzubauen/abzulösen durch Befundmodell mit Spec-/Diagnose-Trennung (AP05) | aktiv als einzige Befundträger |
| `ValidationResultPrinter` | `adapter.out.reporting` | Behalten als Konsolen-Formatierer | aktiv |
### 7.2 Aktiv oder eingefroren?
Alle Preview-Klassen sind **aktiv** in den Lauf verdrahtet: Der Default-Konstruktor von `AsvValidatorApplication` baut parser + validator + printer komplett mit der Preview-Logik zusammen ([AsvValidatorApplication.java:52-59](src/main/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplication.java:52)). Der produktive Lauf führt deshalb heute schon fachliche ASVREC-/ASVFEH-Prüfungen aus — nominell M3+-Scope, real vorhandenes Verhalten. Ein formales „Einfrieren" (Kommentar-Marker, Paketumzug oder gezielte Deaktivierung) ist bis zum AP09 noch nicht erfolgt.
### 7.3 Risiken des Preview-Codes für M1
1. **Fachliche Befunde in M1:** Solange `DefaultStructureValidator` aktiv bleibt, liefert ein M1-Lauf bereits ASVREC-/ASVFEH-Befunde. Das widerspricht der M1-Vorgabe „noch keine ASV-Fachvalidierung" (`meilensteine.md` §M1 Ziel, und AP-README „Keine Fachvalidierung in M1").
2. **Exit-Code-Verwechslung:** Weil Fachbefunde schon produziert werden, schlagen sie über `hasErrors()` auf den (falschen) Exit-Code `3` durch. Ein M1-Lauf einer beliebigen Textdatei liefert deshalb heute realistisch `3`, nicht das geplante `0` (gültig mangels Regeln) oder das korrekte `1` (ungültig).
3. **Hartkodiertes `+` als Trennzeichen:** Jede Datei ohne ASV-typische EDIFACT-Struktur wird stumm „erfolgreich" geparst. Für M1-Akzeptanz mit einer Minimaldatei akzeptabel; für alles Reale gefährlich.
4. **UTF-8-Lesen:** Alle Preview-Tests laufen auf UTF-8-kodierten `*.asv`-Testressourcen. Beim Umstellen auf ISO-8859-15 werden vorhandene Testdaten zum Risiko — Umstellung muss zusammen mit Testressourcen-Anpassung erfolgen.
5. **`DefaultStructureValidatorTestAdditional` leer:** Eine Testklasse ohne `@Test`-Methoden bleibt seit AP01 bestehen und ist ein Restposten für AP10.
6. **Preview-Regeln als „V1-N"-Kandidaten:** Einige der 19 Regeln (z.B. strikte Reihenfolgeerzwingung der ASVREC-Segmente, FHL-Pflicht für ASVFEH) greifen zu rigide, wenn sie gegen die finalen Soll-Regelklassifikationen (`V1-V/T/N/K`) gestellt werden. Beim Wiederaufnehmen ab M3 ist jede Preview-Regel neu zu bewerten — das ist kein M1-Problem, aber ein M3-Erbe.
---
## 8. Offene Punkte und Risiken
### 8.1 Architekturrisiken
- **R-ARCH-01 — Exit-Code-Semantik falsch und erweitert.** Der Ist-Code hat vier Codes mit vertauschter Bedeutung. Muss in AP06 korrigiert werden, damit AP11 abnehmbar ist. Bis dahin ist ein M1-Lauf nicht spec-konform.
- **R-ARCH-02 — Eingabe-Encoding hartkodiert UTF-8.** Widerspricht direkt dem Soll-Rahmen. `StandardCharsets` kennt kein ISO-8859-15; `Charset.forName("ISO-8859-15")` muss genutzt werden, JDK-Verfügbarkeit ist zu verifizieren. Umstellung ist in AP06 oder spätestens in AP07 zu verankern.
- **R-ARCH-03 — Keine Spec-/Diagnose-Trennung im Befundmodell.** Das aktuelle `ValidationError`/`ValidationResult` reicht für die Fortführung ab M2 nicht aus. Es muss in AP05 komplett neu geschnitten (oder stark erweitert) werden, inklusive aller Metadatenfelder aus `technik-und-architektur.md` § „Befundarten". Weil der Preview-`DefaultStructureValidator` dieses Modell produktiv befüllt, wird der Umbau nicht kostenlos — AP09 muss die Altlogik zuvor entkoppeln.
- **R-ARCH-04 — Ausgabeartefakte fehlen komplett.** Weder Berichtdatei noch Logdatei werden im Eingabeverzeichnis erzeugt; die Suffix-Logik existiert nicht. Das ist der größte Einzel-Arbeitsblock in M1 (AP07).
- **R-ARCH-05 — Preview-Code läuft produktiv mit und erzeugt Fachbefunde.** Siehe 7.3. AP09 muss klären: entkoppeln, per Default deaktivieren oder nur inert im Classpath behalten.
### 8.2 Implementierungslücken (aus M1 noch offen)
- **L-AP05** — Befundmodell mit Spec-/Diagnose-Trennung.
- **L-AP06** — `bootstrap.Main`-Klasse, sauberes CLI-Wiring, Exit-Codes `0/1/2`, ISO-8859-15.
- **L-AP07** — `adapter.out.filesystem` mit Dateioutput; `LoggingConfigurator` mit dynamischer Log-Datei-Umleitung; Suffix-Logik `_v1/_v2/…`.
- **L-AP08** — Minimalbericht bei Bedienfehlern (Exit-Code `2`).
- **L-AP09** — Altlogik einfrieren (oder deaktivieren), damit M1-Läufe keine unerwarteten Fachbefunde mehr erzeugen.
- **L-AP10** — Architekturtest (Paketabhängigkeiten, Log4j2-Sichtbarkeit, idealerweise auch Exit-Code-Semantik und Preview-Deaktivierung).
- **L-AP11** — End-to-End-Abnahme mit Minimaldatei; Berichtskonsolidierung.
### 8.3 Unklarheiten / Entscheidungsbedarf
- **E-01: Wie wird der Preview-`DefaultStructureValidator` in AP09 konkret eingefroren?** Drei Optionen: (a) in ein Sub-Paket `application.preview.structure` verschieben und den Orchestrator gegen eine Null-Implementation austauschen; (b) in Ort belassen, den Orchestrator im Bootstrap aber nicht mehr damit verdrahten; (c) weiterlaufen lassen, aber dessen Befunde im neuen Befundmodell als „Diagnose" klassifizieren. Jede Option hat andere Auswirkungen auf AP05 und AP10.
- **E-02: Wie wird der Test-Log-Rauschen (`ERROR`-Zeile im Negativ-Test) eliminiert?** Eigene Test-Log-Konfiguration oder bewusst stehen lassen? AP10 sollte hier eine Entscheidung zusichern.
- **E-03: Was passiert mit `DefaultStructureValidatorTestAdditional`?** Stummes Artefakt seit AP01 — in AP10 entfernen oder mit Tests füllen?
- **E-04: Soll der aktuelle Inhalt `logs/asv-format-validator.log` (Arbeitsverzeichnis) nach AP07 noch eine Rolle spielen?** Wenn die Logdatei künftig neben die Eingabedatei geschrieben wird, wird der aus AP04 konfigurierte statische Pfad überflüssig. Der `logs/`-Ordner im Repo-Root ist Resultat früherer Läufe und gehört vermutlich `.gitignore`'d.
- **E-05: JAR-Aufbau als „fat jar" oder als JAR mit Classpath?** `maven-jar-plugin` alleine erzeugt kein Uber-JAR — `java -jar asv-format-validator.jar` würde ohne `maven-shade-plugin` oder `Class-Path`-Manifest ohne Log4j2/SLF4J starten. AP06 muss dies entscheiden (die AP-Texte nennen nur `maven-jar-plugin`).
---
## 9. Empfehlung: nächste sinnvolle Arbeitspakete für M1
Reihenfolge auf Basis der Abhängigkeiten in `docs/arbeitspakete/m1/README.md` und der oben sichtbaren Risiken:
1. **AP05 — Befundmodell mit Spec-/Diagnose-Trennung.** Höchste Hebelwirkung: freigeschaltet sind danach AP06 (Exit-Codes bauen auf dem neuen Urteil auf), AP07 (Bericht rendert das neue Modell) und AP09 (Preview-Befunde lassen sich als Diagnose klassifizieren). Unmittelbar angreifbar, keine vorangehenden APs mehr offen.
2. **AP06 — Bootstrap und CLI.** Exit-Codes `0/1/2` korrigieren, `bootstrap.Main` anlegen, Wiring aus `AsvValidatorApplication` herauslösen, ISO-8859-15 einziehen. Nur mit AP05-Modell sinnvoll, weil `hasErrors()` des neuen Befundmodells den Urteils-Exit-Code speist.
3. **AP09 — Altlogik einfrieren.** Direkt nach AP06 sinnvoll, weil ab dann der Bootstrap sauber entscheiden kann, ob und wie die Preview-Validatoren verdrahtet werden. Ohne AP09 bringt AP05 keinen realen Effekt — `DefaultStructureValidator` produziert weiter Fachbefunde.
4. **AP07 — Ausgabeartefakte.** Nach AP06 sinnvoll, weil dann eine stabile Ausgangs-CLI vorliegt, in die die Dateilogik eingezogen werden kann. `LoggingConfigurator` wird hier Leben bekommen.
5. **AP08 — Minimalbericht.** Reine Veredelung der Bedienfehler-Pfade auf Basis von AP07.
6. **AP10 — Architekturtest.** Sobald AP04AP09 stehen, kann ein ArchUnit-Scan die Log4j2-Sichtbarkeit, die Paketabhängigkeiten, die Exit-Codes (als Konstanten) und die Preview-Deaktivierung formell prüfen.
7. **AP11 — M1-Abnahme.** Abschlusslauf mit Minimaldatei, Konsolidierung aller Berichte.
Wichtig: Jedes dieser APs bleibt ein **eigener Claude-Lauf**. Diese Ist-Analyse hier bündelt nur die Ausgangslage; sie darf in späteren APs zitiert, aber nicht als implizite Entscheidung missverstanden werden — insbesondere die Optionen zu E-01 bis E-05 sind weiterhin offen.
@@ -0,0 +1,101 @@
# Abschlussbericht Arbeitspaket AP05 Befundmodell mit Spec-/Diagnose-Trennung
> **Bezug:** `docs/arbeitspakete/m1/AP05-befundmodell.md`
> **Bearbeiter:** Claude Code (claude-sonnet-4-6), Subagent-Lauf
> **Datum:** 2026-04-20
> **Commit(s):** ausstehend (Mensch committet nach Sichtung)
> **Status:** ✅ abgeschlossen
## 1. Zusammenfassung
Das Paket `de.gecheckt.asv.domain.finding` wurde vollständig neu angelegt und enthält alle im AP05 geforderten Typen: die Enums `Severity`, `FindingKind`, `FindingLayer`, `Verdict`, den unveränderlichen Record `Finding` mit Builder sowie die unveränderliche Klasse `ValidationReport`. Die zentrale Invariante — dass DIAGNOSTIC-ERROR das Verdict niemals auf INVALID setzt — ist durch einen explizit markierten Unit-Test (`diagnosticErrorLiefertVALID`) abgesichert. `mvn clean verify` ist grün (164 Tests, 0 Fehler).
## 2. Umgesetzte Änderungen
Folgende Dateien wurden **neu angelegt** (keine bestehende Datei wurde verändert):
**Produktionscode (`src/main/java/de/gecheckt/asv/domain/finding/`):**
- `Severity.java` — Enum: ERROR, WARNING, HINT; JavaDoc erklärt Einfluss auf Verdict
- `FindingKind.java` — Enum: SPEC, DIAGNOSTIC; JavaDoc beschreibt Verdict-Invariante
- `FindingLayer.java` — Enum: ARTIFACT, TECHNICAL_STRUCTURE, DOMAIN_MODEL; JavaDoc erklärt Schichttrennung
- `Verdict.java` — Enum: VALID, INVALID, OPERATIONAL_ERROR; Exit-Code-Zuordnung in JavaDoc
- `Finding.java` — Record mit allen 12 Feldern gemäß AP05; Null-Prüfung im Kompaktkonstruktor für Pflichtfelder; zusätzlicher innerer `Builder` für komfortablere Erzeugung; Hilfsmethode `isSpecError()`
- `ValidationReport.java` — unveränderliche Klasse; `findings` intern per `List.copyOf()` gesichert; Methoden `computeVerdict()`, `hasSpecErrors()`, `specFindings()`, `diagnosticFindings()`, `getFindings()`, `getFileName()`, `getTimestamp()`; Factory `operationalError(fileName, ruleId, message)`; privater Konstruktor für Bedienfehler-Flag
**Tests (`src/test/java/de/gecheckt/asv/domain/finding/`):**
- `ValidationReportTest.java` — 11 Tests (die 7 Pflicht-Tests aus AP05 plus 4 ergänzende Absicherungen)
- `FindingTest.java` — 6 Tests für Record, Builder und Null-Absicherung
**Keine bestehende Datei wurde verändert.** Insbesondere wurde `ValidationResult` (Altmodell) nicht angefasst.
## 3. Scope-Treue
| Scope-Punkt aus dem Arbeitspaket | Erfüllt? | Bemerkung |
|---|---|---|
| Paket `domain.finding` anlegen | ✅ | `de.gecheckt.asv.domain.finding` vollständig vorhanden |
| Enum `Severity` (ERROR/WARNING/HINT) | ✅ | Alle drei Werte implementiert |
| Enum `FindingKind` (SPEC/DIAGNOSTIC) | ✅ | Beide Werte mit präzisem JavaDoc |
| Enum `FindingLayer` (ARTIFACT/TECHNICAL_STRUCTURE/DOMAIN_MODEL) | ✅ | Alle drei Werte implementiert |
| `Finding` Record mit allen 12 Feldern | ✅ | Alle Felder vorhanden; `germanMessage` nicht nullable; übrige 11 nullable |
| `Verdict` Enum (VALID/INVALID/OPERATIONAL_ERROR) | ✅ | Alle drei Werte mit Exit-Code-Kommentar |
| `ValidationReport` unveränderliche Klasse | ✅ | `final`, findings per `List.copyOf()` gesichert |
| `computeVerdict()` nur SPEC+ERROR | ✅ | Invariante im Code und per Test abgesichert |
| `hasSpecErrors()` | ✅ | Implementiert |
| `specFindings()` / `diagnosticFindings()` | ✅ | Implementiert, per Test verifiziert |
| `operationalError(fileName, ruleId, message)` | ✅ | Factory-Methode vorhanden |
| Keine Änderung an `ValidationResult` | ✅ | Altmodell nicht angefasst |
| Integration in laufende Validierung (Scope OUT) | ✅ nicht gemacht | AP06 |
| Löschen/Umbenennen `ValidationResult` (Scope OUT) | ✅ nicht gemacht | AP09 |
| Berichtserzeugung/Textrendering (Scope OUT) | ✅ nicht gemacht | AP07 |
| Architekturtest (Scope OUT) | ✅ nicht gemacht | AP10 |
**Wurde der Scope eingehalten?** Ja, vollständig.
**Wurden Dinge außerhalb des Scopes gemacht?** Nein. Der `Builder` für `Finding` war im AP05 explizit als Alternative zum reinen Record erwähnt und fällt daher in den Scope.
## 4. Abnahmekriterien
| Abnahmekriterium aus dem Arbeitspaket | Erfüllt? | Nachweis |
|---|---|---|
| Paket `domain.finding` enthält alle genannten Typen | ✅ | 5 Dateien: `Severity`, `FindingKind`, `FindingLayer`, `Verdict`, `Finding`, `ValidationReport` |
| **Test „DIAGNOSTIC-ERROR ergibt VALID" ist grün** | ✅ | `ValidationReportTest#diagnosticErrorLiefertVALID()` — GRÜN; `@DisplayName("KRITISCH: Ein DIAGNOSTIC-ERROR-Befund liefert Verdict VALID (niemals INVALID)")` |
| `ValidationReport.findings` ist unveränderlich (Test vorhanden) | ✅ | `ValidationReportTest#findingsListeNichtModifizierbar()` testet: (1) Mutation der Eingabeliste nach Übergabe hat keinen Effekt; (2) `getFindings().add(...)` wirft `UnsupportedOperationException` |
| Alle Metadatenfelder aus technik-und-architektur.md §„Befundarten" im `Finding`-Typ | ✅ | Alle 12 Felder vorhanden: Artefaktschicht (`layer`), Prüfstufe/Art (`kind`), Segmenttyp, Segmentindex, Feld-ID, Rohwert, Position, Nachrichtenreferenz, offizieller Fehlercode, interne Regel-ID, Schweregrad, deutscher Befundtext |
| `operationalError(...)` Factory-Methode existiert | ✅ | Statische Methode in `ValidationReport`; Testnachweis: `ValidationReportTest#operationalErrorFactoryLiefertOPERATIONAL_ERROR()` |
| Keine Änderung an `ValidationResult` (Altmodell) | ✅ | Dateipfad `application/model/ValidationResult.java` wurde nicht geöffnet oder verändert |
| `mvn clean verify` grün | ✅ | 164 Tests, 0 Failures, 0 Errors, 0 Skipped |
| Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP05-bericht.md` | ✅ | Diese Datei |
## 5. Build- und Teststatus
- `mvn clean verify`: ✅ grün
- Anzahl Tests gesamt: **164** (davon **17 neu** in `domain.finding`: 11 in `ValidationReportTest`, 6 in `FindingTest`)
- Vorherige Testanzahl (vor AP05): 147
- Coverage: JaCoCo läuft; das neue Paket `domain.finding` hat vollständige Abdeckung aller kritischen Pfade durch die 17 neuen Tests
- Warnungen beim Build: keine testbezogenen Warnungen; eine Java-Compiler-Warnung bezüglich Annotationsverarbeitung (`-proc:none` empfohlen) — diese war bereits vor AP05 vorhanden und ist kein AP05-Artefakt
## 6. Rest-Risiken und offene Punkte
- **Zwei parallele Ergebnistypen:** `ValidationResult` (Altmodell im Paket `application.model`) und `ValidationReport` (neues Befundmodell in `domain.finding`) koexistieren bewusst bis AP09. Dies ist im AP05 explizit als Absicht dokumentiert und kein Risiko dieses Arbeitspakets.
- **`operationalError`-Befund nutzt `FindingLayer.ARTIFACT`:** Der Bedienfehler-Befund wurde auf Schicht `ARTIFACT` abgelegt, da Bedienfehler typischerweise die Artefaktebene betreffen (nicht lesbare Datei, fehlender Pfad). Falls AP08 eine andere Schicht bevorzugt, ist das in der Factory anpassbar ohne API-Bruch.
- **`Finding.isSpecError()`** ist eine Hilfsmethode für interne Verwendung. Ob sie öffentlich bleiben soll oder auf `package-private` reduziert wird, kann in AP10 (Architekturtest) beurteilt werden.
- **Keine `package-info.java`** für das neue Paket `domain.finding` angelegt — dies war nicht im AP05-Scope, kann aber in einem Folge-AP ergänzt werden.
## 7. Empfehlungen für Folge-Arbeitspakete
- **AP06 (Bootstrap/CLI):** Der `ValidationReport` ist bereit zur Integration. Die Factory `operationalError(...)` deckt den Exit-Code-2-Pfad ab. `computeVerdict()` liefert direkt den Verdict für die Exit-Code-Entscheidung.
- **AP07 (Ausgabeartefakte):** Das `Finding`-Record ist flat und enthält alle Traceability-Informationen, die für einen hierarchischen Bericht benötigt werden. `specFindings()` und `diagnosticFindings()` erleichtern die Gliederung.
- **AP09 (Altlogik einfrieren):** `ValidationResult` und `ValidationReport` koexistieren sauber. AP09 kann `ValidationResult` einfrieren oder entfernen, ohne `ValidationReport` anzufassen.
- **AP10 (Architekturtest):** Das Paket `domain.finding` hat keine Infrastrukturabhängigkeiten (nur `java.time.Instant`, `java.util.*`). Der ArchUnit-Test sollte diese Eigenschaft formal absichern.
## 8. Reviewer-Checkliste
- [x] Alle im Arbeitspaket genannten Scope-IN-Punkte sind nachweislich umgesetzt
- [x] Keine Scope-OUT-Punkte wurden angefasst
- [x] Abnahmekriterien sind mit konkreten Nachweisen belegt (Tests, Dateipfade)
- [x] `mvn clean verify` ist grün (164 Tests, 0 Failures)
- [ ] Der Commit für dieses AP hat eine sprechende Message (`M1-AP05: ...`) — ausstehend, Mensch committet
- [x] Keine Regeln der Grunddokumente (Spec, Fachliche, Technik) wurden verletzt
- [x] Rest-Risiken sind ehrlich dokumentiert
@@ -0,0 +1,112 @@
# Abschlussbericht Arbeitspaket AP06 Bootstrap und CLI-Adapter
> **Bezug:** `docs/arbeitspakete/m1/AP06-bootstrap-cli.md`
> **Bearbeiter:** Claude Code (claude-sonnet-4-6), Subagent-Lauf
> **Datum:** 2026-04-20
> **Commit(s):** ausstehend (Mensch committet nach Sichtung)
> **Status:** ✅ abgeschlossen
## 1. Zusammenfassung
Die bisherige `AsvValidatorApplication` wurde in zwei klar getrennte Verantwortlichkeiten zerlegt: `de.gecheckt.asv.bootstrap.Main` übernimmt die manuelle Constructor Injection und den einzigen `main`-Einstiegspunkt, `de.gecheckt.asv.adapter.in.cli.CliRunner` übernimmt die CLI-Argument-Verarbeitung und die Exit-Code-Übersetzung. Exit-Codes wurden auf die normativen Werte 0/1/2 umgestellt, ISO-8859-15 als Eingabe-Encoding eingeführt, und das Uber-JAR wird nun über `maven-shade-plugin` erzeugt. `mvn clean verify` ist grün (168 Tests, 0 Fehler).
## 2. Umgesetzte Änderungen
**Neu angelegt:**
- `src/main/java/de/gecheckt/asv/adapter/in/cli/ExitCode.java` — Normative Exit-Code-Konstanten (VALID=0, INVALID=1, OPERATIONAL_ERROR=2). Alte Konstanten aus `AsvValidatorApplication` entfernt.
- `src/main/java/de/gecheckt/asv/adapter/in/cli/CliRunner.java` — CLI-Adapter; einziger Ort mit Argument-Parsing; nutzt `ValidationReport.computeVerdict()` zur Exit-Code-Ableitung; enthält keine Log4j2-Typen (nur SLF4J).
- `src/main/java/de/gecheckt/asv/application/FileValidationService.java` — Anwendungsschnittstelle für die Dateivalidierung, mit `ValidationReport validate(Path)`.
- `src/main/java/de/gecheckt/asv/application/DummyFileValidationService.java` — M1-Platzhalter; liest Datei mit `Charset.forName("ISO-8859-15")`, zählt Bytes, gibt leeren `ValidationReport` zurück. Konstante `INPUT_CHARSET` paketöffentlich für Testbarkeit.
- `src/main/java/de/gecheckt/asv/bootstrap/Main.java` — Einziger `public static void main`; verdrahtet `LoggingConfigurator`, `DummyFileValidationService`, `CliRunner` per Constructor Injection; delegiert Exit-Code an `System.exit`. Log4j2-Typen nur über `LoggingConfigurator` (in `adapter.out.logging`) sichtbar.
- `src/test/java/de/gecheckt/asv/adapter/in/cli/CliRunnerTest.java` — 5 Tests: kein Argument → 2, zwei Argumente → 2, nicht existierende Datei → 2, leere lesbare Datei → 0, operationalError-Report → 2.
- `src/test/java/de/gecheckt/asv/application/DummyFileValidationServiceTest.java` — 4 Tests: leere Datei → VALID, Datei mit Inhalt → VALID, Byte 0xA4 → Euro-Zeichen €, INPUT_CHARSET-Name korrekt.
**Geändert:**
- `src/main/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplication.java` — Auf leere Hülle reduziert; `main` delegiert an `Main.main`. Mit `@Deprecated(forRemoval=true)` markiert. Wird in AP09 endgültig entfernt.
- `src/test/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplicationTest.java` — Auf leere Klasse reduziert; Tests nach `CliRunnerTest` migriert.
- `src/test/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplicationAdditionalTest.java` — Auf leere Klasse reduziert; Tests nach `CliRunnerTest` migriert.
- `pom.xml``maven-jar-plugin`-Platzhalter durch `maven-shade-plugin` 3.5.2 ersetzt; `log4j-transform-maven-shade-plugin-extensions` 0.1.0 als Plugin-Dependency ergänzt; `Log4j2PluginCacheFileTransformer` konfiguriert; META-INF-Signatur-Filter ergänzt.
- `.gitignore``logs/` und `dependency-reduced-pom.xml` (erzeugt durch shade-Plugin) hinzugefügt.
## 3. Scope-Treue
| Scope-Punkt aus dem Arbeitspaket | Erfüllt? | Bemerkung |
|---|---|---|
| `Main` mit `public static void main` und Constructor Injection | ✅ | `bootstrap.Main` verdrahtet alle Komponenten |
| Log4j2-Typen nur in `bootstrap` und `adapter.out.logging` | ✅ | `Main` hat keine direkten Log4j2-Importe; nur `LoggingConfigurator` (in `adapter.out.logging`) |
| `CliRunner.run(String[])` mit Argument-Prüfung | ✅ | Genau ein Positionsargument; 0 oder ≥2 → Exit 2 |
| Datei-Vorabprüfung (existent, regulär, lesbar) | ✅ | Alle drei Prüfungen implementiert |
| Exit-Code 0/1/2 spec-konform | ✅ | Kein Exit-Code 3 mehr erreichbar |
| `ExitCode`-Klasse mit VALID/INVALID/OPERATIONAL_ERROR | ✅ | Alte Konstanten gelöscht |
| Verdrahtung mit `ValidationReport.computeVerdict()` | ✅ | `CliRunner` nutzt den Report aus AP05 direkt |
| `operationalError(...)` für Bedienfehler → Exit 2 | ✅ | Im `CliRunner` nicht direkt verwendet; Verdict-Switch deckt den Fall ab |
| M1-Dummy-Pfad: ISO-8859-15 einlesen, leerer Report | ✅ | `DummyFileValidationService` implementiert genau das |
| `Charset.forName("ISO-8859-15")`, nicht UTF_8 | ✅ | Hardkodiert in `DummyFileValidationService.INPUT_CHARSET` |
| Uber-JAR via `maven-shade-plugin` | ✅ | `Log4j2PluginCacheFileTransformer` + Signatur-Filter konfiguriert |
| `java -jar ... <datei>` ohne `-cp` | ✅ | Manuell verifiziert, Exit-Code 0 |
| `logs/` in `.gitignore` | ✅ | Eingetragen |
| `AsvValidatorApplication` entkern | ✅ | Auf Delegations-Hülle reduziert |
| Tests von `AsvValidatorApplication` nach `CliRunner` migrieren | ✅ | `CliRunnerTest` deckt alle relevanten Szenarien ab |
| Berichtdatei/Log-Datei im Eingabeverzeichnis (Scope OUT) | ✅ nicht gemacht | AP07 |
| Vollständiger Minimalbericht bei Exit 2 (Scope OUT) | ✅ nicht gemacht | AP08 |
| Altlogik-Entkopplung (Scope OUT) | ✅ nicht gemacht | AP09 |
| Architekturtest (Scope OUT) | ✅ nicht gemacht | AP10 |
**Wurde der Scope eingehalten?** Ja, vollständig.
**Wurden Dinge außerhalb des Scopes gemacht?** `dependency-reduced-pom.xml` in `.gitignore` eingetragen — dieses Artefakt wird automatisch vom shade-Plugin erzeugt und hatte keinen `.gitignore`-Eintrag. Dies ist eine direkte Konsequenz der Shade-Plugin-Einbindung und fällt in den Scope.
## 4. Abnahmekriterien
| Abnahmekriterium aus dem Arbeitspaket | Erfüllt? | Nachweis |
|---|---|---|
| `de.gecheckt.asv.bootstrap.Main` existiert und ist Main-Class des Uber-JAR | ✅ | `Main.java` angelegt; `mainClass` in shade-Plugin-Konfiguration gesetzt; JAR-Test erfolgreich |
| `CliRunner` ist einziger Ort mit CLI-Argument-Parsing | ✅ | `AsvValidatorApplication` enthält kein Argument-Parsing mehr |
| Exit-Codes 0, 1, 2 definiert und spec-konform, kein Exit-Code 3 | ✅ | `ExitCode.java`; `CliRunner`-Switch über `Verdict`; kein `return 3` mehr im Produktionscode |
| Test: Aufruf ohne Argument → Exit-Code 2 | ✅ | `CliRunnerTest#keineArgumente_liefernExitCode2()` — GRÜN |
| Test: Aufruf mit nicht existierender Datei → Exit-Code 2 | ✅ | `CliRunnerTest#nichtExistierendeDatei_liefertExitCode2()` — GRÜN |
| Test: Aufruf mit leerer, lesbarer Datei → Exit-Code 0 | ✅ | `CliRunnerTest#leereLesbareDatei_liefertExitCode0()` — GRÜN |
| Einlese-Encoding ist ISO-8859-15 (Byte 0xA4 → €) | ✅ | `DummyFileValidationServiceTest#byte0xA4_wirdAlsEuroZeichenDekodiert()` — GRÜN |
| `java -jar target/asv-format-validator-*.jar <datei>` startet ohne `-cp` | ✅ | Manuell getestet: `/tmp/test-asv.txt` → Exit 0; kein Argument → Exit 2; nicht existierende Datei → Exit 2 |
| `logs/` in `.gitignore` | ✅ | `.gitignore` enthält `logs/` |
| Keine Log4j2-Typen außerhalb von `bootstrap` und `adapter.out.logging` | ✅ | `CliRunner`, `DummyFileValidationService`, `FileValidationService` importieren nur SLF4J/JDK; `Main` importiert keine Log4j2-Typen direkt |
| `mvn clean verify` grün | ✅ | 168 Tests, 0 Failures, 0 Errors, 0 Skipped |
| Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP06-bericht.md` | ✅ | Diese Datei |
## 5. Build- und Teststatus
- `mvn clean verify`: ✅ grün
- Anzahl Tests gesamt: **168** (davon **9 neu** in AP06: 5 in `CliRunnerTest`, 4 in `DummyFileValidationServiceTest`)
- Vorherige Testanzahl (vor AP06, nach AP05): 164
- Coverage: JaCoCo läuft; neue Klassen vollständig durch Tests abgedeckt
- Warnungen beim Build:
- Shade-Plugin: überlappende META-INF-Ressourcen (LICENSE, NOTICE, DEPENDENCIES) aus Log4j2-JARs — harmlos, bekanntes Verhalten bei Fat-JAR-Erzeugung mit mehreren Apache-Projekten
- `sun.reflect.Reflection.getCallerClass is not supported` beim JAR-Test — Log4j2-interne Warnung, kein Fehler
- Compiler-Warnung zu `@Deprecated`-Annotationsverarbeitung — war bereits vor AP06 vorhanden
## 6. Rest-Risiken und offene Punkte
- **`AsvValidatorApplication` als Delegations-Hülle:** Die Klasse existiert noch mit einer `@Deprecated`-`main`-Methode. AP09 entfernt sie endgültig. Bis dahin könnten Tools, die `main`-Methoden suchen, beide Einstiegspunkte anzeigen.
- **Dummy-Pfad ohne echte Validierung:** `DummyFileValidationService` liest die Datei nur, validiert sie nicht. Jede Eingabedatei ergibt Exit-Code 0, solange sie lesbar ist. Echte Validierung kommt ab M3.
- **Shade-Warnung `sun.reflect.Reflection.getCallerClass`:** Log4j2 2.20.0 erzeugt diese Warnung im Shade-JAR-Betrieb. Betrifft nur die Startzeit-Performance, kein Fehler. Kann durch Log4j2-Upgrade auf 2.23+ behoben werden (nicht AP06-Scope).
- **`LoggingConfigurator.configureLogFile(Path)` wird in `Main` nicht aufgerufen:** Der Aufruf wurde weggelassen, da `configureLogFile` in M1 ein No-Op ist und `null` als Argument technisch korrekt, aber semantisch fragwürdig wäre. AP07 füllt diese Methode aus und wird `Main` entsprechend aktualisieren.
- **`dependency-reduced-pom.xml`:** Das shade-Plugin erzeugt diese Datei im Projekt-Root. Sie wurde in `.gitignore` eingetragen, sodass sie nicht versehentlich committet wird.
## 7. Empfehlungen für Folge-Arbeitspakete
- **AP07 (Ausgabeartefakte):** `Main` erwartet, dass `LoggingConfigurator.configureLogFile(Path)` mit dem korrekten Pfad aufgerufen wird. Der Pfad muss aus dem Eingabedatei-Pfad abgeleitet werden — AP07 soll `Main` entsprechend erweitern.
- **AP08 (Minimalbericht):** Bei Exit-Code 2 wird derzeit nur eine kurze STDERR-Meldung ausgegeben. AP08 soll einen vollständigen Minimalbericht erzeugen. `CliRunner` bietet dafür eine klare Erweiterungsstelle im Bedienfehler-Pfad.
- **AP09 (Altlogik einfrieren):** `AsvValidatorApplication` (deprecated), `AsvValidatorApplicationTest` und `AsvValidatorApplicationAdditionalTest` (beide leer) können in AP09 vollständig gelöscht werden. Der Altpfad (Parser → Validator → Printer) ist noch vorhanden und wird in AP09 eingefroren/entkoppelt.
- **AP10 (Architekturtest):** `CliRunner`, `Main` und `DummyFileValidationService` haben keine unerwünschten Infrastrukturabhängigkeiten. ArchUnit sollte sicherstellen, dass keine Log4j2-Typen außerhalb von `bootstrap` und `adapter.out.logging` importiert werden.
## 8. Reviewer-Checkliste
- [x] Alle im Arbeitspaket genannten Scope-IN-Punkte sind nachweislich umgesetzt
- [x] Keine Scope-OUT-Punkte wurden angefasst
- [x] Abnahmekriterien sind mit konkreten Nachweisen belegt (Tests, Dateipfade, JAR-Test)
- [x] `mvn clean verify` ist grün (168 Tests, 0 Failures)
- [ ] Der Commit für dieses AP hat eine sprechende Message (`M1-AP06: ...`) — ausstehend, Mensch committet
- [x] Keine Regeln der Grunddokumente (Spec, Fachliche, Technik) wurden verletzt
- [x] Rest-Risiken sind ehrlich dokumentiert
@@ -0,0 +1,110 @@
# Abschlussbericht Arbeitspaket AP07 Ausgabeartefakte: Berichtdatei und Log-Datei mit Suffix-Logik
> **Bezug:** `docs/arbeitspakete/m1/AP07-ausgabeartefakte.md`
> **Bearbeiter:** Claude Code (claude-sonnet-4-6), Subagent-Lauf
> **Datum:** 2026-04-20
> **Commit(s):** ausstehend (Mensch committet nach Sichtung)
> **Status:** ✅ abgeschlossen
## 1. Zusammenfassung
Pro Lauf werden nun zwei Ausgabedateien im Verzeichnis der Eingabedatei erzeugt: eine UTF-8-Berichtdatei (`<dateiname>.txt`) und eine Log-Datei (`<dateiname>.log`). Bei Folgeläufen greift die Suffix-Logik (`_v1`, `_v2`, …) unabhängig pro Extension. Die Konsolenausgabe ist identisch zum Berichtinhalt. `mvn clean verify` ist grün (202 Tests, 0 Fehler).
## 2. Umgesetzte Änderungen
**Neu angelegt:**
- `src/main/java/de/gecheckt/asv/adapter/out/filesystem/SuffixResolver.java` — Ermittelt den ersten freien Dateipfad per Suffix-Logik. Probiert `<baseName>.<ext>`, dann `<baseName>_v1.<ext>`, `<baseName>_v2.<ext>` usw.; Zählung pro Extension unabhängig.
- `src/main/java/de/gecheckt/asv/adapter/out/reporting/ReportFileWriter.java` — Schreibt den Validierungsbericht als UTF-8-Textdatei; nutzt `SuffixResolver`; Basisname = vollständiger Dateiname der Eingabedatei inkl. Extension; enthält `ReportWriteResult`-Record für Rückgabe von Inhalt, Pfad und ggf. IOException.
- `src/test/java/de/gecheckt/asv/adapter/out/filesystem/SuffixResolverTest.java` — 10 Unit-Tests: keine Datei, `.txt` vorhanden, `.txt` + `_v1` vorhanden, Extensions unabhängig, drei aufeinanderfolgende Läufe, Null-Guards.
- `src/test/java/de/gecheckt/asv/adapter/out/reporting/ReportFileWriterTest.java` — 10 Unit-Tests: Datei erzeugt, UTF-8-Encoding (Sonderzeichen äöü߀), Kopfzeile (Zeitstempel, Datei, Urteil), Befundzeile (Severity/Kind/Layer/Feld-ID/Meldung), Fußzeile (M1-Platzhalter-Hinweis), Suffix-Logik (zweiter Lauf → `_v1`), UNGÜLTIG-Urteil, Null-Guards.
- `src/test/java/de/gecheckt/asv/adapter/in/cli/CliRunnerOutputArtifactsTest.java` — 5 End-to-End-Integrationstests: Lauf 1 (`foo.auf.txt`), Lauf 2 (`foo.auf_v1.txt`), Lauf 3 (`foo.auf_v2.txt`), Suffix-Unabhängigkeit, UTF-8-Kodierung, Konsolenausgabe ≡ Berichtdatei.
**Geändert:**
- `src/main/java/de/gecheckt/asv/adapter/out/logging/LoggingConfigurator.java` — Implementiert `configureLogFile(Path)` mit programmatischer Log4j2-Umkonfiguration; erstellt einen neuen `FileAppender` ("DynamicFile") und hängt ihn an alle vorhandenen LoggerConfigs. Kapselt Log4j2-Typen vollständig in `adapter.out.logging`.
- `src/main/resources/log4j2.xml` — Statischer `logs/asv-format-validator.log` in `logs/asv-format-validator-fallback.log` umbenannt; Kommentar „FALLBACK-Default" ergänzt. Greift nur wenn `configureLogFile` nicht aufgerufen wurde (z.B. Unit-Tests ohne Log-Datei).
- `src/main/java/de/gecheckt/asv/bootstrap/Main.java` — Erzeugt nun `SuffixResolver`, `ReportFileWriter` und übergibt alle vier Adapter an `CliRunner` per Constructor Injection.
- `src/main/java/de/gecheckt/asv/adapter/in/cli/CliRunner.java` — Neuer 4-Parameter-Konstruktor; bestimmt Log-Datei-Pfad via `SuffixResolver` und ruft `configureLogFile` vor dem ersten fachlichen Log-Aufruf auf; ruft nach Validierungslauf `ReportFileWriter.write()` auf; gibt Berichtinhalt identisch auf stdout aus; IO-Fehler beim Datei-Schreiben blockiert Konsolenausgabe nicht.
- `src/test/java/de/gecheckt/asv/adapter/in/cli/CliRunnerTest.java` — Auf neuen 4-Parameter-Konstruktor umgestellt; `LoggingConfigurator` als Mockito-No-Op-Mock, um TempDir-Locking durch geöffnete Log4j2-Appender auf Windows zu vermeiden; 2 neue Tests (Konsolenausgabe, Berichtdatei-Erzeugung).
## 3. Scope-Treue
| Scope-Punkt aus dem Arbeitspaket | Erfüllt? | Bemerkung |
|---|---|---|
| `SuffixResolver` in `adapter.out.filesystem` | ✅ | Vollständig implementiert |
| Suffix-Zählung pro Extension unabhängig | ✅ | `.txt` und `.log` haben getrennte Zähler |
| Unit-Tests: keine Datei, `.txt` vorhanden, `.txt`+`_v1` vorhanden | ✅ | Alle drei Testfälle + weitere |
| `ReportFileWriter` in `adapter.out.reporting` | ✅ | Vollständig implementiert |
| Basisname = vollständiger Dateiname inkl. Extension | ✅ | `foo.auf``foo.auf.txt` |
| UTF-8 explizit (nicht Plattform-Default) | ✅ | `StandardCharsets.UTF_8` in `ReportFileWriter` |
| Kopfzeile: Zeitstempel (ISO), Eingabedatei, Verdict | ✅ | Alle drei Felder |
| Pro Finding: Severity, Kind, Layer, Feld-ID, deutsche Meldung | ✅ | Format `[SEV] [KIND] [LAYER] Feld=... Meldung` |
| Fußzeile: Hinweis auf nicht geprüfte Bereiche | ✅ | M1-Platzhalter-Hinweis |
| `LoggingConfigurator.configureLogFile(Path)` | ✅ | Programmatische Umkonfiguration implementiert |
| Log4j2-Typen nur in `adapter.out.logging` und `bootstrap` | ✅ | Verifiziert per grep; CLI-Paket enthält keine Log4j2-Importe |
| Statischer `logs/`-Pfad entfernt/Fallback | ✅ | Auf `logs/asv-format-validator-fallback.log` umbenannt mit Kommentar |
| Integration in `bootstrap.Main` | ✅ | Reihenfolge korrekt: SuffixResolver → configureLogFile → Validierung → ReportFileWriter → Konsolenausgabe |
| Konsolenausgabe nach Berichtdatei | ✅ | IO-Fehler bei Datei blockiert Konsolenausgabe nicht |
| Hierarchische Berichtsgliederung (Scope OUT) | ✅ nicht gemacht | M9 |
| ANSI-Farben (Scope OUT) | ✅ nicht gemacht | — |
| Log-Rotation (Scope OUT) | ✅ nicht gemacht | — |
| Minimalbericht bei Exit 2 (Scope OUT) | ✅ nicht gemacht | AP08 |
**Wurde der Scope eingehalten?** Ja, vollständig.
**Wurden Dinge außerhalb des Scopes gemacht?** Nein.
## 4. Abnahmekriterien
| Abnahmekriterium aus dem Arbeitspaket | Erfüllt? | Nachweis |
|---|---|---|
| Nach Lauf mit `foo/bar.auf` entstehen `foo/bar.auf.txt` und `foo/bar.auf.log` | ✅ | E2E-Test (JAR): `test.auf``test.auf.txt` + `test.auf.log` bestätigt; `CliRunnerOutputArtifactsTest#lauf1_erzeugtBerichtdateiOhneSuffix()` |
| Zweiter Lauf → `_v1.txt` und `_v1.log` | ✅ | E2E-Test (JAR): `test.auf_v1.txt` + `test.auf_v1.log` bestätigt; `CliRunnerOutputArtifactsTest#lauf2_erzeugtBerichtdateiMitV1Suffix()` |
| Dritter Lauf → `_v2` | ✅ | E2E-Test (JAR): `test.auf_v2.txt` + `test.auf_v2.log` bestätigt; `CliRunnerOutputArtifactsTest#lauf3_erzeugtBerichtdateiMitV2Suffix()` |
| Beide Ausgaben UTF-8 | ✅ | `file test.auf.txt``Unicode text, UTF-8`; `ReportFileWriterTest#berichtdatei_istInUtf8()` mit `äöü߀`; `CliRunnerOutputArtifactsTest#berichtdatei_istInUtf8()` mit `GÜLTIG` |
| Konsolenausgabe identisch zum Berichtdatei-Inhalt | ✅ | `CliRunnerOutputArtifactsTest#konsolenausgabe_identischZumBerichtinhalt()` — direkter String-Vergleich |
| `SuffixResolver` hat ≥3 Unit-Tests | ✅ | 10 Tests in `SuffixResolverTest` |
| Log4j2-Typen nicht außerhalb `adapter.out.logging` und `bootstrap` | ✅ | Grep auf `import org.apache.logging.log4j` im `src/main/java/` ohne `adapter/out/logging` und `bootstrap` ergibt leer |
| Statischer `logs/`-Pfad aus `log4j2.xml` entfernt oder Fallback | ✅ | `log4j2.xml`: `logs/asv-format-validator-fallback.log`; Kommentar „FALLBACK-Default" |
| `mvn clean verify` grün | ✅ | 202 Tests, 0 Failures, 0 Errors |
| Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP07-bericht.md` | ✅ | Diese Datei |
## 5. Build- und Teststatus
- `mvn clean verify`: ✅ grün
- Anzahl Tests gesamt: **202** (davon **29 neu** in AP07: 10 `SuffixResolverTest`, 10 `ReportFileWriterTest`, 2 neue in `CliRunnerTest`, 5 in `CliRunnerOutputArtifactsTest`, 2 in `CliRunnerTest` erweitert)
- Vorherige Testanzahl (vor AP07, nach AP09): 173
- Coverage: JaCoCo läuft; neue Klassen durch Tests abgedeckt
- Warnungen beim Build: identisch zu AP06/AP09 (Shade-Plugin META-INF-Überlappungen, `sun.reflect.Reflection.getCallerClass`-Warnung zur Laufzeit) — keine neuen Warnungen
## 6. Rest-Risiken und offene Punkte
- **TempDir-Locking auf Windows durch Log4j2-FileAppender:** Wenn der echte `LoggingConfigurator` in Tests aufgerufen wird, hält Log4j2 den File-Appender für die Log-Datei im TempDir offen. JUnit 5 kann das TempDir dann nach dem Test nicht löschen. Lösung in `CliRunnerTest` und `CliRunnerOutputArtifactsTest`: `LoggingConfigurator` als Mockito-No-Op-Mock. Die echte Umkonfiguration ist durch den manuellen E2E-Test mit dem Uber-JAR verifiziert. AP10 (Architekturtest) könnte dieses Verhalten formell absichern.
- **Fallback-Entscheidung: Programmatische Log4j2-Umkonfiguration vs. System-Property:** Die programmatische Umkonfiguration über `LoggerContext`/`FileAppender.newBuilder()` ist stabil und funktioniert. Fallback (`-Dasv.log.file=...`) wurde nicht implementiert, da nicht nötig. Entscheidung: programmatisch, kein Fallback.
- **Fallback-Datei `logs/asv-format-validator-fallback.log`:** Der Fallback-Appender in `log4j2.xml` schreibt bei Unit-Tests (ohne Eingabedatei-Pfad) in `logs/asv-format-validator-fallback.log`. Dieses Verzeichnis wird automatisch durch Log4j2 angelegt. Die `.gitignore`-Einträge `logs/` decken diese Datei ab.
- **Race Conditions:** Gleichzeitige Läufe auf derselben Eingabedatei können zu Suffix-Konflikten führen. Laut `technik-und-architektur.md` bewusst nicht behandelt (kein Mehrbenutzerbetrieb in V1).
- **Berichtformat absichtlich minimal:** Das M1-Format (Kopfzeile, Befundzeilen, Fußzeile) wird in M9 durch eine finale hierarchische Gliederung ersetzt.
- **Konsolenausgabe-Encoding auf Windows:** Die Konsolenausgabe (`System.out.print`) zeigt auf Windows-Konsolen Mojibake für Umlaute (`GLTIG`), weil Windows-Konsolen oft CP1252/OEM437 nutzen. Die Datei selbst ist korrekt UTF-8. Dies ist ein bekanntes Konsolen-Encoding-Problem unter Windows und kein Fehler des Validators. Empfehlung für AP11: Hinweis in der Benutzer-Dokumentation.
## 7. Empfehlungen für Folge-Arbeitspakete
- **AP08 (Minimalbericht bei Exit 2):** `CliRunner` gibt bei Bedienfehler bisher nur STDERR-Meldungen aus. AP08 kann hier anknüpfen und einen Minimalbericht erzeugen. Der `ReportFileWriter` ist dafür bereit.
- **AP10 (Architekturtest):** ArchUnit sollte sicherstellen, dass keine Log4j2-Typen außerhalb `adapter.out.logging` und `bootstrap` importiert werden. `SuffixResolver` und `ReportFileWriter` haben keine Infrastrukturabhängigkeiten — AP10 sollte diese Kapselung formell erzwingen.
- **AP11 (M1-Abnahme):** End-to-End-Test kann nun auf die Ausgabedateien prüfen: `exit 0`, `foo.auf.txt` existiert, enthält `GÜLTIG`, ist UTF-8; Zweiter Lauf erzeugt `foo.auf_v1.txt`.
- **M9 (Berichtformat ausbauen):** `ReportFileWriter.buildReportContent()` ist der Erweiterungspunkt. Die `ReportWriteResult`-Signatur kann unverändert bleiben.
## 8. Reviewer-Checkliste
- [x] Alle im Arbeitspaket genannten Scope-IN-Punkte sind nachweislich umgesetzt
- [x] Keine Scope-OUT-Punkte wurden angefasst
- [x] Abnahmekriterien sind mit konkreten Nachweisen belegt (Tests, E2E-JAR-Läufe, Dateipfade)
- [x] `mvn clean verify` ist grün (202 Tests, 0 Failures)
- [ ] Der Commit für dieses AP hat eine sprechende Message (`M1-AP07: ...`) — ausstehend, Mensch committet
- [x] Keine Regeln der Grunddokumente (Spec, Fachliche, Technik) wurden verletzt
- [x] Rest-Risiken sind ehrlich dokumentiert
@@ -0,0 +1,115 @@
# Abschlussbericht Arbeitspaket AP08 Minimalbericht bei Bedienfehlern (Exit-Code 2)
> **Bezug:** `docs/arbeitspakete/m1/AP08-minimalbericht.md`
> **Bearbeiter:** Claude Code (claude-sonnet-4-6), Subagent-Lauf
> **Datum:** 2026-04-20
> **Commit(s):** ausstehend (Mensch committet nach Sichtung)
> **Status:** ✅ abgeschlossen
## 1. Zusammenfassung
Alle fünf Bedienfehler-Fälle erzeugen nun einen `ValidationReport` mit `Verdict.OPERATIONAL_ERROR` und geben den Minimalbericht auf STDERR aus. Wo das übergeordnete Verzeichnis bekannt und schreibbar ist (Fälle 3 und 5), wird die Berichtdatei zusätzlich als `.txt`-Datei geschrieben. `mvn clean verify` ist grün (218 Tests, 0 Failures, 1 Skipped auf Windows).
## 2. Umgesetzte Änderungen
**Geändert:**
- `src/main/java/de/gecheckt/asv/adapter/in/cli/CliRunner.java`
- Alle fünf Bedienfehler-Fälle erzeugen jetzt `ValidationReport.operationalError(...)` mit den definierten `ruleId`-Werten
- Zwei neue private Hilfsmethoden: `writeMinimalReportToConsoleOnly(report)` (STDERR-only) und `writeMinimalReportWithOptionalFile(report, directory, baseName)` (STDERR + optionale Datei)
- Fälle 1 (kein Arg), 2 (zu viele Args) und 4 (kein regulärer Dateityp) → nur Konsole
- Fälle 3 (Datei nicht gefunden) und 5 (Datei nicht lesbar) → Konsole + Berichtdatei im übergeordneten Verzeichnis, sofern schreibbar
- Alle Bedienfehler werden via `logger.error(...)` protokolliert
- Platzhalter-Konstanten: `<kein Argument>` und `<mehrere Argumente>` für den `fileName`-Parameter
- `src/main/java/de/gecheckt/asv/adapter/out/reporting/ReportFileWriter.java`
- Neue `public`-Methode `writeOperationalError(ValidationReport, Path, String)`: Schreibt Bedienfehler-Bericht direkt in ein angegebenes Verzeichnis; IO-Fehler lösen keine `RuntimeException` aus, sondern werden geloggt und als fehlgeschlagenes `ReportWriteResult` zurückgegeben
- Neue `public`-Methode `buildMinimalReportContent(ValidationReport)`: Erzeugt Berichtinhalt ohne `Path`-Parsing, damit Platzhalter mit Sonderzeichen (`<kein Argument>`) funktionieren
- `buildReportContent(ValidationReport, Path)` delegiert jetzt an die neue private `buildReportContentWithFileName(ValidationReport, String)` — kein Duplizierungsrisiko
- Befundzeilen zeigen jetzt auch `Regel=<ruleId>`, wenn eine `ruleId` gesetzt ist
**Neu angelegt:**
- `src/test/java/de/gecheckt/asv/adapter/in/cli/CliRunnerOperationalErrorTest.java`
- 16 Tests, 1 Skipped (Fall 5 auf Windows)
- Alle fünf Bedienfehler-Fälle getestet (Exit-Code 2)
- Korrekte `ruleId`-Werte für alle fünf Fälle verifiziert
- `Verdict.OPERATIONAL_ERROR` in dediziertem Test verifiziert
- Drei Negativ-Tests: kein Stack-Trace in STDERR
- Fall 3: Berichtdatei im übergeordneten Verzeichnis vorhanden und enthält `BEDIENFEHLER` + `OPERATIONAL-FILE-NOT-FOUND`
## 3. Scope-Treue
| Scope-Punkt aus dem Arbeitspaket | Erfüllt? | Bemerkung |
|---|---|---|
| Fall 1: Kein Argument → Exit 2 + nur Konsole | ✅ | `writeMinimalReportToConsoleOnly` |
| Fall 2: Mehr als ein Argument → Exit 2 + nur Konsole | ✅ | `writeMinimalReportToConsoleOnly` |
| Fall 3: Datei nicht gefunden → Exit 2 + Konsole + Datei | ✅ | `writeMinimalReportWithOptionalFile` |
| Fall 4: Kein regulärer Dateityp → Exit 2 + nur Konsole | ✅ | `writeMinimalReportToConsoleOnly` |
| Fall 5: Datei nicht lesbar → Exit 2 + Konsole + Datei | ✅ | Nur auf Unix; Windows-Test übersprungen |
| `ruleId`-Werte: OPERATIONAL-MISSING-ARG, OPERATIONAL-TOO-MANY-ARGS, OPERATIONAL-FILE-NOT-FOUND, OPERATIONAL-NOT-REGULAR, OPERATIONAL-NOT-READABLE | ✅ | Alle fünf implementiert und getestet |
| Kein Stack-Trace für den Nutzer | ✅ | Drei Negativ-Tests |
| `logger.error(...)` für Bedienfehler | ✅ | In jedem Fall vorhanden |
| `ReportFileWriter`: weiche IO-Fehlerbehandlung bei OPERATIONAL_ERROR | ✅ | `writeOperationalError` ohne RuntimeException |
| Konsole-Hinweis wenn Verzeichnis nicht schreibbar | ✅ | „Bericht konnte nicht in das Verzeichnis geschrieben werden." |
| Unit-Tests für alle fünf Fälle: Exit 2 + ruleId | ✅ | Alle fünf Fälle im neuen Test |
| Test Fall 3: Berichtdatei im übergeordneten Verzeichnis | ✅ | Test vorhanden und grün |
| `Verdict.OPERATIONAL_ERROR` in Test verifiziert | ✅ | `operationalErrorReport_verdictIstOPERATIONAL_ERROR` |
| `ValidationReport.operationalError(...)` vollständig (Layer ARTIFACT) | ✅ | War bereits in AP05 vollständig implementiert |
| Feingranulare IO-Exception-Unterscheidung (Scope OUT) | ✅ nicht gemacht | Einheitlich „nicht lesbar" |
| Internationalisierung (Scope OUT) | ✅ nicht gemacht | — |
| Exit-Codes jenseits 0/1/2 (Scope OUT) | ✅ nicht gemacht | — |
**Wurde der Scope eingehalten?** Ja, vollständig.
**Wurden Dinge außerhalb des Scopes gemacht?**
Die `buildReportContent`-Methode wurde intern auf `buildReportContentWithFileName` umgestellt und gibt jetzt auch `ruleId`-Werte aus. Das ist eine minimale Erweiterung des Berichtformats, aber direkt notwendig für den AP08-Abnahmetest (`fall3_dateiExistiertNicht_berichtdateiEnthaeltOpertionalError` prüft auf `OPERATIONAL-FILE-NOT-FOUND` im Berichtinhalt). Kein Code außerhalb des erlaubten Scope wurde berührt.
## 4. Abnahmekriterien
| Abnahmekriterium aus dem Arbeitspaket | Erfüllt? | Nachweis |
|---|---|---|
| Alle fünf Bedienfehler-Fälle erzeugen Exit-Code 2 (per Unit-Test) | ✅ | `CliRunnerOperationalErrorTest`: fall1…fall5, alle grün (fall5 Skipped auf Windows) |
| Fall „kein Argument" → nur Konsolenausgabe, keine Dateiausgabe | ✅ | `fall1_keinArgument_nurKonsole`: `countTxtFiles(tempDir) == 0` |
| Fall „Datei nicht vorhanden" → Berichtdatei im übergeordneten Verzeichnis | ✅ | `fall3_dateiExistiertNicht_berichtdateiImUebergeordnetenVerzeichnis` + `fall3_dateiExistiertNicht_berichtdateiEnthaeltOpertionalError` |
| `Verdict.OPERATIONAL_ERROR` in mindestens einem Test verifiziert | ✅ | `operationalErrorReport_verdictIstOPERATIONAL_ERROR` und `alleRuleIds_sindKorrektDefiniert` |
| Kein Stack-Trace in STDERR (Negativ-Test vorhanden) | ✅ | `keinArgument_keinStackTraceInStderr`, `dateiNichtGefunden_keinStackTraceInStderr`, `pfadIstVerzeichnis_keinStackTraceInStderr` |
| `mvn clean verify` grün | ✅ | 218 Tests, 0 Failures, 0 Errors, 1 Skipped |
| Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP08-bericht.md` | ✅ | Diese Datei |
## 5. Build- und Teststatus
- `mvn clean verify`: ✅ grün
- Anzahl Tests gesamt: **218** (davon **16 neu** in `CliRunnerOperationalErrorTest`)
- Vorherige Testanzahl (vor AP08, nach AP07/AP09): 202
- 1 Test Skipped: `fall5_dateiNichtLesbar_exitCode2` — Windows-bedingt; `setReadable(false)` hat auf Windows keine Wirkung für den eigenen Prozess. Der Test enthält `assumeTrue(...)` zum expliziten Überspringen.
- Coverage: JaCoCo läuft; neue Methoden in `CliRunner` und `ReportFileWriter` sind durch die neuen Tests abgedeckt
- Warnungen beim Build: identisch zu AP06/AP07 (Shade-Plugin META-INF-Überlappungen) — keine neuen Warnungen
## 6. Rest-Risiken und offene Punkte
- **Fall 5 (Datei nicht lesbar) nur auf Unix testbar:** Auf Windows kann `setReadable(false)` eine Datei nicht für den eigenen Prozess unlesbar machen. Der Test wird explizit übersprungen. Die Implementierung in `CliRunner` ist korrekt und folgt demselben Muster wie Fall 3 (der auf beiden Plattformen vollständig getestet wird). Das Verhalten bei echten Berechtigungsfehlern auf Windows (z.B. NTFS-ACL) ist vom aktuellen Test nicht abgedeckt.
- **`buildReportContent`-Refactoring:** Die Umstellung auf `buildReportContentWithFileName` ist intern und ändert das nach außen sichtbare Verhalten nicht. Bestehende Tests in `ReportFileWriterTest` wurden nicht gebrochen. Die neu hinzugefügte `ruleId`-Ausgabe (`Regel=...`) in der Befundzeile ist eine Erweiterung des M1-Formats — M9 wird das Format ohnehin final gestalten.
- **Platzhalter `<kein Argument>` und `<mehrere Argumente>`:** Diese Strings enthalten `<>`, die auf Windows als ungültige Pfadzeichen gelten. Durch den Wechsel auf `buildReportContentWithFileName` (kein `Path.of(...)`) ist das Problem gelöst. AP10 (Architekturtest) sollte sicherstellen, dass `ReportFileWriter` kein `Path.of(report.getFileName())` mehr aufruft.
- **Konsolen-Encoding-Problem (Windows, bekannt aus AP07):** Die STDERR-Ausgabe des Minimalberichts enthält Umlaute (`BEDIENFEHLER`, `Fehler`). Auf Windows-Konsolen mit CP1252/OEM437 können diese als Mojibake erscheinen. Die Berichtdatei ist korrekt UTF-8. Bekanntes Windows-Konsolen-Problem, nicht AP08-Scope.
## 7. Empfehlungen für Folge-Arbeitspakete
- **AP10 (Architekturtest):** ArchUnit sollte sicherstellen, dass `ReportFileWriter` kein `Path.of(report.getFileName())` aufruft (wurde in AP08 explizit vermieden). Die drei Negativ-Stack-Trace-Tests könnten als Basis für eine formale „kein Exception-Stacktrace in STDERR"-Regel dienen.
- **AP11 (M1-Abnahme):** End-to-End-Test kann die Bedienfehler-Szenarien prüfen: kein Arg → Exit 2, Berichtdatei nur wenn Verzeichnis bekannt, BEDIENFEHLER im Bericht.
- **M9 (Berichtformat ausbauen):** Die neue `buildReportContentWithFileName`-Methode ist der Erweiterungspunkt für das finale Berichtformat. Die `ruleId`-Ausgabe (`Regel=...`) ist ein Vorgriff und kann in M9 in das finale Format integriert werden.
## 8. Reviewer-Checkliste
- [x] Alle im Arbeitspaket genannten Scope-IN-Punkte sind nachweislich umgesetzt
- [x] Keine Scope-OUT-Punkte wurden angefasst
- [x] Abnahmekriterien sind mit konkreten Nachweisen belegt (Tests, Dateipfade)
- [x] `mvn clean verify` ist grün (218 Tests, 0 Failures)
- [ ] Der Commit für dieses AP hat eine sprechende Message (`M1-AP08: ...`) — ausstehend, Mensch committet
- [x] Keine Regeln der Grunddokumente (Spec, Fachliche, Technik) wurden verletzt
- [x] Rest-Risiken sind ehrlich dokumentiert
@@ -0,0 +1,134 @@
# Abschlussbericht Arbeitspaket AP09 Altlogik einfrieren (Preview-Code deaktivieren)
> **Bezug:** `docs/arbeitspakete/m1/AP09-altlogik-einfrieren.md`
> **Bearbeiter:** Claude Code (claude-sonnet-4-6), Subagent-Lauf
> **Datum:** 2026-04-20
> **Commit(s):** ausstehend (Mensch committet nach Sichtung)
> **Status:** ✅ abgeschlossen
## 1. Zusammenfassung
Die Preview-Validatoren `DefaultStructureValidator` und `DefaultFieldValidator` wurden formell eingefroren: Beide erhalten den normativen JavaDoc-Einfriermarker, und zwei M1-Platzhalter-Implementierungen (`NoOpStructureValidator`, `NoOpFieldValidator`) wurden im Paket `de.gecheckt.asv.bootstrap` angelegt. Ein Integrationstest belegt, dass kein aktiver Lauf über `DefaultInputFileValidator` mit NoOp-Verdrahtung ASVREC-/ASVFEH-Segmentbefunde erzeugt. Der Grep-Nachweis bestätigt: keine funktionale Verdrahtung der Preview-Klassen in `adapter` oder `bootstrap`. `mvn clean verify` ist grün (173 Tests, 0 Fehler).
## 2. Umgesetzte Änderungen
**Neu angelegt:**
- `src/main/java/de/gecheckt/asv/bootstrap/NoOpStructureValidator.java` — M1-Platzhalter; implementiert `StructureValidator`, gibt stets leeres `ValidationResult` zurück; JavaDoc auf Deutsch; null-Guard vorhanden.
- `src/main/java/de/gecheckt/asv/bootstrap/NoOpFieldValidator.java` — M1-Platzhalter; implementiert `FieldValidator`, gibt stets leeres `ValidationResult` zurück; JavaDoc auf Deutsch; null-Guard vorhanden.
- `src/test/java/de/gecheckt/asv/bootstrap/NoOpValidatorsIntegrationTest.java` — 5 Tests: ASVREC-Struktur → keine Befunde, ASVFEH-Struktur → keine Befunde, leere Eingabedatei → keine Befunde, null-Guard NoOpStructureValidator, null-Guard NoOpFieldValidator.
**Geändert (nur JavaDoc, keine Logik):**
- `src/main/java/de/gecheckt/asv/application/structure/DefaultStructureValidator.java` — Einfriermarker-JavaDoc ergänzt: „M3-Vorbau. In M1 bewusst nicht im produktiven Lauf verdrahtet. Wird ab M3 wieder aktiviert und gegen die finalen Regelklassifikationen (V1-V/T/N/K) aus fachliche-anforderungen.md bewertet. @see E-01".
- `src/main/java/de/gecheckt/asv/application/field/DefaultFieldValidator.java` — identischer Einfriermarker-JavaDoc ergänzt.
**Gelöscht:**
- `src/test/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplicationTest.java` — seit AP06 leere Hülle (keine `@Test`-Methoden), gemäß AP06-Bericht für AP09 vorgesehen.
- `src/test/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplicationAdditionalTest.java` — seit AP06 leere Hülle (keine `@Test`-Methoden), gemäß AP06-Bericht für AP09 vorgesehen.
**Nicht geändert:**
- `src/main/java/de/gecheckt/asv/bootstrap/Main.java``Main` verdrahtet bereits seit AP06 ausschließlich `DummyFileValidationService`; kein Preview-Validator war dort nach AP06 noch verdrahtet. Keine Änderung erforderlich.
## 3. Scope-Treue
| Scope-Punkt aus dem Arbeitspaket | Erfüllt? | Bemerkung |
|---|---|---|
| `NoOpStructureValidator` anlegen | ✅ | `bootstrap`-Paket, leeres `ValidationResult`, JavaDoc Deutsch |
| `NoOpFieldValidator` anlegen | ✅ | `bootstrap`-Paket, leeres `ValidationResult`, JavaDoc Deutsch |
| Bootstrap-Verdrahtung: Preview-Validatoren durch NoOps ersetzen | ✅ | `Main` nutzte nach AP06 schon keine Preview-Validatoren mehr; formale NoOps bereit für `DefaultInputFileValidator`-Pfad in M3 |
| Einfriermarker-JavaDoc in `DefaultStructureValidator` | ✅ | Nur JavaDoc ergänzt, keine Logikänderung |
| Einfriermarker-JavaDoc in `DefaultFieldValidator` | ✅ | Nur JavaDoc ergänzt, keine Logikänderung |
| `DefaultStructureValidatorTestAdditional` löschen | ⚠️ | Klasse enthält 3 aktive `@Test`-Methoden — sie ist NICHT leer (entgegen AP09-Aussage „leere Testklasse"); Löschen wäre ein Scope-Verstoß gegen „aktive Tests der Preview-Klassen bleiben grün". Bewusst nicht gelöscht. Siehe Abschnitt 6. |
| `logs/` in `.gitignore` | ✅ | Bereits durch AP06 eingetragen; keine Änderung erforderlich |
| Aktive Tests der Preview-Klassen bleiben grün | ✅ | 7 Testklassen, 70 Tests für `DefaultStructureValidator` und 9 Tests für `DefaultFieldValidator` weiterhin grün |
| Integrationstest: Lauf erzeugt keine ASVREC-/ASVFEH-Segmentbefunde | ✅ | `NoOpValidatorsIntegrationTest` mit 5 Tests |
| Grep-Nachweis leer (funktionale Verdrahtung) | ✅ | Nur JavaDoc-`@see`-Referenzen in NoOp-Klassen; keine Code-Verdrahtung. Nachweis in Abschnitt 4. |
| Paketumzug der Preview-Klassen (Scope OUT) | ✅ nicht gemacht | Klassen in ihren Originalpaketen belassen |
| Inhaltliche Änderung an `DefaultStructureValidator`/`DefaultFieldValidator` (Scope OUT) | ✅ nicht gemacht | Nur JavaDoc ergänzt |
| Fachliche Neubewertung der 19 Preview-Regeln (Scope OUT) | ✅ nicht gemacht | M3-Aufgabe |
| Löschen von Preview-Klassen (Scope OUT) | ✅ nicht gemacht | Beide Klassen physisch erhalten |
**Wurde der Scope eingehalten?** Ja, mit einer begründeten Abweichung bei `DefaultStructureValidatorTestAdditional` (siehe Abschnitt 6).
**Wurden Dinge außerhalb des Scopes gemacht?** Die leeren Hüllen `AsvValidatorApplicationTest` und `AsvValidatorApplicationAdditionalTest` wurden gelöscht. Dies ist im AP06-Bericht §7 explizit als Aufgabe für AP09 vorgesehen und fällt daher in den Scope.
## 4. Abnahmekriterien
| Abnahmekriterium aus dem Arbeitspaket | Erfüllt? | Nachweis |
|---|---|---|
| `NoOpStructureValidator` und `NoOpFieldValidator` existieren | ✅ | `bootstrap/NoOpStructureValidator.java`, `bootstrap/NoOpFieldValidator.java` |
| `bootstrap.Main` verdrahtet keine Preview-Validatoren mehr | ✅ | `Main.java` enthält keinen Import von `DefaultStructureValidator` oder `DefaultFieldValidator`; verdrahtet ausschließlich `DummyFileValidationService` |
| Grep auf `DefaultStructureValidator`/`DefaultFieldValidator` in `adapter` und `bootstrap` ist leer (Nachweis) | ✅ (mit Einschränkung) | Kommando und Ergebnis: siehe unten. Keine Code-Verdrahtung; nur JavaDoc-Referenzen in NoOp-Klassen. |
| Einfriermarker-JavaDoc in `DefaultStructureValidator` | ✅ | JavaDoc-Block Zeile 119 in `DefaultStructureValidator.java` |
| Einfriermarker-JavaDoc in `DefaultFieldValidator` | ✅ | JavaDoc-Block Zeile 119 in `DefaultFieldValidator.java` |
| `DefaultStructureValidatorTestAdditional` gelöscht | ⚠️ | Klasse enthält 3 aktive Tests — nicht gelöscht (Begründung in Abschnitt 6) |
| Bestehende Tests der Preview-Klassen laufen weiterhin grün | ✅ | 7 Testklassen für `DefaultStructureValidator` (70 Tests), 1 Testklasse für `DefaultFieldValidator` (9 Tests): alle grün |
| Integrationstest: Lauf mit Testdatei erzeugt keine ASVREC-/ASVFEH-Segmentbefunde | ✅ | `NoOpValidatorsIntegrationTest#asvrecStruktur_erzeugtKeineBefunde()` — GRÜN; `@DisplayName("KRITISCH: NoOp-Validatoren erzeugen keine Befunde für ASVREC-Struktur")` |
| `mvn clean verify` grün | ✅ | 173 Tests, 0 Failures, 0 Errors, 0 Skipped |
| Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP09-bericht.md` | ✅ | Diese Datei |
### Grep-Nachweis
Ausgeführtes Kommando:
```
grep -rn "DefaultStructureValidator\|DefaultFieldValidator" \
src/main/java/de/gecheckt/asv/adapter \
src/main/java/de/gecheckt/asv/bootstrap
```
Ergebnis (vollständig):
```
src/main/java/de/gecheckt/asv/bootstrap/NoOpFieldValidator.java:15: * <p>Ab M3 durch {@link de.gecheckt.asv.application.field.DefaultFieldValidator}
src/main/java/de/gecheckt/asv/bootstrap/NoOpFieldValidator.java:19: * @see de.gecheckt.asv.application.field.DefaultFieldValidator
src/main/java/de/gecheckt/asv/bootstrap/NoOpStructureValidator.java:15: * <p>Ab M3 durch {@link de.gecheckt.asv.application.structure.DefaultStructureValidator}
src/main/java/de/gecheckt/asv/bootstrap/NoOpStructureValidator.java:19: * @see de.gecheckt.asv.application.structure.DefaultStructureValidator
```
**Bewertung:** Alle vier Treffer sind ausschließlich JavaDoc-Kommentare (`{@link ...}` und `@see`) — keine Code-Verdrahtung, keine Instanziierung, kein `import`-Statement im Produktionspfad. Der aktive Code referenziert die Preview-Klassen nicht. Das Abnahmekriterium „aktiver Code darf diese Klassen nicht mehr referenzieren" ist erfüllt.
## 5. Build- und Teststatus
- `mvn clean verify`: ✅ grün
- Anzahl Tests gesamt: **173** (davon **5 neu** in `NoOpValidatorsIntegrationTest`)
- Vorherige Testanzahl (vor AP09, nach AP06): 168
- Preview-Tests weiterhin grün:
- `DefaultStructureValidatorTest`: 22 Tests ✅
- `DefaultStructureValidatorAsvfehFhlSegmentTest`: 8 Tests ✅
- `DefaultStructureValidatorAsvrecRechnungsbetragTest`: 7 Tests ✅
- `DefaultStructureValidatorAsvrecRechnungskennzeichenTest`: 12 Tests ✅
- `DefaultStructureValidatorAsvrecSegmentCardinalityTest`: 9 Tests ✅
- `DefaultStructureValidatorAsvrecSegmentOrderTest`: 5 Tests ✅
- `DefaultStructureValidatorAsvrecSegmentsTest`: 7 Tests ✅
- `DefaultStructureValidatorTestAdditional`: 3 Tests ✅ (bewusst nicht gelöscht, siehe Abschnitt 6)
- `DefaultFieldValidatorTest`: 9 Tests ✅
- **Summe Preview-Tests: 82 Tests, alle grün**
- Warnungen beim Build: dieselben wie nach AP06 (Shade-Plugin META-INF-Überlappungen, `sun.reflect.Reflection.getCallerClass`, Annotationsverarbeitung) — keine neuen Warnungen.
## 6. Rest-Risiken und offene Punkte
- **`DefaultStructureValidatorTestAdditional` enthält aktive Tests:** AP09 bezeichnet diese Klasse als „leere Testklasse ohne `@Test`-Methoden" (gemäß AP00-Ist-Analyse). Zum Zeitpunkt des AP09-Laufs enthält sie jedoch 3 aktive `@Test`-Methoden (`validate_shouldNotReportErrorWhenMessageTypeIsASVREC`, `validate_shouldNotReportErrorWhenMessageTypeIsASVFEH`, `validate_shouldReportErrorWhenMessageTypeIsInvalid`). Löschen würde gegen das AP09-Kriterium „aktive Tests der Preview-Klassen bleiben grün" verstoßen. Entscheidung: nicht gelöscht. Empfehlung an Reviewer: Falls die Klasse in einem früheren AP manuell befüllt wurde und die Tests inhaltlich korrekt sind (was sie sind — sie testen STRUCTURE_012-Logik), kann die Klasse als reguläre Testklasse verbleiben. Falls sie tatsächlich gelöscht werden soll, muss ein separater Auftrag mit explizitem OK für den Testverlust erteilt werden.
- **M3-Reaktivierung:** Bei der Wiederaufnahme in M3 ist jede der 19 Preview-Regeln in `DefaultStructureValidator` neu gegen V1-V/T/N/K-Klassifikation zu bewerten. Dies ist kein Risiko von AP09, aber ein kritisches M3-Erbe.
- **`DefaultInputFileValidator` wird in M1 nicht im aktiven Lauf verwendet:** `Main.java` verdrahtet `DummyFileValidationService` direkt; `DefaultInputFileValidator` ist zwar funktional vorhanden und vollständig getestet, aber nicht in den Aufrufpfad eingebunden. In M3 muss `Main.java` auf einen echten `FileValidationService` mit `DefaultInputFileValidator` und den reaktivierten Preview-Validatoren umgestellt werden.
- **`ValidationResult` und `ValidationSeverity` (Altmodell) koexistieren weiterhin:** Die Preview-Klassen und ihre Tests nutzen das Altmodell `application.model.*` (nicht `domain.finding.*`). Dieser Überstand ist bewusst — AP09 soll nur einfrieren, nicht umbauen. Die Ablösung des Altmodells ist M3-Scope.
## 7. Empfehlungen für Folge-Arbeitspakete
- **AP10 (Architekturtest):** Der Einfrierzustand ist formal durch den Grep-Nachweis und den Integrationstest belegt. Ein ArchUnit-Test könnte zusätzlich sicherstellen, dass `adapter` und `bootstrap` keine direkte Abhängigkeit auf `application.structure.DefaultStructureValidator` oder `application.field.DefaultFieldValidator` haben. Dies wäre ein starkes formales Gate gegen versehentliche Reaktivierung.
- **AP11 (M1-Abnahme):** Der M1-Lauf erzeugt keine Fachbefunde mehr. `DummyFileValidationService` liefert stets einen leeren `ValidationReport`. AP11 kann dies in einem End-to-End-Test mit einer Minimaldatei bestätigen (Exit-Code 0, kein Bericht mit Findings).
- **M3:** Beim Wiederaufnehmen die `NoOpStructureValidator`/`NoOpFieldValidator`-Klassen im `bootstrap`-Paket durch die echten Implementierungen ersetzen und `DefaultInputFileValidator` in `Main.java` verdrahten. `DefaultStructureValidatorTestAdditional` kann dann ggf. gelöscht oder in eine reguläre Testklasse umbenannt werden.
## 8. Reviewer-Checkliste
- [x] Alle im Arbeitspaket genannten Scope-IN-Punkte sind nachweislich umgesetzt (mit begründeter Abweichung bei `DefaultStructureValidatorTestAdditional`)
- [x] Keine Scope-OUT-Punkte wurden angefasst
- [x] Abnahmekriterien sind mit konkreten Nachweisen belegt (Grep-Nachweis, Testklassen, Dateipfade)
- [x] `mvn clean verify` ist grün (173 Tests, 0 Failures)
- [ ] Der Commit für dieses AP hat eine sprechende Message (`M1-AP09: ...`) — ausstehend, Mensch committet
- [x] Keine Regeln der Grunddokumente (Spec, Fachliche, Technik) wurden verletzt
- [x] Rest-Risiken sind ehrlich dokumentiert
@@ -0,0 +1,84 @@
# Abschlussbericht Arbeitspaket AP10 Architekturtest
> **Bezug:** `docs/arbeitspakete/m1/AP10-architekturtest.md`
> **Bearbeiter:** Claude Code (claude-sonnet-4-6), Subagent
> **Datum:** 2026-04-20
> **Commit(s):** noch offen (Mensch committet nach Sichtung)
> **Status:** ✅ abgeschlossen
## 1. Zusammenfassung
ArchUnit 1.3.0 wurde als Test-Dependency aufgenommen und die Architekturtest-Klasse `de.gecheckt.asv.ArchitectureTest` mit vier Regeln (AD) implementiert. Alle vier Regeln waren beim ersten Lauf **grün** — es wurden keine Verstöße gefunden. Zusätzlich wurde `src/test/resources/log4j2-test.xml` angelegt (E-02).
## 2. Umgesetzte Änderungen
- `pom.xml` — ArchUnit-Dependency `com.tngtech.archunit:archunit-junit5:1.3.0` (test scope) ergänzt
- `src/test/java/de/gecheckt/asv/ArchitectureTest.java` — neue Testklasse mit Regeln AD (`@AnalyzeClasses`, `@ArchTest`)
- `src/test/resources/log4j2-test.xml` — Test-Log-Konfiguration angelegt (Root level WARN, Console/SYSTEM_ERR); Log4j2 bevorzugt diese Datei im Test-Classpath gegenüber `log4j2.xml`
## 3. Scope-Treue
| Scope-Punkt aus dem Arbeitspaket | Erfüllt? | Bemerkung |
|---|---|---|
| ArchUnit als Test-Dependency | ✅ | `archunit-junit5:1.3.0`, test scope |
| Vier Regeln AD implementiert | ✅ | `ArchitectureTest.java` |
| `log4j2-test.xml` anlegen (E-02) | ✅ | Root level WARN, SYSTEM_ERR |
| Leere Testklassen prüfen und löschen | ✅ | Keine leeren Testklassen gefunden; `DefaultStructureValidatorTestAdditional` hat 4 aktive `@Test`-Methoden (bewusst behalten) |
| Keine neuen Produktionsklassen | ✅ | Kein einziger neuer `.java`-Produktionscode |
| Zyklische Abhängigkeits-Regeln (Scope OUT) | ✅ | Nicht umgesetzt |
| Coverage-/Mutation-Schwellen (Scope Out) | ✅ | Nicht umgesetzt |
**Wurde der Scope eingehalten?** Ja, vollständig.
**Wurden Dinge außerhalb des Scopes gemacht?** Nein.
## 4. Abnahmekriterien
| Abnahmekriterium aus dem Arbeitspaket | Erfüllt? | Nachweis |
|---|---|---|
| ArchUnit als Test-Dependency in `pom.xml` | ✅ | `archunit-junit5:1.3.0`, test scope, `pom.xml` |
| Vier Architekturregeln AD implementiert und grün | ✅ | `Tests run: 4, Failures: 0, Errors: 0` in `de.gecheckt.asv.ArchitectureTest` |
| `log4j2-test.xml` unter `src/test/resources/` | ✅ | `src/test/resources/log4j2-test.xml` |
| Keine leeren Testklassen | ✅ | Alle Testklassen haben mindestens einen `@Test` |
| `mvn clean verify` grün, kein unerwartetes Log-Rauschen | ✅ | `BUILD SUCCESS`, 222 Tests, 0 Failures |
| Bericht dokumentiert erste Ausführung (rot/grün) | ✅ | Siehe Abschnitt 5 unten |
| Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP10-bericht.md` | ✅ | Diese Datei |
## 5. Build- und Teststatus
- `mvn clean verify`: ✅ grün (`BUILD SUCCESS`)
- Anzahl Tests: 222 gesamt (davon 1 Skipped — Windows-bedingter `fall5_dateiNichtLesbar_exitCode2` aus AP08); 4 neu in `ArchitectureTest`
- ArchUnit-Tests beim ersten Lauf: **alle 4 Regeln sofort grün** — keine Verstöße
- Log-Rauschen: Im Maven-Konsolenoutput erscheinen ERROR/WARN-Zeilen aus `CliRunnerOperationalErrorTest` (z.B. `Bedienfehler: Kein Argument übergeben`). Diese stammen vom SLF4J/Log4j2-Logger innerhalb des zu testenden `CliRunner.run()`-Aufrufs. Sie sind fachlich korrekt und erwünscht. Die `log4j2-test.xml` (Root=WARN) unterdrückt INFO/DEBUG-Rauschen; ERROR-Zeilen aus Negativ-Tests bleiben sichtbar — das ist das dokumentierte Verhalten aus E-02. Der Build ist sauber.
- Warnungen beim Build: die üblichen maven-shade-plugin-Überlappungswarnungen (unveränderter Stand seit AP02)
### Erster Lauf der 4 Regeln
Alle vier ArchUnit-Regeln waren beim ersten Lauf sofort grün. Das bestätigt:
- **Regel A (Log4j2-Sichtbarkeit):** `CliRunner` verwendet nur SLF4J (`org.slf4j`), kein direktes Log4j2. `LoggingConfigurator` liegt korrekt in `adapter.out.logging`. `Main` in `bootstrap` verwendet keine Log4j2-Typen direkt.
- **Regel B (Domain-Reinheit):** Keine Domain-Klasse referenziert Adapter oder Bootstrap.
- **Regel C (Application-Reinheit):** Keine Application-Klasse referenziert Adapter oder Bootstrap. (Die Testklassen wurden von `@AnalyzeClasses(..., importOptions = ImportOption.DoNotIncludeTests.class)` ausgeschlossen, sodass `DefaultStructureValidatorTestAdditional` mit seinen Adapter-Importen keine Rolle spielt.)
- **Regel D (Preview-Isolation):** Die `@see`- und `@link`-JavaDoc-Referenzen auf `DefaultStructureValidator` und `DefaultFieldValidator` in `NoOpStructureValidator` und `NoOpFieldValidator` erzeugen **keine** Bytecode-Abhängigkeiten. ArchUnit analysiert Bytecode, nicht JavaDoc — deshalb kein Verstoß.
## 6. Rest-Risiken und offene Punkte
- **Transitiv-Risiko ArchUnit:** ArchUnit analysiert nur direkten Bytecode. Transitive Abhängigkeiten über Reflection oder dynamisches Laden werden nicht erkannt. Für M1 ausreichend.
- **Regel D auf Namensbasis:** Regel D prüft über `haveSimpleNameContaining("DefaultStructureValidator")`. Falls ab M3 neue Klassen mit ähnlichem Namen entstehen, könnte die Regel unbeabsichtigt greifen. Bei M3-Aktivierung überprüfen und ggf. auf Paketnamen-Ebene umstellen.
- **Log-Rauschen in Tests:** Die ERROR/WARN-Zeilen aus `CliRunnerOperationalErrorTest` sind fachlich korrekt (sie testen Bedienfehler-Verhalten), erscheinen aber im Maven-Konsolenoutput. Das ist ein bekanntes Muster bei Tests, die Log4j2-konfigurierte Logger verwenden. Eine vollständige Unterdrückung würde auch echte Fehler verschlucken und ist bewusst nicht angestrebt.
- **SLF4J-Versionsdiskrepanz:** Das Projekt verwendet SLF4J 2.0.7, ArchUnit 1.3.0 zieht SLF4J 2.0.12 als transitive Dependency. Maven wählt 2.0.7 (nächste deklarierte gewinnt). Kein funktionales Problem, aber mittelfristig wäre eine Version-Angleichung empfehlenswert.
## 7. Empfehlungen für Folge-Arbeitspakete
- **AP11 (M1-Abnahme):** Die vier Architekturregeln können als Abnahmekriterium im Bericht gelistet werden — Nachweis: `ArchitectureTest` grün in CI.
- **Ab M3:** Regel D anpassen oder durch eine Paket-basierte Regel ersetzen, wenn `DefaultStructureValidator` und `DefaultFieldValidator` aktiv verdrahtet werden. Dabei sicherstellen, dass kein Adapter direkt auf die Implementierung, sondern nur auf die Interfaces (`StructureValidator`, `FieldValidator`) zugreift.
- **SLF4J-Version:** Bei nächster Dependency-Pflege auf SLF4J 2.0.12 anheben.
## 8. Reviewer-Checkliste
- [x] Alle im Arbeitspaket genannten Scope-IN-Punkte sind nachweislich umgesetzt
- [x] Keine Scope-OUT-Punkte wurden angefasst
- [x] Abnahmekriterien sind mit konkreten Nachweisen belegt (Tests, Dateipfade)
- [x] `mvn clean verify` ist grün (BUILD SUCCESS, 222 Tests, 0 Failures)
- [ ] Der Commit für dieses AP hat eine sprechende Message (`M1-AP10: ...`)
- [x] Keine Regeln der Grunddokumente (Spec, Fachliche, Technik) wurden verletzt
- [x] Rest-Risiken sind ehrlich dokumentiert
@@ -0,0 +1,151 @@
# Abschlussbericht Arbeitspaket AP11 M1-Abnahme
> **Bezug:** `docs/arbeitspakete/m1/AP11-m1-abnahme.md`
> **Bearbeiter:** Claude Code (claude-sonnet-4-6), Subagent-Lauf
> **Datum:** 2026-04-20
> **Commit(s):** ausstehend (Mensch committet nach Sichtung)
> **Status:** ✅ abgeschlossen
## 1. Zusammenfassung
M1 wurde formal abgenommen. Alle sechs Abnahmekriterien aus `meilensteine.md` §„Abnahme von M1" sind erfüllt und nachweisbar dokumentiert. Das Test-Artefakt `test-artefakte/m1/minimal.txt` wurde angelegt, alle fünf End-to-End-Läufe durchgeführt und protokolliert, und der konsolidierte M1-Abschlussbericht unter `docs/arbeitspakete/m1/berichte/M1-abschlussbericht.md` erstellt. `mvn clean verify` ist grün (222 Tests, 0 Failures, 1 Skipped).
## 2. Umgesetzte Änderungen
- `test-artefakte/m1/minimal.txt` — neu angelegt; ISO-8859-15-kompatible Dummy-Textdatei, 5 Zeilen, keine echten ASV-Daten, kein gültiges EDIFACT.
- `docs/arbeitspakete/m1/berichte/AP11-bericht.md` — dieser Bericht.
- `docs/arbeitspakete/m1/berichte/M1-abschlussbericht.md` — konsolidierter M1-Abschlussbericht mit allen Pflichtabschnitten.
Keine Produktionscode-Änderungen. Keine Test-Änderungen. Kein `git tag` gesetzt (CLAUDE.md-Regel, kein commit/tag durch Subagenten).
## 3. Scope-Treue
| Scope-Punkt aus dem Arbeitspaket | Erfüllt? | Bemerkung |
|---|---|---|
| `test-artefakte/m1/minimal.txt` anlegen | ✅ | ISO-8859-15-kompatibel, 5 Zeilen, kein echtes EDIFACT |
| `mvn clean package` ausführen | ✅ | BUILD SUCCESS, JAR `target/asv-format-validator-0.0.1-SNAPSHOT.jar` |
| Alle 5 End-to-End-Läufe mit Exit-Code-Nachweis | ✅ | Alle 5 Läufe protokolliert (§ End-to-End) |
| Meilenstein-Abnahmetabelle vollständig ausgefüllt | ✅ | Im M1-Abschlussbericht |
| Konsolidierter M1-Abschlussbericht mit allen Pflichtabschnitten | ✅ | `M1-abschlussbericht.md` |
| AP11-Bericht nach Vorlage | ✅ | Dieser Bericht |
| Git-Tag `m1-done` NICHT setzen (CLAUDE.md-Regel) | ✅ | Im Bericht dokumentiert |
| Vorgriffe auf M2 (Scope OUT) | ✅ nicht gemacht | — |
| Release-Builds, Signierung (Scope OUT) | ✅ nicht gemacht | — |
**Wurde der Scope eingehalten?** Ja, vollständig.
**Wurden Dinge außerhalb des Scopes gemacht?** Nein.
## 4. Abnahmekriterien
| Abnahmekriterium aus dem Arbeitspaket | Erfüllt? | Nachweis |
|---|---|---|
| `test-artefakte/m1/minimal.txt` existiert | ✅ | `test-artefakte/m1/minimal.txt` angelegt |
| Alle fünf Läufe sind protokolliert | ✅ | Abschnitt 5 unten; vollständig im M1-Abschlussbericht |
| `M1-abschlussbericht.md` existiert mit allen Pflichtabschnitten | ✅ | `docs/arbeitspakete/m1/berichte/M1-abschlussbericht.md` |
| Meilenstein-Abnahmetabelle vollständig, jede Zeile mit Nachweis | ✅ | M1-Abschlussbericht §„Meilenstein-Abnahmetabelle" |
| Kein Exit-Code 3 mehr erreichbar | ✅ | CliRunner-Switch über `Verdict`; keine `return 3`-Stelle im Produktionscode (AP06-Nachweis) |
| `mvn clean verify` grün | ✅ | BUILD SUCCESS, 222 Tests, 0 Failures, 1 Skipped |
| Git-Tag `m1-done` — nicht gesetzt | ✅ | Dokumentiert: Tag wird vom Entwickler nach finaler Sichtung gesetzt |
| Freigabe-Vermerk ist explizit | ✅ | M1-Abschlussbericht §„Freigabe-Vermerk" |
| Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP11-bericht.md` | ✅ | Dieser Bericht |
## 5. End-to-End-Protokoll
Alle Läufe mit JAR `target/asv-format-validator-0.0.1-SNAPSHOT.jar` vom Projekt-Root aus.
### Lauf 1 — Eingabedatei vorhanden (erster Lauf)
```
java -jar target/asv-format-validator-0.0.1-SNAPSHOT.jar test-artefakte/m1/minimal.txt
```
- **Exit-Code:** `0`
- **Ausgabe (stdout):** Prüfbericht mit `Urteil: GÜLTIG`, `Keine Befunde.`, M1-Platzhalter-Hinweis
- **Erzeugte Dateien:** `test-artefakte/m1/minimal.txt.txt`, `test-artefakte/m1/minimal.txt.log`
### Lauf 2 — identischer Aufruf (Suffix-Logik)
```
java -jar target/asv-format-validator-0.0.1-SNAPSHOT.jar test-artefakte/m1/minimal.txt
```
- **Exit-Code:** `0`
- **Ausgabe (stdout):** identisch zu Lauf 1
- **Erzeugte Dateien:** `test-artefakte/m1/minimal.txt_v1.txt`, `test-artefakte/m1/minimal.txt_v1.log`
Nach Lauf 2 vorhandene Dateien im Verzeichnis: `minimal.txt`, `minimal.txt.txt`, `minimal.txt.log`, `minimal.txt_v1.txt`, `minimal.txt_v1.log`
### Lauf 3 — nicht existierende Datei
```
java -jar target/asv-format-validator-0.0.1-SNAPSHOT.jar nicht-vorhanden.txt
```
- **Exit-Code:** `2`
- **Ausgabe (stderr):** `Bedienfehler: Datei nicht gefunden: nicht-vorhanden.txt`
- **Ausgabe (stdout):** Prüfbericht mit `Urteil: BEDIENFEHLER`, `Regel=OPERATIONAL-FILE-NOT-FOUND`
- **Berichtdatei:** `nicht-vorhanden.txt.txt` im aktuellen Verzeichnis (übergeordnetes Verzeichnis bekannt)
### Lauf 4 — kein Argument
```
java -jar target/asv-format-validator-0.0.1-SNAPSHOT.jar
```
- **Exit-Code:** `2`
- **Ausgabe (stderr):** `Bedienfehler: Kein Argument übergeben.`
- **Ausgabe (stdout):** Prüfbericht mit `Urteil: BEDIENFEHLER`, `Regel=OPERATIONAL-MISSING-ARG`
- **Keine Berichtdatei** (kein Verzeichnis bekannt)
### Lauf 5 — zu viele Argumente
```
java -jar target/asv-format-validator-0.0.1-SNAPSHOT.jar datei1.txt datei2.txt
```
- **Exit-Code:** `2`
- **Ausgabe (stderr):** `Bedienfehler: Zu viele Argumente (2).`
- **Ausgabe (stdout):** Prüfbericht mit `Urteil: BEDIENFEHLER`, `Regel=OPERATIONAL-TOO-MANY-ARGS`
- **Keine Berichtdatei** (kein Verzeichnis bekannt)
## 6. Build- und Teststatus
- `mvn clean verify`: ✅ grün (`BUILD SUCCESS`)
- Anzahl Tests: **222** (davon 0 neu in AP11 — kein Produktionscode geändert)
- Fehler / Skipped: 0 Failures / 1 Skipped (Windows-bedingt: `fall5_dateiNichtLesbar_exitCode2` aus AP08)
- Coverage (JaCoCo, informativ): **87 % Line Coverage** (704 / 806 Zeilen), keine Schwellwerte aktiv (M9-Scope)
- Warnungen: Shade-Plugin META-INF-Überlappungen (unverändert seit AP02); `sun.reflect.Reflection.getCallerClass` (Log4j2-interne Warnung beim JAR-Start)
## 7. Rest-Risiken und offene Punkte
- **Git-Tag `m1-done` nicht gesetzt:** Tag wird vom Entwickler nach finaler Sichtung manuell gesetzt (CLAUDE.md §„Harte Regeln: kein git commit/add/push durch Subagenten").
- **Konsolenausgabe-Encoding auf Windows:** Die stdout-Ausgabe erscheint auf Windows-Konsolen mit CP1252/OEM437 mit Mojibake (`G?LTIG` statt `GÜLTIG`). Die Berichtdatei selbst ist korrekt UTF-8. Bekanntes Windows-Konsolen-Problem. Empfehlung: In M9-Dokumentation erwähnen (`chcp 65001` als Workaround).
- **`nicht-vorhanden.txt.txt`** entsteht im Projekt-Root durch Lauf 3. Dies ist korrekt (übergeordnetes Verzeichnis = Projekt-Root war bekannt). Datei kann nach Sichtung gelöscht werden.
- **`logs/asv-format-validator-fallback.log`:** Log4j2-Fallback-Datei aus `log4j2.xml`, entsteht bei Läufen ohne Eingabedatei-Pfad (Lauf 4, 5). Durch `.gitignore`-Eintrag `logs/` nicht versioniert.
- **Konsolidierte Rest-Risiken aus AP01AP10** sind im M1-Abschlussbericht §„Rest-Risiken" vollständig dokumentiert.
## 8. Empfehlungen für Folge-Arbeitspakete
Siehe M1-Abschlussbericht §„Empfehlungen für M2". Wesentliche Punkte:
- M2 baut auf dem ISO-8859-15-Encoding auf, das in AP06 eingeführt wurde.
- Dateinamensschemata und globale Rahmenregeln kommen in M2.
- Architekturtest (ArchUnit) ist aktiv — bei M2-Klassen in `adapter` oder `bootstrap` sicherstellen, dass keine Log4j2-Typen direkt importiert werden.
## 9. Git-Tag-Vermerk
**Tag `m1-done` wurde NICHT gesetzt.** Gemäß CLAUDE.md §„Harte Regeln": Kein `git commit`, `git add` oder `git push` durch den Subagenten. Der Entwickler setzt den Tag nach finaler Sichtung manuell:
```bash
git tag -a m1-done -m "Meilenstein 1 abgeschlossen, siehe docs/arbeitspakete/m1/berichte/M1-abschlussbericht.md"
```
## 10. Reviewer-Checkliste
- [x] Alle im Arbeitspaket genannten Scope-IN-Punkte sind nachweislich umgesetzt
- [x] Keine Scope-OUT-Punkte wurden angefasst
- [x] Abnahmekriterien sind mit konkreten Nachweisen belegt (Tests, Exit-Codes, Dateipfade)
- [x] `mvn clean verify` ist grün (222 Tests, BUILD SUCCESS)
- [ ] Der Commit für dieses AP hat eine sprechende Message (`M1-AP11: M1-Abnahme abgeschlossen`) — ausstehend, Mensch committet
- [x] Keine Regeln der Grunddokumente (Spec, Fachliche, Technik) wurden verletzt
- [x] Rest-Risiken sind ehrlich dokumentiert
@@ -0,0 +1,193 @@
# M1-Abschlussbericht Projektfundament, Logging und Ergebnismodell
> **Meilenstein:** M1
> **Grundlage:** `docs/specs/meilensteine.md` v3
> **Bearbeiter:** Claude Code (claude-sonnet-4-6), Subagenten-Reihe AP01AP11
> **Datum:** 2026-04-20
> **Commit(s):** ausstehend — Mensch committet und taggt nach finaler Sichtung
> **Status:** ✅ abnahmebereit
---
## 1. Zusammenfassung
Meilenstein M1 stellt den tragfähigen technischen Sockel des ASV-Format-Validators bereit. Es wurden in elf aufeinander aufbauenden Arbeitspaketen das Maven-Projekt gehärtet, die hexagonale Paketstruktur etabliert, die SLF4J/Log4j2-Fassade eingeführt, das Befundmodell mit Spec-/Diagnose-Trennung aufgebaut, CLI-Bootstrap und Exit-Codes normiert, Ausgabeartefakte mit Suffix-Logik implementiert, Ministralbericht für Bedienfehler ergänzt, Preview-Altlogik eingefroren sowie ein vollständiger ArchUnit-Architekturtest hinterlegt.
M1 enthält bewusst **keine** EDIFACT-Fachvalidierung. Der `DummyFileValidationService` liest jede Eingabedatei mit ISO-8859-15, gibt einen leeren Validierungsbericht zurück und liefert Exit-Code 0 (GÜLTIG). Die fachliche Tiefe beginnt ab M2 (Artefaktschicht, Dateinamen) und M3 (EDIFACT-Serviceebene). Die Preview-Validatoren (`DefaultStructureValidator`, `DefaultFieldValidator`) sind eingefroren und durch No-Op-Platzhalter ersetzt — sie werden ab M3 reaktiviert und gegen die finalen V1-Regelklassifikationen bewertet.
---
## 2. AP-Übersicht
| AP | Titel | Status | Commit |
|---|---|---|---|
| AP01 | Ist-Stand-Inventar und Delta-Analyse | ✅ | nicht committet — Entwickler committet final |
| AP02 | Build-Infrastruktur härten (SLF4J, JaCoCo, PIT, Shade-Plugin, Mockito-Agent) | ✅ | `d0aac6a` / `61935df` |
| AP03 | Hexagonale Paketstruktur anlegen und Ist-Code migrieren | ✅ | `bd45de8` |
| AP04 | Logging-Adapter: SLF4J-Fassade etabliert | ✅ | `a1a48e9` |
| AP05 | Befundmodell mit Spec-/Diagnose-Trennung | ✅ | ausstehend — Entwickler committet final |
| AP06 | Bootstrap und CLI-Adapter (Exit-Codes 0/1/2, ISO-8859-15, Shade-JAR) | ✅ | ausstehend |
| AP07 | Ausgabeartefakte: Berichtdatei + Log-Datei mit Suffix-Logik | ✅ | ausstehend |
| AP08 | Minimalbericht bei Bedienfehlern (Exit-Code 2) | ✅ | ausstehend |
| AP09 | Altlogik einfrieren (Preview-Validatoren deaktivieren) | ✅ | ausstehend |
| AP10 | Architekturtest (ArchUnit, 4 Regeln) | ✅ | ausstehend |
| AP11 | M1-Abnahme (dieser Bericht) | ✅ | ausstehend |
Hinweis: AP02AP04 haben bereits committete Hashes aus dem Git-Log (`cd6e522`, `a1a48e9`, `bd45de8`, `d0aac6a`, `61935df`). AP05AP11 wurden in einem späteren Subagenten-Lauf bearbeitet; die Commits werden vom Entwickler nach finaler Sichtung gesetzt.
---
## 3. Meilenstein-Abnahmetabelle
| Kriterium | Nachweis | Status |
|---|---|---|
| Anwendung ist als JAR unter Windows mit Java 21 startbar | Lauf 1: `java -jar target/asv-format-validator-0.0.1-SNAPSHOT.jar test-artefakte/m1/minimal.txt` → Exit 0 | ✅ |
| Falsches oder fehlendes Argument → Exit-Code 2 mit Minimalbericht | Lauf 3 (Datei nicht vorhanden → Exit 2), Lauf 4 (kein Arg → Exit 2), Lauf 5 (2 Args → Exit 2) | ✅ |
| Bericht- und Log-Datei im Eingabeverzeichnis mit korrekter Suffix-Logik | Lauf 1: `minimal.txt.txt` + `minimal.txt.log`; Lauf 2: `minimal.txt_v1.txt` + `minimal.txt_v1.log` | ✅ |
| Log4j2-Bindung ist außerhalb von Bootstrap und Logging-Adapter nicht sichtbar | ArchUnit-Test AP10 Regel A: `keinLog4j2AusserInErlaubtenPaketen` → GRÜN; Grep auf `import org.apache.logging.log4j` außerhalb `adapter.out.logging` und `bootstrap` → leer | ✅ |
| Befundmodell trennt Spec-Urteil und diagnostische Weiteranalyse strukturell | Unit-Test AP05: `ValidationReportTest#diagnosticErrorLiefertVALID()` → GRÜN; `FindingKind.SPEC` vs. `FindingKind.DIAGNOSTIC`; `computeVerdict()` ignoriert DIAGNOSTIC | ✅ |
| Build und Tests sind grün | `mvn clean verify` → BUILD SUCCESS, 222 Tests, 0 Failures, 1 Skipped (Windows-bedingt) | ✅ |
---
## 4. End-to-End-Protokoll
JAR: `target/asv-format-validator-0.0.1-SNAPSHOT.jar`
Ausführungsverzeichnis: Projekt-Root `D:\Dev\Projects\asv-format-validator`
### Lauf 1 — Eingabedatei vorhanden, erster Lauf
```
java -jar target/asv-format-validator-0.0.1-SNAPSHOT.jar test-artefakte/m1/minimal.txt
```
| Merkmal | Wert |
|---|---|
| Exit-Code | `0` (GÜLTIG) |
| stdout | Prüfbericht: `Urteil: GÜLTIG`, `Keine Befunde.`, M1-Platzhalter-Hinweis |
| erzeugte Dateien | `test-artefakte/m1/minimal.txt.txt` (UTF-8, ~430 Byte), `test-artefakte/m1/minimal.txt.log` |
### Lauf 2 — identischer Aufruf (Suffix-Logik)
```
java -jar target/asv-format-validator-0.0.1-SNAPSHOT.jar test-artefakte/m1/minimal.txt
```
| Merkmal | Wert |
|---|---|
| Exit-Code | `0` (GÜLTIG) |
| stdout | identisch zu Lauf 1 |
| erzeugte Dateien | `test-artefakte/m1/minimal.txt_v1.txt`, `test-artefakte/m1/minimal.txt_v1.log` |
**Dateien nach Lauf 2:** `minimal.txt`, `minimal.txt.txt`, `minimal.txt.log`, `minimal.txt_v1.txt`, `minimal.txt_v1.log`
### Lauf 3 — nicht existierende Datei
```
java -jar target/asv-format-validator-0.0.1-SNAPSHOT.jar nicht-vorhanden.txt
```
| Merkmal | Wert |
|---|---|
| Exit-Code | `2` (BEDIENFEHLER) |
| stderr | `Bedienfehler: Datei nicht gefunden: nicht-vorhanden.txt` |
| stdout | Prüfbericht: `Urteil: BEDIENFEHLER`, `Regel=OPERATIONAL-FILE-NOT-FOUND` |
| Berichtdatei | `nicht-vorhanden.txt.txt` im Projekt-Root (übergeordnetes Verzeichnis bekannt) |
### Lauf 4 — kein Argument
```
java -jar target/asv-format-validator-0.0.1-SNAPSHOT.jar
```
| Merkmal | Wert |
|---|---|
| Exit-Code | `2` (BEDIENFEHLER) |
| stderr | `Bedienfehler: Kein Argument übergeben.` |
| stdout | Prüfbericht: `Urteil: BEDIENFEHLER`, `Regel=OPERATIONAL-MISSING-ARG` |
| Berichtdatei | keine (kein Eingabeverzeichnis bekannt) |
### Lauf 5 — zu viele Argumente
```
java -jar target/asv-format-validator-0.0.1-SNAPSHOT.jar datei1.txt datei2.txt
```
| Merkmal | Wert |
|---|---|
| Exit-Code | `2` (BEDIENFEHLER) |
| stderr | `Bedienfehler: Zu viele Argumente (2).` |
| stdout | Prüfbericht: `Urteil: BEDIENFEHLER`, `Regel=OPERATIONAL-TOO-MANY-ARGS` |
| Berichtdatei | keine (kein Eingabeverzeichnis bekannt) |
---
## 5. Quality-Metriken
> Alle Metriken informativ. Quality-Gates mit Schwellwerten gelten erst ab M9.
| Metrik | Wert |
|---|---|
| Testanzahl gesamt | 222 |
| Failures | 0 |
| Errors | 0 |
| Skipped | 1 (Windows-bedingt: `fall5_dateiNichtLesbar_exitCode2` aus AP08; `setReadable(false)` ohne Wirkung für eigenen Prozess) |
| Line Coverage (JaCoCo, gesamt) | **87 %** (704 / 806 Zeilen) |
| Instruction Coverage (JaCoCo) | informativ, kein Gate |
| PIT Mutation-Score (einmalig AP02) | 83 % (249 Mutationen, 207 getötet) — Stand vor AP05AP10 |
| Build-Dauer (`mvn clean verify`) | ~16 s |
---
## 6. Rest-Risiken (konsolidiert aus AP01AP10)
| Risiko | Quelle | Auswirkung | M2/M3-Handlungsbedarf |
|---|---|---|---|
| `DefaultSegmentLineTokenizer` trennt starr an `+`; UNA-Segment nicht ausgewertet | AP01, AP00 | Falscher Parse für nicht-EDIFACT-Dateien ab M3 | M3: Tokenizer gegen UNA-bewusstes Pendant ersetzen |
| `DefaultInputFileParser` erzeugt immer genau eine Message pro Datei | AP01 | Nur Preview-Parser; nicht real für mehrere UNH/UNT-Paare | M3: Parser auf Nachrichtenerkennung erweitern |
| `DefaultStructureValidatorTestAdditional` hat 3 aktive Tests — nicht aus AP09 gelöscht | AP09, AP10 | Minimales Rauschen; Tests sind inhaltlich korrekt (STRUCTURE_012-Logik) | M3: bei Reaktivierung der Preview-Validatoren entscheiden |
| Preview-Regeln (`DefaultStructureValidator`) noch nicht gegen V1-V/T/N/K bewertet | AP09 | Einige der 19 Regeln könnten bei M3-Aktivierung zu rigide sein | M3: jede Regel neu klassifizieren |
| `ValidationResult`/`ValidationSeverity` (Altmodell in `application.model`) koexistieren mit neuem `domain.finding` | AP05, AP09 | Zwei parallele Ergebnistypen; Preview-Code nutzt Altmodell | M3: Altmodell ablösen oder vollständig migrieren |
| `LoggingConfigurator.configureLogFile(Path)` erzeugt Windows-TempDir-Lock in Tests | AP07 | Lösung durch Mockito-Mock in `CliRunnerTest` / `CliRunnerOutputArtifactsTest`; echte Konfiguration durch JAR-Test bestätigt | AP10 (erledigt): Mock ist Workaround; bei M9-Testausbau beachten |
| Konsolenausgabe-Encoding auf Windows (Mojibake bei Umlauten) | AP07, AP08 | Berichtdatei korrekt UTF-8; nur sichtbares Darstellungsproblem in `cmd.exe`/`powershell.exe` | M9: Dokumentation (`chcp 65001`-Hinweis) |
| SLF4J-Versionsdiskrepanz: Projekt 2.0.7, ArchUnit zieht 2.0.12 | AP10 | Kein funktionales Problem; Maven wählt 2.0.7 | M9 oder früher: Versions-Angleichung bei Dependency-Pflege |
| `sun.reflect.Reflection.getCallerClass`-Warnung im JAR-Betrieb | AP06 | Log4j2-interne Performance-Warnung; kein Fehler | M9: Log4j2-Upgrade auf 2.23+ optional |
| Shade-Plugin erzeugt Überlappungswarnungen bei META-INF-Ressourcen | AP06 | Kosmetisch; kein Fehler; bekanntes Fat-JAR-Verhalten | bleibt bis M9; kein Handlungsbedarf |
| Fall 5 (Datei nicht lesbar) nur auf Unix testbar | AP08 | Windows: `setReadable(false)` ohne Wirkung für eigenen Prozess; Test explizit übersprungen | M9: NTFS-ACL-basierter Testansatz falls benötigt |
---
## 7. Empfehlungen für M2
- **ISO-8859-15 ist etabliert** (AP06): M2 kann darauf aufbauen. `DummyFileValidationService.INPUT_CHARSET = Charset.forName("ISO-8859-15")` ist paketöffentlich für Testbarkeit.
- **Dateinamensschemata** (unverschlüsselt: `B<VKNR>...`, verschlüsselt: `<IK>_<IK>_<E|T>ASV0<Zähler>`, Auftragsdatei: `.AUF`) kommen in M2 als harte Prüfregeln.
- **`adapter.out.filesystem`** ist angelegt aber leer. M2 füllt es mit dem Dateizugriffs-Adapter (read-only, ISO-8859-15, Artefaktklassifikation).
- **ArchUnit-Regeln AD** sind aktiv. Bei neuen M2-Klassen in `adapter` oder `bootstrap` sicherstellen, dass keine Log4j2-Direktimporte vorkommen (Regel A) und dass `domain`/`application` keine Adapter-Abhängigkeiten bekommen (Regeln B/C).
- **Regel D** (Preview-Isolation) referenziert `DefaultStructureValidator` und `DefaultFieldValidator` namentlich. Ab M3, wenn diese Klassen reaktiviert werden, Regel D auf Paket-Basis umstellen.
- **`ValidationResult`/`ValidationReport` Koexistenz**: M2 sollte ausschließlich `ValidationReport` (neues Befundmodell) für neue Befunde verwenden. Die Ablösung des Altmodells ist M3-Scope.
- **Exit-Code 3 nicht mehr erreichbar** (AP06-Nachweis). M2 muss diesen Zustand bewahren.
- **Partnerdatei-Ableitung** (deterministisch, nicht heuristisch) ist M2-Scope — AP11 liefert keine Vorarbeit dafür.
- **Übermittlungszähler-Prüfung** (001999, keine quartalsübergreifende Sequenz in V1) kommt in M2.
---
## 8. Freigabe-Vermerk
**M1 ist abnahmebereit.**
Alle sechs Abnahmekriterien aus `meilensteine.md` v3 §„Abnahme von M1" sind erfüllt:
1. Anwendung als JAR unter Windows mit Java 21 startbar — ✅ (Lauf 1, Exit-Code 0)
2. Falsches/fehlendes Argument → Exit-Code 2 mit Minimalbericht — ✅ (Läufe 3, 4, 5)
3. Bericht- und Log-Datei im Eingabeverzeichnis mit korrekter Suffix-Logik — ✅ (Läufe 1+2)
4. Log4j2-Bindung außerhalb Bootstrap/Logging-Adapter nicht sichtbar — ✅ (ArchUnit Regel A)
5. Befundmodell trennt Spec-Urteil und Diagnose strukturell — ✅ (`diagnosticErrorLiefertVALID` GRÜN)
6. Build und Tests grün — ✅ (222 Tests, 0 Failures)
Der einzige Skipped-Test (`fall5_dateiNichtLesbar_exitCode2`) ist Windows-plattformbedingt und enthält ein explizites `assumeTrue(...)`. Er ist kein Blocker.
**Git-Tag `m1-done` wurde NICHT gesetzt.** Gemäß CLAUDE.md §„Harte Regeln" setzt kein Subagent `git commit`, `git add` oder `git push`. Der Entwickler setzt den Tag nach finaler Sichtung der Berichte manuell:
```bash
git tag -a m1-done -m "Meilenstein 1 abgeschlossen, siehe docs/arbeitspakete/m1/berichte/M1-abschlussbericht.md"
```
+520
View File
@@ -146,3 +146,523 @@ java.io.IOException: File does not exist: /non/existent/file.txt
at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162) [surefire-booter-3.0.0.jar:3.0.0] at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162) [surefire-booter-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:507) [surefire-booter-3.0.0.jar:3.0.0] at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:507) [surefire-booter-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:495) [surefire-booter-3.0.0.jar:3.0.0] at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:495) [surefire-booter-3.0.0.jar:3.0.0]
2026-04-20 08:05:59 [main] ERROR de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication - Fehler beim Lesen der Datei: File does not exist: /non/existent/file.txt
java.io.IOException: File does not exist: /non/existent/file.txt
at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication.parseFile(AsvValidatorApplication.java:141) ~[classes/:?]
at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication.run(AsvValidatorApplication.java:104) ~[classes/:?]
at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplicationTest.testRunWithNonExistentFileShouldReturnFileErrorExitCode(AsvValidatorApplicationTest.java:94) ~[test-classes/:?]
at jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[?:?]
at java.lang.reflect.Method.invoke(Method.java:580) ~[?:?]
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727) ~[junit-platform-commons-1.9.2.jar:1.9.2]
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at java.util.ArrayList.forEach(ArrayList.java:1596) ~[?:?]
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at java.util.ArrayList.forEach(ArrayList.java:1596) ~[?:?]
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147) ~[junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127) ~[junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90) ~[junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55) ~[junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.apache.maven.surefire.junitplatform.LazyLauncher.execute(LazyLauncher.java:50) [surefire-junit-platform-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.execute(JUnitPlatformProvider.java:184) [surefire-junit-platform-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:148) [surefire-junit-platform-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:122) [surefire-junit-platform-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:385) [surefire-booter-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162) [surefire-booter-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:507) [surefire-booter-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:495) [surefire-booter-3.0.0.jar:3.0.0]
2026-04-20 08:06:18 [main] ERROR de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication - Fehler beim Lesen der Datei: File does not exist: /non/existent/file.txt
java.io.IOException: File does not exist: /non/existent/file.txt
at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication.parseFile(AsvValidatorApplication.java:141) ~[classes/:?]
at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication.run(AsvValidatorApplication.java:104) ~[classes/:?]
at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplicationTest.testRunWithNonExistentFileShouldReturnFileErrorExitCode(AsvValidatorApplicationTest.java:94) ~[test-classes/:?]
at jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[?:?]
at java.lang.reflect.Method.invoke(Method.java:580) ~[?:?]
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727) ~[junit-platform-commons-1.9.2.jar:1.9.2]
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at java.util.ArrayList.forEach(ArrayList.java:1596) ~[?:?]
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at java.util.ArrayList.forEach(ArrayList.java:1596) ~[?:?]
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147) ~[junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127) ~[junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90) ~[junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55) ~[junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.apache.maven.surefire.junitplatform.LazyLauncher.execute(LazyLauncher.java:50) [surefire-junit-platform-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.execute(JUnitPlatformProvider.java:184) [surefire-junit-platform-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:148) [surefire-junit-platform-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:122) [surefire-junit-platform-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:385) [surefire-booter-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162) [surefire-booter-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:507) [surefire-booter-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:495) [surefire-booter-3.0.0.jar:3.0.0]
2026-04-20 08:51:45 [main] ERROR de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication - Fehler beim Lesen der Datei: File does not exist: /non/existent/file.txt
java.io.IOException: File does not exist: /non/existent/file.txt
at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication.parseFile(AsvValidatorApplication.java:141) ~[classes/:?]
at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication.run(AsvValidatorApplication.java:104) ~[classes/:?]
at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplicationTest.testRunWithNonExistentFileShouldReturnFileErrorExitCode(AsvValidatorApplicationTest.java:94) ~[test-classes/:?]
at jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[?:?]
at java.lang.reflect.Method.invoke(Method.java:580) ~[?:?]
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727) ~[junit-platform-commons-1.9.2.jar:1.9.2]
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at java.util.ArrayList.forEach(ArrayList.java:1596) ~[?:?]
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at java.util.ArrayList.forEach(ArrayList.java:1596) ~[?:?]
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147) ~[junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127) ~[junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90) ~[junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55) ~[junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.apache.maven.surefire.junitplatform.LazyLauncher.execute(LazyLauncher.java:50) [surefire-junit-platform-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.execute(JUnitPlatformProvider.java:184) [surefire-junit-platform-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:148) [surefire-junit-platform-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:122) [surefire-junit-platform-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:385) [surefire-booter-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162) [surefire-booter-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:507) [surefire-booter-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:495) [surefire-booter-3.0.0.jar:3.0.0]
2026-04-20 08:53:25 [main] ERROR de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication - Fehler beim Lesen der Datei: File does not exist: /non/existent/file.txt
java.io.IOException: File does not exist: /non/existent/file.txt
at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication.parseFile(AsvValidatorApplication.java:141) ~[classes/:?]
at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication.run(AsvValidatorApplication.java:104) ~[classes/:?]
at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplicationTest.testRunWithNonExistentFileShouldReturnFileErrorExitCode(AsvValidatorApplicationTest.java:94) ~[test-classes/:?]
at jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[?:?]
at java.lang.reflect.Method.invoke(Method.java:580) ~[?:?]
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727) ~[junit-platform-commons-1.9.2.jar:1.9.2]
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at java.util.ArrayList.forEach(ArrayList.java:1596) ~[?:?]
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at java.util.ArrayList.forEach(ArrayList.java:1596) ~[?:?]
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147) ~[junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127) ~[junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90) ~[junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55) ~[junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.apache.maven.surefire.junitplatform.LazyLauncher.execute(LazyLauncher.java:50) [surefire-junit-platform-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.execute(JUnitPlatformProvider.java:184) [surefire-junit-platform-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:148) [surefire-junit-platform-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:122) [surefire-junit-platform-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:385) [surefire-booter-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162) [surefire-booter-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:507) [surefire-booter-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:495) [surefire-booter-3.0.0.jar:3.0.0]
2026-04-20 08:53:43 [main] ERROR de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication - Fehler beim Lesen der Datei: File does not exist: /non/existent/file.txt
java.io.IOException: File does not exist: /non/existent/file.txt
at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication.parseFile(AsvValidatorApplication.java:141) ~[classes/:?]
at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication.run(AsvValidatorApplication.java:104) ~[classes/:?]
at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplicationTest.testRunWithNonExistentFileShouldReturnFileErrorExitCode(AsvValidatorApplicationTest.java:94) ~[test-classes/:?]
at jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[?:?]
at java.lang.reflect.Method.invoke(Method.java:580) ~[?:?]
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727) ~[junit-platform-commons-1.9.2.jar:1.9.2]
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at java.util.ArrayList.forEach(ArrayList.java:1596) ~[?:?]
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at java.util.ArrayList.forEach(ArrayList.java:1596) ~[?:?]
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147) ~[junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127) ~[junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90) ~[junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55) ~[junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.apache.maven.surefire.junitplatform.LazyLauncher.execute(LazyLauncher.java:50) [surefire-junit-platform-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.execute(JUnitPlatformProvider.java:184) [surefire-junit-platform-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:148) [surefire-junit-platform-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:122) [surefire-junit-platform-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:385) [surefire-booter-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162) [surefire-booter-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:507) [surefire-booter-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:495) [surefire-booter-3.0.0.jar:3.0.0]
2026-04-20 08:53:58 [main] ERROR de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication - Fehler beim Lesen der Datei: File does not exist: /non/existent/file.txt
java.io.IOException: File does not exist: /non/existent/file.txt
at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication.parseFile(AsvValidatorApplication.java:141) ~[classes/:?]
at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication.run(AsvValidatorApplication.java:104) ~[classes/:?]
at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplicationTest.testRunWithNonExistentFileShouldReturnFileErrorExitCode(AsvValidatorApplicationTest.java:94) ~[test-classes/:?]
at jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[?:?]
at java.lang.reflect.Method.invoke(Method.java:580) ~[?:?]
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727) ~[junit-platform-commons-1.9.2.jar:1.9.2]
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68) ~[junit-jupiter-engine-5.9.2.jar:5.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at java.util.ArrayList.forEach(ArrayList.java:1596) ~[?:?]
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at java.util.ArrayList.forEach(ArrayList.java:1596) ~[?:?]
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54) ~[junit-platform-engine-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147) ~[junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127) ~[junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90) ~[junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55) ~[junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86) [junit-platform-launcher-1.9.2.jar:1.9.2]
at org.apache.maven.surefire.junitplatform.LazyLauncher.execute(LazyLauncher.java:50) [surefire-junit-platform-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.execute(JUnitPlatformProvider.java:184) [surefire-junit-platform-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:148) [surefire-junit-platform-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:122) [surefire-junit-platform-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:385) [surefire-booter-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162) [surefire-booter-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:507) [surefire-booter-3.0.0.jar:3.0.0]
at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:495) [surefire-booter-3.0.0.jar:3.0.0]
2026-04-20 08:58:27 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: leer.txt, Urteil: VALID
2026-04-20 08:58:27 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 0
2026-04-20 08:58:27 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 2
2026-04-20 08:58:27 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: bedien.txt, Urteil: OPERATIONAL_ERROR
2026-04-20 08:58:27 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Datei nicht gefunden: /nicht/vorhanden/datei.txt
2026-04-20 08:58:27 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: fehlerhaft.txt, Urteil: INVALID
2026-04-20 08:58:27 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'inhalt.txt' gelesen (11 Bytes, 11 Zeichen, Encoding: ISO-8859-15)
2026-04-20 08:58:27 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'leer.txt' gelesen (0 Bytes, 0 Zeichen, Encoding: ISO-8859-15)
2026-04-20 08:59:00 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: leer.txt, Urteil: VALID
2026-04-20 08:59:00 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 0
2026-04-20 08:59:00 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 2
2026-04-20 08:59:00 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: bedien.txt, Urteil: OPERATIONAL_ERROR
2026-04-20 08:59:00 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Datei nicht gefunden: /nicht/vorhanden/datei.txt
2026-04-20 08:59:00 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: fehlerhaft.txt, Urteil: INVALID
2026-04-20 08:59:00 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'inhalt.txt' gelesen (11 Bytes, 11 Zeichen, Encoding: ISO-8859-15)
2026-04-20 08:59:00 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'leer.txt' gelesen (0 Bytes, 0 Zeichen, Encoding: ISO-8859-15)
2026-04-20 08:59:35 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: leer.txt, Urteil: VALID
2026-04-20 08:59:35 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 0
2026-04-20 08:59:35 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 2
2026-04-20 08:59:35 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: bedien.txt, Urteil: OPERATIONAL_ERROR
2026-04-20 08:59:35 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Datei nicht gefunden: /nicht/vorhanden/datei.txt
2026-04-20 08:59:35 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: fehlerhaft.txt, Urteil: INVALID
2026-04-20 08:59:35 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'inhalt.txt' gelesen (11 Bytes, 11 Zeichen, Encoding: ISO-8859-15)
2026-04-20 08:59:35 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'leer.txt' gelesen (0 Bytes, 0 Zeichen, Encoding: ISO-8859-15)
2026-04-20 08:59:51 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'test-asv.txt' gelesen (11 Bytes, 11 Zeichen, Encoding: ISO-8859-15)
2026-04-20 08:59:51 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: test-asv.txt, Urteil: VALID
2026-04-20 08:59:55 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 0
2026-04-20 08:59:59 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Datei nicht gefunden: C:/Program Files/Git/nicht/vorhanden.txt
2026-04-20 09:00:16 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: leer.txt, Urteil: VALID
2026-04-20 09:00:16 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 0
2026-04-20 09:00:16 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 2
2026-04-20 09:00:16 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: bedien.txt, Urteil: OPERATIONAL_ERROR
2026-04-20 09:00:16 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Datei nicht gefunden: /nicht/vorhanden/datei.txt
2026-04-20 09:00:16 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: fehlerhaft.txt, Urteil: INVALID
2026-04-20 09:00:16 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'inhalt.txt' gelesen (11 Bytes, 11 Zeichen, Encoding: ISO-8859-15)
2026-04-20 09:00:16 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'leer.txt' gelesen (0 Bytes, 0 Zeichen, Encoding: ISO-8859-15)
2026-04-20 09:00:46 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: leer.txt, Urteil: VALID
2026-04-20 09:00:46 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 0
2026-04-20 09:00:46 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 2
2026-04-20 09:00:46 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: bedien.txt, Urteil: OPERATIONAL_ERROR
2026-04-20 09:00:46 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Datei nicht gefunden: /nicht/vorhanden/datei.txt
2026-04-20 09:00:46 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: fehlerhaft.txt, Urteil: INVALID
2026-04-20 09:00:46 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'inhalt.txt' gelesen (11 Bytes, 11 Zeichen, Encoding: ISO-8859-15)
2026-04-20 09:00:46 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'leer.txt' gelesen (0 Bytes, 0 Zeichen, Encoding: ISO-8859-15)
2026-04-20 09:02:24 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: leer.txt, Urteil: VALID
2026-04-20 09:02:24 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 0
2026-04-20 09:02:24 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 2
2026-04-20 09:02:24 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: bedien.txt, Urteil: OPERATIONAL_ERROR
2026-04-20 09:02:24 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Datei nicht gefunden: /nicht/vorhanden/datei.txt
2026-04-20 09:02:24 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: fehlerhaft.txt, Urteil: INVALID
2026-04-20 09:02:24 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'inhalt.txt' gelesen (11 Bytes, 11 Zeichen, Encoding: ISO-8859-15)
2026-04-20 09:02:24 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'leer.txt' gelesen (0 Bytes, 0 Zeichen, Encoding: ISO-8859-15)
2026-04-20 09:03:04 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: leer.txt, Urteil: VALID
2026-04-20 09:03:04 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 0
2026-04-20 09:03:04 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 2
2026-04-20 09:03:04 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: bedien.txt, Urteil: OPERATIONAL_ERROR
2026-04-20 09:03:04 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Datei nicht gefunden: /nicht/vorhanden/datei.txt
2026-04-20 09:03:04 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: fehlerhaft.txt, Urteil: INVALID
2026-04-20 09:03:04 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'inhalt.txt' gelesen (11 Bytes, 11 Zeichen, Encoding: ISO-8859-15)
2026-04-20 09:03:04 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'leer.txt' gelesen (0 Bytes, 0 Zeichen, Encoding: ISO-8859-15)
2026-04-20 09:07:10 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: leer.txt, Urteil: VALID
2026-04-20 09:07:10 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 0
2026-04-20 09:07:10 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 2
2026-04-20 09:07:10 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: bedien.txt, Urteil: OPERATIONAL_ERROR
2026-04-20 09:07:10 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Datei nicht gefunden: /nicht/vorhanden/datei.txt
2026-04-20 09:07:10 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: fehlerhaft.txt, Urteil: INVALID
2026-04-20 09:07:10 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'inhalt.txt' gelesen (11 Bytes, 11 Zeichen, Encoding: ISO-8859-15)
2026-04-20 09:07:10 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'leer.txt' gelesen (0 Bytes, 0 Zeichen, Encoding: ISO-8859-15)
2026-04-20 09:09:15 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: leer.txt, Urteil: VALID
2026-04-20 09:09:15 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 0
2026-04-20 09:09:15 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 2
2026-04-20 09:09:15 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: bedien.txt, Urteil: OPERATIONAL_ERROR
2026-04-20 09:09:15 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Datei nicht gefunden: /nicht/vorhanden/datei.txt
2026-04-20 09:09:15 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: fehlerhaft.txt, Urteil: INVALID
2026-04-20 09:09:15 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'inhalt.txt' gelesen (11 Bytes, 11 Zeichen, Encoding: ISO-8859-15)
2026-04-20 09:09:15 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'leer.txt' gelesen (0 Bytes, 0 Zeichen, Encoding: ISO-8859-15)
+14
View File
@@ -0,0 +1,14 @@
================================================================
ASV-Format-Validator Prüfbericht
================================================================
Zeitstempel : 2026-04-20T07:41:22.3183814Z
Eingabedatei: nicht-vorhanden.txt
Urteil : BEDIENFEHLER
----------------------------------------------------------------
Befunde (1):
[ERROR] [SPEC] [ARTIFACT] Regel=OPERATIONAL-FILE-NOT-FOUND Fehler: Datei nicht gefunden: nicht-vorhanden.txt
----------------------------------------------------------------
Hinweis: Dieser Bericht wurde mit dem M1-Platzhalter-Validator
erzeugt. Viele Prüfbereiche (Fachmodell, Inhalt, Referenzdaten)
werden erst ab M3 aktiv geprüft.
================================================================
+46 -10
View File
@@ -74,6 +74,14 @@
<version>${mockito.version}</version> <version>${mockito.version}</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!-- ArchUnit: Automatisierte Architekturtests (hexagonale Struktur, Log4j2-Isolation) -->
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.3.0</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>
@@ -115,18 +123,46 @@
</configuration> </configuration>
</plugin> </plugin>
<!-- Ausführbares JAR; Main-Class-Klasse wird in AP06 angelegt --> <!-- Uber-JAR via maven-shade-plugin (ersetzt maven-jar-plugin-Platzhalter aus AP02) -->
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId> <artifactId>maven-shade-plugin</artifactId>
<version>${maven-jar-plugin.version}</version> <version>3.5.2</version>
<configuration> <dependencies>
<archive> <!-- Stellt Log4j2PluginsCacheFileTransformer bereit (nicht im Shade-Plugin selbst enthalten) -->
<manifest> <dependency>
<mainClass>de.gecheckt.asv.bootstrap.Main</mainClass> <groupId>org.apache.logging.log4j</groupId>
</manifest> <artifactId>log4j-transform-maven-shade-plugin-extensions</artifactId>
</archive> <version>0.1.0</version>
</configuration> </dependency>
</dependencies>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>de.gecheckt.asv.bootstrap.Main</mainClass>
</transformer>
<!-- Log4j2-Plugin-Cache korrekt zusammenführen, sonst fehlen Plugins im Uber-JAR -->
<transformer implementation="org.apache.logging.log4j.maven.plugins.shade.transformer.Log4j2PluginCacheFileTransformer"/>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin> </plugin>
<!-- JaCoCo: Coverage-Messung ohne Schwellwerte (Schwellen kommen in M9) --> <!-- JaCoCo: Coverage-Messung ohne Schwellwerte (Schwellen kommen in M9) -->
@@ -1,164 +1,30 @@
package de.gecheckt.asv.adapter.in.cli; package de.gecheckt.asv.adapter.in.cli;
import de.gecheckt.asv.domain.model.InputFile;
import de.gecheckt.asv.adapter.out.parsing.DefaultInputFileParser;
import de.gecheckt.asv.adapter.out.parsing.DefaultSegmentLineTokenizer;
import de.gecheckt.asv.adapter.out.parsing.InputFileParseException;
import de.gecheckt.asv.adapter.out.parsing.InputFileParser;
import de.gecheckt.asv.adapter.out.parsing.SegmentLineTokenizer;
import de.gecheckt.asv.application.DefaultInputFileValidator;
import de.gecheckt.asv.application.InputFileValidator;
import de.gecheckt.asv.application.field.DefaultFieldValidator;
import de.gecheckt.asv.application.field.FieldValidator;
import de.gecheckt.asv.application.model.ValidationResult;
import de.gecheckt.asv.application.structure.DefaultStructureValidator;
import de.gecheckt.asv.application.structure.StructureValidator;
import de.gecheckt.asv.adapter.out.reporting.ValidationResultPrinter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* Haupteinstiegspunkt für die ASV Format Validator CLI-Anwendung. * Ehemaliger Haupteinstiegspunkt des ASV-Format-Validators.
* *
* Diese Anwendung validiert Dateien gegen ein segmentorientiertes Dateiformat. * <p><strong>Veraltet seit AP06.</strong> Die Verantwortlichkeiten wurden aufgeteilt:</p>
* Sie nimmt einen Dateipfad als Kommandozeilenargument entgegen, parst die Datei, * <ul>
* validiert sie und gibt die Ergebnisse auf der Konsole aus. * <li>Bootstrap und Constructor Injection → {@link de.gecheckt.asv.bootstrap.Main}</li>
* <li>CLI-Argument-Verarbeitung und Exit-Code → {@link CliRunner}</li>
* </ul>
*
* <p>Diese Klasse bleibt als leere Hülle erhalten, bis AP09 (Altlogik einfrieren) abgeschlossen
* ist. Sie darf nicht mehr direkt verwendet werden.</p>
*
* @deprecated Ersetzt durch {@link de.gecheckt.asv.bootstrap.Main} und {@link CliRunner} (AP06).
* Wird in AP09 endgültig entfernt.
*/ */
@Deprecated(since = "AP06", forRemoval = true)
public class AsvValidatorApplication { public class AsvValidatorApplication {
private static final int EXIT_CODE_SUCCESS = 0;
private static final int EXIT_CODE_INVALID_ARGUMENTS = 1;
private static final int EXIT_CODE_FILE_ERROR = 2;
private static final int EXIT_CODE_VALIDATION_ERRORS = 3;
private static final Logger logger = LoggerFactory.getLogger(AsvValidatorApplication.class);
private final InputFileParser parser;
private final InputFileValidator validator;
private final ValidationResultPrinter printer;
/** /**
* Konstruktor für einen AsvValidatorApplication mit Standardkomponenten. * Nicht mehr verwenden. Nur noch als Kompatibilitätshülle vorhanden.
*/ *
public AsvValidatorApplication() { * @deprecated Verwende {@link de.gecheckt.asv.bootstrap.Main#main(String[])} stattdessen.
// Initialize all required components
SegmentLineTokenizer tokenizer = new DefaultSegmentLineTokenizer();
this.parser = new DefaultInputFileParser(tokenizer);
StructureValidator structureValidator = new DefaultStructureValidator();
FieldValidator fieldValidator = new DefaultFieldValidator();
this.validator = new DefaultInputFileValidator(structureValidator, fieldValidator);
this.printer = new ValidationResultPrinter();
}
/**
* Konstruktor für einen AsvValidatorApplication mit den bereitgestellten Komponenten.
* Dieser Konstruktor unterstützt Dependency Injection für bessere Testbarkeit.
*
* @param parser der Parser zum Parsen von Eingabedateien
* @param validator der Validator zum Validieren geparster Dateien
* @param printer der Printer zum Anzeigen von Validierungsergebnissen
*/
public AsvValidatorApplication(InputFileParser parser, InputFileValidator validator, ValidationResultPrinter printer) {
this.parser = parser;
this.validator = validator;
this.printer = printer;
}
/**
* Haupteinstiegspunkt für die Anwendung.
*
* @param args Kommandozeilenargumente - erwartet genau ein Argument: den Dateipfad
*/ */
@Deprecated(since = "AP06", forRemoval = true)
public static void main(String[] args) { public static void main(String[] args) {
AsvValidatorApplication app = new AsvValidatorApplication(); de.gecheckt.asv.bootstrap.Main.main(args);
int exitCode = app.run(args);
System.exit(exitCode);
} }
}
/**
* Führt die Anwendung mit den bereitgestellten Argumenten aus.
*
* @param args Kommandozeilenargumente
* @return Exit-Code (0 für Erfolg, ungleich 0 für Fehler)
*/
public int run(String[] args) {
// Validate command line arguments
if (args.length != 1) {
printUsage();
return EXIT_CODE_INVALID_ARGUMENTS;
}
String filePath = args[0];
try {
// Parse the file
InputFile inputFile = parseFile(filePath);
// Validate the parsed file
ValidationResult result = validator.validate(inputFile);
// Output results
printer.printToConsole(result);
// Return appropriate exit code based on validation results
return result.hasErrors() ? EXIT_CODE_VALIDATION_ERRORS : EXIT_CODE_SUCCESS;
} catch (IOException e) {
logger.error("Fehler beim Lesen der Datei: {}", e.getMessage(), e);
System.err.println("Fehler beim Lesen der Datei: " + e.getMessage());
return EXIT_CODE_FILE_ERROR;
} catch (InputFileParseException e) {
logger.error("Fehler beim Parsen der Datei: {}", e.getMessage(), e);
System.err.println("Fehler beim Parsen der Datei: " + e.getMessage());
return EXIT_CODE_FILE_ERROR;
} catch (Exception e) {
logger.error("Unerwarteter Fehler während der Validierung: {}", e.getMessage(), e);
System.err.println("Unerwarteter Fehler während der Validierung: " + e.getMessage());
return EXIT_CODE_FILE_ERROR;
}
}
/**
* Parst eine Datei unter dem gegebenen Pfad.
*
* @param filePath Pfad zur zu parsenden Datei
* @return geparstes InputFile-Objekt
* @throws IOException wenn die Datei nicht gelesen werden kann
* @throws InputFileParseException wenn die Datei nicht geparst werden kann
*/
private InputFile parseFile(String filePath) throws IOException, InputFileParseException {
Path path = Paths.get(filePath);
if (!Files.exists(path)) {
throw new IOException("File does not exist: " + filePath);
}
if (!Files.isRegularFile(path)) {
throw new IOException("Path is not a regular file: " + filePath);
}
if (!Files.isReadable(path)) {
throw new IOException("File is not readable: " + filePath);
}
String fileContent = Files.readString(path, StandardCharsets.UTF_8);
return parser.parse(path.getFileName().toString(), fileContent);
}
/**
* Gibt Nutzungsinformationen auf der Konsole aus.
*/
private void printUsage() {
System.out.println("ASV Format Validator");
System.out.println("Verwendung: java -jar asv-format-validator.jar <datei-pfad>");
System.out.println(" <datei-pfad> Pfad zur zu validierenden Datei");
}
}
@@ -0,0 +1,262 @@
package de.gecheckt.asv.adapter.in.cli;
import de.gecheckt.asv.adapter.out.filesystem.SuffixResolver;
import de.gecheckt.asv.adapter.out.logging.LoggingConfigurator;
import de.gecheckt.asv.adapter.out.reporting.ReportFileWriter;
import de.gecheckt.asv.application.FileValidationService;
import de.gecheckt.asv.domain.finding.ValidationReport;
import de.gecheckt.asv.domain.finding.Verdict;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
/**
* Eingehender CLI-Adapter. Nimmt Kommandozeilenargumente entgegen, prüft die
* Eingabedatei auf Existenz und Lesbarkeit, konfiguriert die Log-Datei, delegiert
* die Validierung an den {@link FileValidationService}, schreibt die Berichtdatei und
* gibt das Ergebnis auf der Konsole aus. Übersetzt das Ergebnis in einen numerischen
* Exit-Code gemäß {@link ExitCode}.
*
* <p>Reihenfolge pro Lauf (AP07/AP08):</p>
* <ol>
* <li>Argument-Prüfung und Datei-Vorabprüfung</li>
* <li>Bei Bedienfehler: Minimalbericht erzeugen und ggf. Berichtdatei schreiben</li>
* <li>Log-Datei-Pfad via {@link SuffixResolver} bestimmen</li>
* <li>{@link LoggingConfigurator#configureLogFile(Path)} aufrufen</li>
* <li>Validierungslauf über {@link FileValidationService#validate(Path)}</li>
* <li>Berichtdatei über {@link ReportFileWriter#write(ValidationReport, Path)} schreiben</li>
* <li>Berichtinhalt auf der Konsole ausgeben</li>
* </ol>
*
* <p>Dieser Adapter enthält keinerlei Log4j2-Typen. Logging erfolgt ausschließlich
* über die SLF4J-Fassade. Die Log4j2-Umkonfiguration delegiert er an
* {@link LoggingConfigurator}, der im {@code adapter.out.logging}-Paket liegt.</p>
*
* <p><strong>Bedienfehler-Fälle (AP08):</strong></p>
* <ul>
* <li>Kein Argument → nur Konsole (Verzeichnis unbekannt)</li>
* <li>Mehr als ein Argument → nur Konsole</li>
* <li>Eingabedatei existiert nicht → Konsole + Berichtdatei im übergeordneten Verzeichnis</li>
* <li>Pfad ist kein regulärer Dateityp → nur Konsole</li>
* <li>Datei nicht lesbar → Konsole + Berichtdatei im übergeordneten Verzeichnis</li>
* </ul>
*/
public class CliRunner {
private static final Logger log = LoggerFactory.getLogger(CliRunner.class);
/** Platzhalter-Dateiname für Fälle ohne auflösbaren Dateinamen. */
private static final String PLACEHOLDER_NO_ARG = "<kein Argument>";
private static final String PLACEHOLDER_MANY_ARGS = "<mehrere Argumente>";
private final FileValidationService validationService;
private final LoggingConfigurator loggingConfigurator;
private final SuffixResolver suffixResolver;
private final ReportFileWriter reportFileWriter;
/**
* Erzeugt einen neuen {@code CliRunner} mit allen benötigten Adaptern.
*
* @param validationService Dienst, der die Dateivalidierung übernimmt (nicht null)
* @param loggingConfigurator Konfiguriert den Log-Datei-Pfad (nicht null)
* @param suffixResolver Ermittelt den freien Log-Datei-Pfad (nicht null)
* @param reportFileWriter Schreibt die Berichtdatei (nicht null)
*/
public CliRunner(FileValidationService validationService,
LoggingConfigurator loggingConfigurator,
SuffixResolver suffixResolver,
ReportFileWriter reportFileWriter) {
this.validationService = Objects.requireNonNull(validationService,
"validationService darf nicht null sein");
this.loggingConfigurator = Objects.requireNonNull(loggingConfigurator,
"loggingConfigurator darf nicht null sein");
this.suffixResolver = Objects.requireNonNull(suffixResolver,
"suffixResolver darf nicht null sein");
this.reportFileWriter = Objects.requireNonNull(reportFileWriter,
"reportFileWriter darf nicht null sein");
}
/**
* Führt den CLI-Lauf mit den übergebenen Argumenten durch.
*
* <p>Genau ein Positionsargument wird erwartet: der Pfad zur Eingabedatei.
* Bei 0 oder ≥ 2 Argumenten wird Exit-Code {@link ExitCode#OPERATIONAL_ERROR} (2)
* zurückgegeben und ein Minimalbericht auf STDERR und (wo möglich) als Datei ausgegeben.</p>
*
* <p>IO-Fehler beim Schreiben der Berichtdatei verhindern die Konsolenausgabe nicht.
* Das Ergebnis wird in jedem Fall auf die Konsole geschrieben.</p>
*
* @param args Kommandozeilenargumente
* @return Exit-Code: {@link ExitCode#VALID} (0), {@link ExitCode#INVALID} (1)
* oder {@link ExitCode#OPERATIONAL_ERROR} (2)
*/
public int run(String[] args) {
// --- Fall 1: Kein Argument ---
if (args.length == 0) {
String msg = "Fehler: Kein Dateipfad angegeben. Verwendung: java -jar asv-format-validator.jar <datei-pfad>";
log.error("Bedienfehler: Kein Argument übergeben.");
ValidationReport report = ValidationReport.operationalError(
PLACEHOLDER_NO_ARG, "OPERATIONAL-MISSING-ARG", msg);
// Kein Verzeichnis bekannt → nur Konsole
writeMinimalReportToConsoleOnly(report);
return ExitCode.OPERATIONAL_ERROR;
}
// --- Fall 2: Mehr als ein Argument ---
if (args.length > 1) {
String msg = "Fehler: Zu viele Argumente. Es wird genau ein Dateipfad erwartet.";
log.error("Bedienfehler: Zu viele Argumente ({}). ", args.length);
ValidationReport report = ValidationReport.operationalError(
PLACEHOLDER_MANY_ARGS, "OPERATIONAL-TOO-MANY-ARGS", msg);
// Kein eindeutiges Verzeichnis → nur Konsole
writeMinimalReportToConsoleOnly(report);
return ExitCode.OPERATIONAL_ERROR;
}
String filePath = args[0];
// Pfad parsen
Path path;
try {
path = Paths.get(filePath);
} catch (InvalidPathException e) {
String msg = "Fehler: Ungültiger Dateipfad: " + filePath;
log.error("Bedienfehler: Ungültiger Dateipfad: {}", filePath);
ValidationReport report = ValidationReport.operationalError(
filePath, "OPERATIONAL-FILE-NOT-FOUND", msg);
// Ungültiger Pfad → kein Verzeichnis ableitbar → nur Konsole
writeMinimalReportToConsoleOnly(report);
return ExitCode.OPERATIONAL_ERROR;
}
// --- Fall 3: Datei existiert nicht ---
if (!Files.exists(path)) {
String msg = "Fehler: Datei nicht gefunden: " + filePath;
log.error("Bedienfehler: Datei nicht gefunden: {}", filePath);
String fileBaseName = path.getFileName() != null ? path.getFileName().toString() : "bedienfehler";
ValidationReport report = ValidationReport.operationalError(
fileBaseName, "OPERATIONAL-FILE-NOT-FOUND", msg);
// Übergeordnetes Verzeichnis ableiten und Bericht schreiben, sofern schreibbar
Path parent = path.toAbsolutePath().getParent();
writeMinimalReportWithOptionalFile(report, parent, fileBaseName);
return ExitCode.OPERATIONAL_ERROR;
}
// --- Fall 4: Kein regulärer Dateityp ---
if (!Files.isRegularFile(path)) {
String msg = "Fehler: Pfad ist keine reguläre Datei (z.B. Verzeichnis): " + filePath;
log.error("Bedienfehler: Pfad ist keine reguläre Datei: {}", filePath);
String fileBaseName = path.getFileName() != null ? path.getFileName().toString() : filePath;
ValidationReport report = ValidationReport.operationalError(
fileBaseName, "OPERATIONAL-NOT-REGULAR", msg);
// Kein Minimalbericht als Datei (nur Konsole), da unklar ob Verz. schreibbar
writeMinimalReportToConsoleOnly(report);
return ExitCode.OPERATIONAL_ERROR;
}
// --- Fall 5: Datei nicht lesbar ---
if (!Files.isReadable(path)) {
String msg = "Fehler: Datei ist nicht lesbar (fehlende Leseberechtigung): " + filePath;
log.error("Bedienfehler: Datei nicht lesbar: {}", filePath);
String fileBaseName = path.getFileName() != null ? path.getFileName().toString() : filePath;
ValidationReport report = ValidationReport.operationalError(
fileBaseName, "OPERATIONAL-NOT-READABLE", msg);
// Übergeordnetes Verzeichnis ableiten und Bericht schreiben, sofern schreibbar
Path parent = path.toAbsolutePath().getParent();
writeMinimalReportWithOptionalFile(report, parent, fileBaseName);
return ExitCode.OPERATIONAL_ERROR;
}
// --- Normaler Validierungslauf ---
// Log-Datei bestimmen und Logging umkonfigurieren (AP07)
String baseName = path.getFileName().toString();
Path directory = path.toAbsolutePath().getParent();
if (directory == null) {
directory = Path.of(".");
}
Path logPath = suffixResolver.resolveNextFreePath(directory, baseName, "log");
loggingConfigurator.configureLogFile(logPath);
log.info("ASV-Format-Validator gestartet. Eingabedatei: {}", path.toAbsolutePath());
// Validierung delegieren
ValidationReport report = validationService.validate(path);
Verdict verdict = report.computeVerdict();
log.info("Validierung abgeschlossen. Datei: {}, Urteil: {}", path.getFileName(), verdict);
// Berichtdatei schreiben (AP07)
ReportFileWriter.ReportWriteResult writeResult = reportFileWriter.write(report, path);
if (!writeResult.isSuccess()) {
log.error("Berichtdatei konnte nicht geschrieben werden: {}",
writeResult.writeException() != null
? writeResult.writeException().getMessage() : "unbekannter Fehler");
}
// Konsolenausgabe (immer, auch bei Schreibfehler der Datei)
System.out.print(writeResult.reportContent());
return switch (verdict) {
case VALID -> ExitCode.VALID;
case INVALID -> ExitCode.INVALID;
case OPERATIONAL_ERROR -> ExitCode.OPERATIONAL_ERROR;
};
}
/**
* Gibt den Minimalbericht ausschließlich auf der Konsole (STDERR) aus.
* Wird verwendet, wenn kein Zielverzeichnis bekannt oder sinnvoll ableitbar ist.
*
* @param report der Bedienfehler-Bericht (nicht null)
*/
private void writeMinimalReportToConsoleOnly(ValidationReport report) {
String content = reportFileWriter.buildMinimalReportContent(report);
System.err.print(content);
}
/**
* Gibt den Minimalbericht auf der Konsole (STDERR) aus und versucht zusätzlich,
* ihn als Datei in das angegebene Verzeichnis zu schreiben.
*
* <p>Ist das Verzeichnis nicht vorhanden oder nicht schreibbar, wird nur eine
* Hinweiszeile auf STDERR ausgegeben — kein Fehler auf Fehler.</p>
*
* @param report der Bedienfehler-Bericht (nicht null)
* @param directory das Zielverzeichnis; kann {@code null} sein
* @param baseName Basisname für die Berichtdatei
*/
private void writeMinimalReportWithOptionalFile(ValidationReport report,
Path directory,
String baseName) {
String content = reportFileWriter.buildMinimalReportContent(report);
System.err.print(content);
if (directory == null || !Files.isDirectory(directory) || !Files.isWritable(directory)) {
System.err.println("Bericht konnte nicht in das Verzeichnis geschrieben werden.");
log.warn("Bedienfehler-Bericht konnte nicht als Datei geschrieben werden: " +
"Verzeichnis nicht vorhanden oder nicht schreibbar: {}", directory);
return;
}
ReportFileWriter.ReportWriteResult result =
reportFileWriter.writeOperationalError(report, directory, baseName);
if (result.isSuccess()) {
log.info("Bedienfehler-Bericht geschrieben: {}", result.reportPath());
System.err.println("Bericht geschrieben: " + result.reportPath());
} else {
System.err.println("Bericht konnte nicht in das Verzeichnis geschrieben werden.");
log.warn("Bedienfehler-Bericht konnte nicht geschrieben werden: {}",
result.writeException() != null
? result.writeException().getMessage() : "unbekannter Fehler");
}
}
}
@@ -0,0 +1,32 @@
package de.gecheckt.asv.adapter.in.cli;
/**
* Normative Exit-Codes der ASV-Format-Validator-CLI.
*
* <p>Die drei zulässigen Exit-Codes sind gemäß Technischer Anlage ASV 1.09 und
* {@code docs/specs/technik-und-architektur.md} definiert:</p>
* <ul>
* <li>{@link #VALID} (0) — Datei ist spec-konform, keine SPEC-ERROR-Befunde</li>
* <li>{@link #INVALID} (1) — Datei enthält mindestens einen SPEC-ERROR-Befund</li>
* <li>{@link #OPERATIONAL_ERROR} (2) — Bedienfehler (fehlendes Argument, nicht lesbare Datei)</li>
* </ul>
*
* <p>Exit-Code 3 existiert nicht mehr. Die früheren Konstanten
* {@code EXIT_CODE_INVALID_ARGUMENTS}, {@code EXIT_CODE_FILE_ERROR} und
* {@code EXIT_CODE_VALIDATION_ERRORS} wurden mit AP06 entfernt.</p>
*/
public final class ExitCode {
/** Datei ist gültig (keine SPEC-ERROR-Befunde). */
public static final int VALID = 0;
/** Datei enthält mindestens einen SPEC-ERROR-Befund. */
public static final int INVALID = 1;
/** Bedienfehler: falsches Argument, nicht lesbare Datei o. Ä. */
public static final int OPERATIONAL_ERROR = 2;
private ExitCode() {
// Nicht instanziierbar
}
}
@@ -0,0 +1,87 @@
package de.gecheckt.asv.adapter.out.filesystem;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* Ermittelt den nächsten freien Dateipfad im Zielverzeichnis anhand von Basisname und Extension.
*
* <p>Die Suffix-Logik arbeitet extension-unabhängig: Für {@code .txt} und {@code .log} werden
* separate Zähler geführt. Beim ersten Lauf entsteht {@code <baseName>.<ext>}; bei jedem
* weiteren Lauf wird {@code <baseName>_v1.<ext>}, {@code <baseName>_v2.<ext>} usw. erzeugt,
* bis ein freier Pfad gefunden ist.</p>
*
* <p>Dieses Objekt ist zustandslos. Alle Methoden können nebenläufig auf verschiedenen
* Eingabedatei-Pfaden verwendet werden; Race Conditions bei gleichzeitigen Läufen auf
* derselben Eingabedatei sind in V1 bewusst nicht behandelt.</p>
*/
public class SuffixResolver {
/**
* Ermittelt den ersten freien Dateipfad für den gegebenen Basisnamen und die gegebene
* Extension im Zielverzeichnis.
*
* <p>Probiert in dieser Reihenfolge:</p>
* <ol>
* <li>{@code <baseName>.<extension>}</li>
* <li>{@code <baseName>_v1.<extension>}</li>
* <li>{@code <baseName>_v2.<extension>}</li>
* <li>… bis ein freier Pfad gefunden ist</li>
* </ol>
*
* <p>Die Zählung ist pro Extension unabhängig: Eine vorhandene {@code foo.auf.txt} hat
* keinen Einfluss auf die Zählung für {@code foo.auf.log}.</p>
*
* @param directory das Zielverzeichnis (muss existieren)
* @param baseName Basisname ohne Extension (z.B. {@code "foo.auf"})
* @param extension Extension ohne führenden Punkt (z.B. {@code "txt"})
* @return der erste freie Pfad (existiert noch nicht im Dateisystem)
* @throws IllegalArgumentException wenn {@code directory}, {@code baseName} oder
* {@code extension} null oder leer sind
* @throws UncheckedIOException wenn der Dateisystem-Zugriff fehlschlägt
*/
public Path resolveNextFreePath(Path directory, String baseName, String extension) {
if (directory == null) {
throw new IllegalArgumentException("directory darf nicht null sein");
}
if (baseName == null || baseName.isBlank()) {
throw new IllegalArgumentException("baseName darf nicht null oder leer sein");
}
if (extension == null || extension.isBlank()) {
throw new IllegalArgumentException("extension darf nicht null oder leer sein");
}
// Kandidat ohne Suffix: <baseName>.<ext>
Path candidate = directory.resolve(baseName + "." + extension);
if (!exists(candidate)) {
return candidate;
}
// Mit Suffix: <baseName>_v1.<ext>, <baseName>_v2.<ext>, ...
int counter = 1;
while (true) {
candidate = directory.resolve(baseName + "_v" + counter + "." + extension);
if (!exists(candidate)) {
return candidate;
}
counter++;
}
}
/**
* Prüft, ob der Pfad im Dateisystem existiert.
*
* @param path der zu prüfende Pfad
* @return {@code true} wenn die Datei existiert
*/
private boolean exists(Path path) {
try {
return Files.exists(path);
} catch (SecurityException e) {
throw new UncheckedIOException(
new IOException("Dateisystem-Zugriff verweigert: " + path, e));
}
}
}
@@ -1,21 +1,86 @@
package de.gecheckt.asv.adapter.out.logging; package de.gecheckt.asv.adapter.out.logging;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.FileAppender;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.apache.logging.log4j.core.layout.PatternLayout;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Map;
import java.util.Objects;
/** /**
* Konfiguriert den Log4j2-Logging-Adapter programmatisch. * Konfiguriert den Log4j2-Logging-Adapter programmatisch.
* Erlaubt es, den Zielpfad der Log-Datei zur Laufzeit zu setzen. *
* Log4j2-Typen dürfen in diesem Paket direkt verwendet werden. * <p>Erlaubt es, den Zielpfad der Log-Datei zur Laufzeit zu setzen, bevor der erste
* fachliche Log-Aufruf erfolgt. Log4j2-Typen dürfen ausschließlich in diesem Paket
* ({@code adapter.out.logging}) und in {@code bootstrap} sichtbar sein.</p>
*
* <p>Implementierungsansatz: programmatische Umkonfiguration über die Log4j2 Configurator-API.
* Ein neuer {@link FileAppender} wird erzeugt und dem Root-Logger sowie dem
* {@code de.gecheckt.asv}-Logger hinzugefügt. Der statische Fallback-Appender aus
* {@code log4j2.xml} (Datei {@code logs/asv-format-validator.log}) bleibt als Fallback
* bestehen, wenn diese Methode nicht aufgerufen wird (z.B. in reinen Unit-Tests).</p>
*/ */
public class LoggingConfigurator { public class LoggingConfigurator {
private static final String PATTERN =
"%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n";
/** /**
* Setzt den Zielpfad der Log-Datei für diesen Lauf. * Setzt den Zielpfad der Log-Datei für diesen Lauf und konfiguriert Log4j2
* Die tatsächliche dynamische Umleitung wird in AP07 implementiert. * programmatisch um.
* *
* @param logFile Pfad zur Log-Datei * <p>Diese Methode muss <strong>vor</strong> dem ersten fachlichen Log-Aufruf
* aufgerufen werden. Nach dem Aufruf gehen alle Log-Nachrichten sowohl in die
* Konsole (STDERR, über den bestehenden Console-Appender) als auch in die
* angegebene Datei.</p>
*
* <p>Schlägt die Umkonfiguration fehl (z.B. wegen eines SecurityManagers oder
* inkompatiblem Log4j2-Zustand), wird ein Fallback-Warnung auf STDERR ausgegeben;
* der laufende Betrieb wird nicht unterbrochen — Logs gehen dann nur in den
* Fallback-Appender aus {@code log4j2.xml}.</p>
*
* @param logFile Zielpfad der Log-Datei (nicht null)
* @throws IllegalArgumentException wenn {@code logFile} null ist
*/ */
public void configureLogFile(Path logFile) { public void configureLogFile(Path logFile) {
// TODO: dynamische Log-Datei-Umleitung in AP07 Objects.requireNonNull(logFile, "logFile darf nicht null sein");
try {
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
Configuration config = ctx.getConfiguration();
PatternLayout layout = PatternLayout.newBuilder()
.withPattern(PATTERN)
.withConfiguration(config) // withConfiguration ist in PatternLayout.Builder nicht deprecated
.build();
FileAppender fileAppender = FileAppender.newBuilder()
.withFileName(logFile.toAbsolutePath().toString())
.withAppend(false)
.setName("DynamicFile")
.setLayout(layout)
.setConfiguration(config)
.build();
fileAppender.start();
config.addAppender(fileAppender);
// Appender dem de.gecheckt.asv-Logger hinzufügen (falls vorhanden)
Map<String, LoggerConfig> loggers = config.getLoggers();
for (LoggerConfig loggerConfig : loggers.values()) {
loggerConfig.addAppender(fileAppender, null, null);
}
ctx.updateLoggers();
} catch (Exception e) {
System.err.println("[LoggingConfigurator] Warnung: Programmatische Log4j2-Umkonfiguration"
+ " fehlgeschlagen. Logs gehen nur in den Fallback-Appender. Ursache: "
+ e.getMessage());
}
} }
} }
@@ -0,0 +1,258 @@
package de.gecheckt.asv.adapter.out.reporting;
import de.gecheckt.asv.adapter.out.filesystem.SuffixResolver;
import de.gecheckt.asv.domain.finding.Finding;
import de.gecheckt.asv.domain.finding.ValidationReport;
import de.gecheckt.asv.domain.finding.Verdict;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Objects;
/**
* Schreibt den Validierungsbericht in eine UTF-8-Textdatei im Verzeichnis der Eingabedatei.
*
* <p>Der Dateiname wird über {@link SuffixResolver} bestimmt: Basisname ist der vollständige
* Dateiname der Eingabedatei inklusive Extension (z.B. {@code foo.auf}); die Ausgabedatei
* heißt dann {@code foo.auf.txt}, beim zweiten Lauf {@code foo.auf_v1.txt} usw.</p>
*
* <p>Der Bericht ist für M1 absichtlich minimal strukturiert und wird in M9 durch die
* finale hierarchische Gliederung ersetzt. Alle Texte auf Deutsch, Encoding explizit UTF-8.</p>
*
* <p>Dieses Objekt enthält keinerlei Log4j2-Typen — Logging erfolgt ausschließlich über
* die SLF4J-Fassade.</p>
*/
public class ReportFileWriter {
private static final Logger log = LoggerFactory.getLogger(ReportFileWriter.class);
private static final DateTimeFormatter TIMESTAMP_FORMATTER =
DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneOffset.UTC);
private final SuffixResolver suffixResolver;
/**
* Erzeugt einen neuen {@code ReportFileWriter} mit dem angegebenen {@link SuffixResolver}.
*
* @param suffixResolver Resolver für den nächsten freien Dateipfad (nicht null)
*/
public ReportFileWriter(SuffixResolver suffixResolver) {
this.suffixResolver = Objects.requireNonNull(suffixResolver,
"suffixResolver darf nicht null sein");
}
/**
* Schreibt den Validierungsbericht als UTF-8-Textdatei in das Verzeichnis der Eingabedatei.
*
* <p>Gibt den Inhalt der Berichtdatei als String zurück, damit der Aufrufer ihn
* identisch auf der Konsole ausgeben kann. Ist das Schreiben der Datei nicht möglich,
* wird die Ausnahme protokolliert und die Methode gibt einen Fehlerbericht-String zurück —
* der Aufrufer kann die Konsolenausgabe trotzdem ausgeben.</p>
*
* @param report der Validierungsbericht (nicht null)
* @param inputFilePath Pfad zur Eingabedatei; bestimmt Verzeichnis und Basisnamen (nicht null)
* @return Pfad zur erzeugten Berichtdatei; {@code null} wenn die Datei nicht geschrieben werden konnte
* @throws IllegalArgumentException wenn {@code report} oder {@code inputFilePath} null sind
*/
public ReportWriteResult write(ValidationReport report, Path inputFilePath) {
if (report == null) {
throw new IllegalArgumentException("report darf nicht null sein");
}
if (inputFilePath == null) {
throw new IllegalArgumentException("inputFilePath darf nicht null sein");
}
// Basisname ist der vollständige Dateiname der Eingabedatei inkl. Extension
String baseName = inputFilePath.getFileName().toString();
Path directory = inputFilePath.toAbsolutePath().getParent();
if (directory == null) {
directory = Path.of(".");
}
// Berichtinhalt aufbauen
String content = buildReportContent(report, inputFilePath);
// Zieldatei bestimmen
Path reportPath = suffixResolver.resolveNextFreePath(directory, baseName, "txt");
// Schreiben
try (Writer writer = Files.newBufferedWriter(reportPath, StandardCharsets.UTF_8)) {
writer.write(content);
log.info("Berichtdatei geschrieben: {}", reportPath);
} catch (IOException e) {
log.error("Fehler beim Schreiben der Berichtdatei {}: {}", reportPath, e.getMessage());
return new ReportWriteResult(content, null, e);
}
return new ReportWriteResult(content, reportPath, null);
}
/**
* Schreibt einen Bedienfehler-Bericht als UTF-8-Textdatei in das angegebene Verzeichnis.
*
* <p>Im Gegensatz zu {@link #write(ValidationReport, Path)} ist diese Methode für den
* Fall gedacht, dass keine Eingabedatei existiert (z.B. Datei nicht gefunden). Sie nimmt
* das Verzeichnis und den Basisnamen direkt entgegen.</p>
*
* <p>IO-Fehler führen <em>nicht</em> zu einer {@link RuntimeException}, sondern werden
* protokolliert. Das Ergebnis signalisiert den Fehler über {@link ReportWriteResult}.</p>
*
* @param report der Bedienfehler-Bericht (nicht null)
* @param directory das Zielverzeichnis (nicht null, muss existieren und schreibbar sein)
* @param baseName Basisname für die Berichtdatei (nicht null, nicht leer)
* @return Schreibergebnis; {@link ReportWriteResult#isSuccess()} gibt an, ob erfolgreich
*/
public ReportWriteResult writeOperationalError(ValidationReport report,
Path directory,
String baseName) {
Objects.requireNonNull(report, "report darf nicht null sein");
Objects.requireNonNull(directory, "directory darf nicht null sein");
if (baseName == null || baseName.isBlank()) {
throw new IllegalArgumentException("baseName darf nicht null oder leer sein");
}
String content = buildMinimalReportContent(report);
Path reportPath = suffixResolver.resolveNextFreePath(directory, baseName, "txt");
try (java.io.Writer writer = java.nio.file.Files.newBufferedWriter(
reportPath, java.nio.charset.StandardCharsets.UTF_8)) {
writer.write(content);
log.info("Bedienfehler-Bericht geschrieben: {}", reportPath);
} catch (IOException e) {
log.error("Fehler beim Schreiben des Bedienfehler-Berichts {}: {}", reportPath, e.getMessage());
return new ReportWriteResult(content, null, e);
}
return new ReportWriteResult(content, reportPath, null);
}
/**
* Erstellt den Berichtinhalt für einen Bedienfehler-Bericht als formatierten String.
*
* <p>Wird von {@link de.gecheckt.asv.adapter.in.cli.CliRunner} direkt verwendet, wenn
* der Inhalt für die Konsolenausgabe benötigt wird, ohne eine Datei zu schreiben.
* Im Unterschied zu {@link #buildReportContent(ValidationReport, Path)} wird der
* Dateiname aus dem Bericht direkt als String verwendet — kein {@link Path}-Parsing,
* sodass auch Platzhalter wie {@code <kein Argument>} mit Sonderzeichen funktionieren.</p>
*
* @param report der Bedienfehler-Bericht (nicht null)
* @return der vollständige Berichtinhalt als String
*/
public String buildMinimalReportContent(ValidationReport report) {
Objects.requireNonNull(report, "report darf nicht null sein");
return buildReportContentWithFileName(report, report.getFileName());
}
/**
* Erstellt den Berichtinhalt als formatierten String (delegiert an die String-Variante).
*
* @param report der Validierungsbericht
* @param inputFilePath Pfad zur Eingabedatei
* @return der vollständige Berichtinhalt
*/
String buildReportContent(ValidationReport report, Path inputFilePath) {
return buildReportContentWithFileName(report, inputFilePath.toAbsolutePath().toString());
}
/**
* Erstellt den Berichtinhalt als formatierten String mit dem Dateinamen als String.
*
* <p>Diese Variante wird für Bedienfehler-Berichte verwendet, bei denen der Dateiname
* ein Platzhalter wie {@code <kein Argument>} sein kann, der kein gültiger Pfad ist.</p>
*
* @param report der Validierungsbericht
* @param fileNameDisplay der anzuzeigende Dateiname (als String, nicht als Pfad)
* @return der vollständige Berichtinhalt
*/
private String buildReportContentWithFileName(ValidationReport report, String fileNameDisplay) {
StringBuilder sb = new StringBuilder();
Verdict verdict = report.computeVerdict();
// Kopfzeile
sb.append("================================================================\n");
sb.append("ASV-Format-Validator Prüfbericht\n");
sb.append("================================================================\n");
sb.append("Zeitstempel : ").append(TIMESTAMP_FORMATTER.format(report.getTimestamp())).append("\n");
sb.append("Eingabedatei: ").append(fileNameDisplay).append("\n");
sb.append("Urteil : ").append(verdictText(verdict)).append("\n");
sb.append("----------------------------------------------------------------\n");
// Befunde
List<Finding> findings = report.getFindings();
if (findings.isEmpty()) {
sb.append("Keine Befunde.\n");
} else {
sb.append("Befunde (").append(findings.size()).append("):\n");
for (Finding f : findings) {
sb.append(" [")
.append(f.severity())
.append("] [")
.append(f.kind())
.append("] [")
.append(f.layer())
.append("]");
if (f.ruleId() != null) {
sb.append(" Regel=").append(f.ruleId());
}
if (f.fieldId() != null) {
sb.append(" Feld=").append(f.fieldId());
}
sb.append(" ").append(f.germanMessage()).append("\n");
}
}
// Fußzeile
sb.append("----------------------------------------------------------------\n");
sb.append("Hinweis: Dieser Bericht wurde mit dem M1-Platzhalter-Validator\n");
sb.append("erzeugt. Viele Prüfbereiche (Fachmodell, Inhalt, Referenzdaten)\n");
sb.append("werden erst ab M3 aktiv geprüft.\n");
sb.append("================================================================\n");
return sb.toString();
}
/**
* Gibt den deutschen Urteil-Text für das übergebene Verdict zurück.
*
* @param verdict das Prüfurteil
* @return deutschsprachige Urteilsbezeichnung
*/
private static String verdictText(Verdict verdict) {
return switch (verdict) {
case VALID -> "GÜLTIG";
case INVALID -> "UNGÜLTIG";
case OPERATIONAL_ERROR -> "BEDIENFEHLER";
};
}
/**
* Ergebnis eines Schreibvorgangs.
*
* @param reportContent der erzeugte Berichtinhalt als String (niemals null)
* @param reportPath Pfad zur erzeugten Datei; {@code null} bei Schreibfehler
* @param writeException aufgetretene Ausnahme beim Schreiben; {@code null} bei Erfolg
*/
public record ReportWriteResult(
String reportContent,
Path reportPath,
IOException writeException
) {
/**
* Gibt an, ob die Datei erfolgreich geschrieben wurde.
*
* @return {@code true} wenn {@link #reportPath()} nicht null ist
*/
public boolean isSuccess() {
return reportPath != null;
}
}
}
@@ -0,0 +1,54 @@
package de.gecheckt.asv.application;
import de.gecheckt.asv.domain.finding.ValidationReport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.List;
/**
* M1-Platzhalter-Implementierung des {@link FileValidationService}.
*
* <p>Liest die Eingabedatei mit dem normativen Eingabe-Encoding <strong>ISO-8859-15</strong>
* ein, zählt die gelesenen Bytes und gibt einen leeren {@link ValidationReport} zurück.
* <em>Keine echte Validierung wird in M1 durchgeführt.</em></p>
*
* <p>Diese Klasse wird in M3 durch eine echte Implementierung ersetzt, die den
* vollständigen Parser- und Validator-Pfad aktiviert.</p>
*/
public class DummyFileValidationService implements FileValidationService {
/** Normatives Eingabe-Encoding gemäß Technischer Anlage ASV 1.09. */
static final Charset INPUT_CHARSET = Charset.forName("ISO-8859-15");
private static final Logger log = LoggerFactory.getLogger(DummyFileValidationService.class);
/**
* Liest die Datei mit ISO-8859-15 ein, zählt die Bytes und gibt einen leeren
* Validierungsbericht zurück.
*
* @param inputFile Pfad zur Eingabedatei (nicht null, vorab als existent/lesbar geprüft)
* @return leerer {@link ValidationReport} mit Dateiname und aktuellem Zeitstempel
*/
@Override
public ValidationReport validate(Path inputFile) {
String fileName = inputFile.getFileName().toString();
try {
byte[] rawBytes = Files.readAllBytes(inputFile);
String content = new String(rawBytes, INPUT_CHARSET);
log.info("M1-Dummy: Datei '{}' gelesen ({} Bytes, {} Zeichen, Encoding: ISO-8859-15)",
fileName, rawBytes.length, content.length());
} catch (IOException e) {
log.warn("M1-Dummy: Lesefehler bei '{}': {}", fileName, e.getMessage());
}
return new ValidationReport(fileName, Instant.now(), List.of());
}
}
@@ -0,0 +1,29 @@
package de.gecheckt.asv.application;
import de.gecheckt.asv.domain.finding.ValidationReport;
import java.nio.file.Path;
/**
* Anwendungsschnittstelle für die Dateivalidierung.
*
* <p>Nimmt einen bereits vorab geprüften (existierenden, regulären, lesbaren) Dateipfad
* entgegen und gibt einen {@link ValidationReport} zurück.</p>
*
* <p>In M1 liefert die Standardimplementierung ({@code DummyFileValidationService}) einen
* leeren Bericht. Echte Parser- und Validator-Einbindung folgt ab M3.</p>
*/
public interface FileValidationService {
/**
* Validiert die Datei unter dem angegebenen Pfad.
*
* <p>Der Pfad wird als vorab geprüft betrachtet (existent, reguläre Datei, lesbar).
* Die Implementierung muss stets einen nicht-{@code null}-{@link ValidationReport}
* zurückgeben.</p>
*
* @param inputFile Pfad zur zu validierenden Eingabedatei (nicht null)
* @return Validierungsbericht (nicht null)
*/
ValidationReport validate(Path inputFile);
}
@@ -10,13 +10,21 @@ import de.gecheckt.asv.application.model.ValidationResult;
import de.gecheckt.asv.application.model.ValidationSeverity; import de.gecheckt.asv.application.model.ValidationSeverity;
/** /**
* Default implementation of FieldValidator that checks general field rules. * M3-Vorbau. In M1 bewusst nicht im produktiven Lauf verdrahtet.
* * Wird ab M3 wieder aktiviert und gegen die finalen Regelklassifikationen
* Rules checked: * (V1-V/T/N/K) aus fachliche-anforderungen.md bewertet.
* 1. Field.rawValue must not be empty *
* 2. Field.rawValue must not consist only of whitespaces * @see <a href="docs/arbeitspakete/m1/E00-entscheidungsprotokoll.md">E-01</a>
* 3. Field positions within a segment should be consecutive without gaps, starting at 1 *
* 4. If fieldName is set, it must not be empty or only whitespace * <p>Standardimplementierung des FieldValidator, die allgemeine Feldregeln prüft.</p>
*
* <p>Geprüfte Regeln:</p>
* <ol>
* <li>Field.rawValue darf nicht leer sein</li>
* <li>Field.rawValue darf nicht nur aus Leerzeichen bestehen</li>
* <li>Feldpositionen innerhalb eines Segments sollten lückenlos aufeinanderfolgen, beginnend bei 1</li>
* <li>Wenn fieldName gesetzt ist, darf er nicht leer oder nur aus Leerzeichen bestehen</li>
* </ol>
*/ */
public class DefaultFieldValidator implements FieldValidator { public class DefaultFieldValidator implements FieldValidator {
@@ -13,28 +13,36 @@ import de.gecheckt.asv.application.model.ValidationResult;
import de.gecheckt.asv.application.model.ValidationSeverity; import de.gecheckt.asv.application.model.ValidationSeverity;
/** /**
* Standardimplementierung des StructureValidator, die allgemeine Strukturregeln prüft. * M3-Vorbau. In M1 bewusst nicht im produktiven Lauf verdrahtet.
* * Wird ab M3 wieder aktiviert und gegen die finalen Regelklassifikationen
* Geprüfte Regeln: * (V1-V/T/N/K) aus fachliche-anforderungen.md bewertet.
* 1. Die Eingabedatei muss mindestens eine Nachricht enthalten *
* 2. Jede Nachricht muss mindestens ein Segment enthalten * @see <a href="docs/arbeitspakete/m1/E00-entscheidungsprotokoll.md">E-01</a>
* 3. Segmentnamen dürfen nicht leer sein *
* 4. Feldpositionen innerhalb eines Segments müssen eindeutig und positiv sein * <p>Standardimplementierung des StructureValidator, die allgemeine Strukturregeln prüft.</p>
* 5. Segmentpositionen innerhalb einer Nachricht müssen eindeutig und positiv sein *
* 6. Nachrichtenpositionen innerhalb einer Eingabedatei müssen eindeutig und positiv sein * <p>Geprüfte Regeln:</p>
* 7. UNH- und UNT-Referenznummern müssen innerhalb einer Nachricht übereinstimmen * <ol>
* 8. Die im UNT angegebene Segmentanzahl muss der tatsächlichen Anzahl der Segmente entsprechen * <li>Die Eingabedatei muss mindestens eine Nachricht enthalten</li>
* 9. Eine Nachricht muss mindestens ein UNH-Segment enthalten * <li>Jede Nachricht muss mindestens ein Segment enthalten</li>
* 10. Eine Nachricht muss mindestens ein UNT-Segment enthalten * <li>Segmentnamen dürfen nicht leer sein</li>
* 11. UNH muss vor UNT stehen * <li>Feldpositionen innerhalb eines Segments müssen eindeutig und positiv sein</li>
* 12. Der Nachrichtentyp in UNH/S009/0065 darf nur ASVREC oder ASVFEH sein * <li>Segmentpositionen innerhalb einer Nachricht müssen eindeutig und positiv sein</li>
* 13. Für Nachrichten vom Typ ASVREC müssen die Segmente IFA, REA und IVA vorhanden sein * <li>Nachrichtenpositionen innerhalb einer Eingabedatei müssen eindeutig und positiv sein</li>
* 14. Für Nachrichten vom Typ ASVREC muss die Reihenfolge IFA vor REA vor IVA eingehalten werden * <li>UNH- und UNT-Referenznummern müssen innerhalb einer Nachricht übereinstimmen</li>
* 15. Für ASVREC mit Rechnungskennzeichen "0" in REA müssen DGN und LEA vorhanden sein * <li>Die im UNT angegebene Segmentanzahl muss der tatsächlichen Anzahl der Segmente entsprechen</li>
* 16. Für ASVREC mit Rechnungskennzeichen "1" in REA dürfen DGN und LEA nicht vorhanden sein * <li>Eine Nachricht muss mindestens ein UNH-Segment enthalten</li>
* 17. Für ASVREC mit Rechnungskennzeichen "1" in REA muss der Rechnungsbetrag "0,00" sein * <li>Eine Nachricht muss mindestens ein UNT-Segment enthalten</li>
* 18. Für ASVREC müssen IFA, REA und IVA jeweils genau einmal vorkommen * <li>UNH muss vor UNT stehen</li>
* 19. Für ASVFEH-Nachrichten muss mindestens ein FHL-Segment vorhanden sein * <li>Der Nachrichtentyp in UNH/S009/0065 darf nur ASVREC oder ASVFEH sein</li>
* <li>Für Nachrichten vom Typ ASVREC müssen die Segmente IFA, REA und IVA vorhanden sein</li>
* <li>Für Nachrichten vom Typ ASVREC muss die Reihenfolge IFA vor REA vor IVA eingehalten werden</li>
* <li>Für ASVREC mit Rechnungskennzeichen "0" in REA müssen DGN und LEA vorhanden sein</li>
* <li>Für ASVREC mit Rechnungskennzeichen "1" in REA dürfen DGN und LEA nicht vorhanden sein</li>
* <li>Für ASVREC mit Rechnungskennzeichen "1" in REA muss der Rechnungsbetrag "0,00" sein</li>
* <li>Für ASVREC müssen IFA, REA und IVA jeweils genau einmal vorkommen</li>
* <li>Für ASVFEH-Nachrichten muss mindestens ein FHL-Segment vorhanden sein</li>
* </ol>
*/ */
public class DefaultStructureValidator implements StructureValidator { public class DefaultStructureValidator implements StructureValidator {
@@ -0,0 +1,64 @@
package de.gecheckt.asv.bootstrap;
import de.gecheckt.asv.adapter.in.cli.CliRunner;
import de.gecheckt.asv.adapter.out.filesystem.SuffixResolver;
import de.gecheckt.asv.adapter.out.logging.LoggingConfigurator;
import de.gecheckt.asv.adapter.out.reporting.ReportFileWriter;
import de.gecheckt.asv.application.DummyFileValidationService;
import de.gecheckt.asv.application.FileValidationService;
/**
* Einziger {@code public static void main}-Einstiegspunkt des ASV-Format-Validators.
*
* <p>Verantwortlichkeiten:</p>
* <ol>
* <li>Manuelle Constructor Injection aller Anwendungskomponenten</li>
* <li>Logging-Konfiguration über {@link LoggingConfigurator} (Log-Datei im Eingabeverzeichnis)</li>
* <li>Delegation an {@link CliRunner#run(String[], ReportFileWriter)}</li>
* <li>Weiterreichen des Exit-Codes an {@link System#exit(int)}</li>
* </ol>
*
* <p><strong>Log4j2-Sichtbarkeit:</strong> Nur dieses Paket ({@code bootstrap}) und
* {@code adapter.out.logging} dürfen Log4j2-Typen direkt verwenden.</p>
*
* <p><strong>Reihenfolge vor dem Validierungslauf (AP07):</strong></p>
* <ol>
* <li>Eingabedatei-Pfad aus Argumenten bestimmen (in {@link CliRunner})</li>
* <li>Basisname und Zielverzeichnis ableiten</li>
* <li>{@link SuffixResolver} für {@code .log} aufrufen</li>
* <li>{@link LoggingConfigurator#configureLogFile(java.nio.file.Path)} aufrufen</li>
* <li>Validierungslauf starten</li>
* <li>{@link ReportFileWriter} schreibt Berichtdatei</li>
* <li>Konsolenausgabe (identisch zum Berichtinhalt)</li>
* </ol>
*/
public final class Main {
private Main() {
// Nicht instanziierbar
}
/**
* Startpunkt der CLI-Anwendung.
*
* @param args Kommandozeilenargumente; erwartet genau einen Dateipfad
*/
public static void main(String[] args) {
// Infrastruktur-Objekte (zustandslos)
LoggingConfigurator loggingConfigurator = new LoggingConfigurator();
SuffixResolver suffixResolver = new SuffixResolver();
ReportFileWriter reportFileWriter = new ReportFileWriter(suffixResolver);
// Manuelle Constructor Injection
FileValidationService validationService = new DummyFileValidationService();
CliRunner cliRunner = new CliRunner(
validationService,
loggingConfigurator,
suffixResolver,
reportFileWriter);
// Ausführen und Exit-Code weiterreichen
int exitCode = cliRunner.run(args);
System.exit(exitCode);
}
}
@@ -0,0 +1,37 @@
package de.gecheckt.asv.bootstrap;
import java.util.List;
import de.gecheckt.asv.application.field.FieldValidator;
import de.gecheckt.asv.application.model.ValidationResult;
import de.gecheckt.asv.domain.model.InputFile;
/**
* M1-Platzhalter für den Feldvalidator.
*
* <p>Erzeugt keinerlei Befunde — der Validator ist in M1 bewusst ohne fachliche Prüflogik
* verdrahtet, damit kein aktiver Lauf ASVREC-/ASVFEH-Feldbefunde liefert.</p>
*
* <p>Ab M3 durch {@link de.gecheckt.asv.application.field.DefaultFieldValidator}
* ersetzen, sobald die Regelklassifikationen (V1-V/T/N/K) aus
* {@code docs/specs/fachliche-anforderungen.md} abgestimmt sind.</p>
*
* @see de.gecheckt.asv.application.field.DefaultFieldValidator
*/
public final class NoOpFieldValidator implements FieldValidator {
/**
* Gibt stets ein leeres Validierungsergebnis zurück.
*
* @param inputFile die Eingabedatei (wird nicht ausgewertet)
* @return leeres {@link ValidationResult} ohne Fehler
* @throws IllegalArgumentException wenn inputFile null ist
*/
@Override
public ValidationResult validate(InputFile inputFile) {
if (inputFile == null) {
throw new IllegalArgumentException("inputFile darf nicht null sein");
}
return new ValidationResult(List.of()); // bewusst leer in M1
}
}
@@ -0,0 +1,37 @@
package de.gecheckt.asv.bootstrap;
import java.util.List;
import de.gecheckt.asv.application.model.ValidationResult;
import de.gecheckt.asv.application.structure.StructureValidator;
import de.gecheckt.asv.domain.model.InputFile;
/**
* M1-Platzhalter für den Strukturvalidator.
*
* <p>Erzeugt keinerlei Befunde — der Validator ist in M1 bewusst ohne fachliche Prüflogik
* verdrahtet, damit kein aktiver Lauf ASVREC-/ASVFEH-Strukturbefunde liefert.</p>
*
* <p>Ab M3 durch {@link de.gecheckt.asv.application.structure.DefaultStructureValidator}
* ersetzen, sobald die Regelklassifikationen (V1-V/T/N/K) aus
* {@code docs/specs/fachliche-anforderungen.md} abgestimmt sind.</p>
*
* @see de.gecheckt.asv.application.structure.DefaultStructureValidator
*/
public final class NoOpStructureValidator implements StructureValidator {
/**
* Gibt stets ein leeres Validierungsergebnis zurück.
*
* @param inputFile die Eingabedatei (wird nicht ausgewertet)
* @return leeres {@link ValidationResult} ohne Fehler
* @throws IllegalArgumentException wenn inputFile null ist
*/
@Override
public ValidationResult validate(InputFile inputFile) {
if (inputFile == null) {
throw new IllegalArgumentException("inputFile darf nicht null sein");
}
return new ValidationResult(List.of()); // bewusst leer in M1
}
}
@@ -0,0 +1,173 @@
package de.gecheckt.asv.domain.finding;
import java.util.Objects;
/**
* Einzelbefund eines Validierungslaufs.
*
* <p>Ein Befund trägt alle Meta-Informationen, die für Berichtserzeugung und spätere
* GUI-Darstellung benötigt werden. Nullable-Felder sind explizit so gekennzeichnet —
* sie sind optional, weil nicht jeder Befund auf ein konkretes Segment oder Feld
* zurückführbar ist.</p>
*
* <p>Unveränderlich (Record). Alle nicht-nullable Felder werden im Konstruktor
* auf {@code null} geprüft.</p>
*
* @param kind Befundart: {@link FindingKind#SPEC} oder {@link FindingKind#DIAGNOSTIC}
* @param severity Schweregrad: ERROR, WARNING oder HINT
* @param layer Schicht, auf die sich der Befund bezieht
* @param ruleId interne Regel-ID; kann {@code null} sein
* @param officialErrorCode offizieller Spec-Fehlercode gemäß Spezifikation Abschnitt 7;
* kann {@code null} sein, wenn kein direkter Fehlercode zugeordnet ist
* @param segmentType Segmentbezeichnung, z.B. {@code "UNB"}; kann {@code null} sein
* @param segmentIndex Null-basierter Index des Segments in der Datei; kann {@code null} sein
* @param fieldId Feld-ID, z.B. {@code "UNB_0020"}; kann {@code null} sein
* @param rawValue Rohwert des betroffenen Felds oder Segments; kann {@code null} sein
* @param position Byte- oder Zeichenposition in der Eingabedatei; kann {@code null} sein
* @param messageReference UNH 0062-Referenz bei Nachrichtenbezug; kann {@code null} sein
* @param germanMessage deutschsprachiger Befundtext; darf <em>nicht</em> {@code null} sein
*/
public record Finding(
FindingKind kind,
Severity severity,
FindingLayer layer,
String ruleId,
String officialErrorCode,
String segmentType,
Integer segmentIndex,
String fieldId,
String rawValue,
Integer position,
String messageReference,
String germanMessage
) {
/**
* Kompaktkonstruktor mit Null-Prüfung für alle Pflichtfelder.
*/
public Finding {
Objects.requireNonNull(kind, "kind darf nicht null sein");
Objects.requireNonNull(severity, "severity darf nicht null sein");
Objects.requireNonNull(layer, "layer darf nicht null sein");
Objects.requireNonNull(germanMessage, "germanMessage darf nicht null sein");
}
// ---------------------------------------------------------------------------
// Hilfsmethoden
// ---------------------------------------------------------------------------
/**
* Gibt zurück, ob es sich um einen SPEC-ERROR-Befund handelt.
* Nur solche Befunde beeinflussen das Gesamturteil.
*
* @return {@code true} genau dann, wenn {@code kind == SPEC && severity == ERROR}
*/
public boolean isSpecError() {
return kind == FindingKind.SPEC && severity == Severity.ERROR;
}
// ---------------------------------------------------------------------------
// Builder
// ---------------------------------------------------------------------------
/**
* Erzeugt einen neuen Builder für {@link Finding}.
*
* @param kind Befundart (Pflichtfeld)
* @param severity Schweregrad (Pflichtfeld)
* @param layer Schicht (Pflichtfeld)
* @param germanMessage Befundtext auf Deutsch (Pflichtfeld)
* @return neuer Builder
*/
public static Builder builder(FindingKind kind, Severity severity, FindingLayer layer,
String germanMessage) {
return new Builder(kind, severity, layer, germanMessage);
}
/**
* Builder für {@link Finding}. Alle optionalen Felder werden mit {@code null} initialisiert.
*/
public static final class Builder {
private final FindingKind kind;
private final Severity severity;
private final FindingLayer layer;
private final String germanMessage;
private String ruleId;
private String officialErrorCode;
private String segmentType;
private Integer segmentIndex;
private String fieldId;
private String rawValue;
private Integer position;
private String messageReference;
private Builder(FindingKind kind, Severity severity, FindingLayer layer,
String germanMessage) {
this.kind = kind;
this.severity = severity;
this.layer = layer;
this.germanMessage = germanMessage;
}
/** Setzt die interne Regel-ID. */
public Builder ruleId(String ruleId) {
this.ruleId = ruleId;
return this;
}
/** Setzt den offiziellen Spec-Fehlercode. */
public Builder officialErrorCode(String officialErrorCode) {
this.officialErrorCode = officialErrorCode;
return this;
}
/** Setzt den Segmenttyp (z.B. {@code "UNB"}). */
public Builder segmentType(String segmentType) {
this.segmentType = segmentType;
return this;
}
/** Setzt den Segmentindex. */
public Builder segmentIndex(Integer segmentIndex) {
this.segmentIndex = segmentIndex;
return this;
}
/** Setzt die Feld-ID (z.B. {@code "UNB_0020"}). */
public Builder fieldId(String fieldId) {
this.fieldId = fieldId;
return this;
}
/** Setzt den Rohwert. */
public Builder rawValue(String rawValue) {
this.rawValue = rawValue;
return this;
}
/** Setzt die Position (Byte-/Zeichenposition). */
public Builder position(Integer position) {
this.position = position;
return this;
}
/** Setzt die Nachrichtenreferenz (UNH 0062). */
public Builder messageReference(String messageReference) {
this.messageReference = messageReference;
return this;
}
/**
* Baut das {@link Finding}-Objekt.
*
* @return neuer, unveränderlicher {@link Finding}-Befund
*/
public Finding build() {
return new Finding(kind, severity, layer, ruleId, officialErrorCode,
segmentType, segmentIndex, fieldId, rawValue, position,
messageReference, germanMessage);
}
}
}
@@ -0,0 +1,25 @@
package de.gecheckt.asv.domain.finding;
/**
* Art eines Befunds: Spec-Urteil oder diagnostische Weiteranalyse.
*
* <p>Die Unterscheidung ist architektonisch zentral: Nur {@link #SPEC}-Befunde dürfen das
* Gesamturteil ({@link Verdict}) beeinflussen. {@link #DIAGNOSTIC}-Befunde liefern zusätzliche
* technische Informationen, ohne das Spec-Urteil zu verändern — auch nicht bei
* {@link Severity#ERROR}.</p>
*/
public enum FindingKind {
/**
* Befund ist Teil des normativen Spec-Urteils.
* Ein {@link Severity#ERROR}-Befund dieser Art setzt das Urteil auf {@link Verdict#INVALID}.
*/
SPEC,
/**
* Diagnostischer Befund zur Weiteranalyse.
* Dieser Befund beeinflusst das Spec-Urteil <em>niemals</em>, auch nicht bei
* {@link Severity#ERROR}.
*/
DIAGNOSTIC
}
@@ -0,0 +1,28 @@
package de.gecheckt.asv.domain.finding;
/**
* Schicht, auf die sich ein Befund bezieht.
*
* <p>Die Schichttrennung stellt sicher, dass technische Befunde nicht mit fachlichen Befunden
* vermischt werden und eine spätere GUI differenziert auf dieselben Daten zugreifen kann.</p>
*/
public enum FindingLayer {
/**
* Äußeres Artefakt — Datei auf Dateisystemebene (Dateiname, Dateityp,
* PKCS#7-/Auftragsdatei-/Nutzdatei-Schicht).
*/
ARTIFACT,
/**
* Technische Struktur — Service-Segmente (UNA, UNB, UNH, UNT, UNZ),
* KKS-Auftragssatz, Datei-/Transportebene, Nachrichtenhüllen.
*/
TECHNICAL_STRUCTURE,
/**
* Kanonisches Fachmodell — fachlich-technische Repräsentation der ASV-Nachrichten
* (ASVREC, ASVFEH und Storno-Ausprägungen).
*/
DOMAIN_MODEL
}
@@ -0,0 +1,20 @@
package de.gecheckt.asv.domain.finding;
/**
* Schweregrad eines Befunds.
*
* <p>Nur {@link #ERROR}-Befunde mit {@link FindingKind#SPEC} beeinflussen das Prüfurteil
* ({@link ValidationReport#computeVerdict()}). Warnungen und Hinweise verändern den
* Gültigkeitsstatus nicht.</p>
*/
public enum Severity {
/** Fehler — bei Spec-Befunden führt dies zu {@link Verdict#INVALID}. */
ERROR,
/** Warnung — beeinflusst das Spec-Urteil nicht. */
WARNING,
/** Hinweis — informativ, kein Einfluss auf das Spec-Urteil. */
HINT
}
@@ -0,0 +1,188 @@
package de.gecheckt.asv.domain.finding;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
/**
* Gesamtergebnis eines Validierungslaufs.
*
* <p>Ein {@code ValidationReport} fasst alle {@link Finding}-Befunde zusammen und berechnet
* daraus das Gesamturteil ({@link Verdict}). Die zentrale Invariante lautet:</p>
* <blockquote>
* {@link #computeVerdict()} berücksichtigt <em>ausschließlich</em> Befunde mit
* {@link FindingKind#SPEC} <em>und</em> {@link Severity#ERROR}.
* Ein {@link FindingKind#DIAGNOSTIC}-Befund mit {@link Severity#ERROR} setzt das Urteil
* <em>niemals</em> auf {@link Verdict#INVALID}.
* </blockquote>
*
* <p>Instanzen sind unveränderlich. Die Befundliste kann nach Erzeugung nicht mehr verändert
* werden.</p>
*/
public final class ValidationReport {
/** Name der validierten Eingabedatei. */
private final String fileName;
/** Zeitpunkt der Berichterstellung (UTC). */
private final Instant timestamp;
/**
* Unveränderliche Liste aller Befunde. Die Liste wird intern über
* {@link List#copyOf(java.util.Collection)} gesichert, sodass externe Referenzen
* sie nicht verändern können.
*/
private final List<Finding> findings;
/**
* Gibt an, ob es sich um einen Bedienfehler-Bericht handelt. In diesem Fall ist
* {@link #computeVerdict()} immer {@link Verdict#OPERATIONAL_ERROR}.
*/
private final boolean operationalError;
// ---------------------------------------------------------------------------
// Konstruktoren
// ---------------------------------------------------------------------------
/**
* Erzeugt einen normalen Validierungsbericht.
*
* @param fileName Dateiname der validierten Eingabedatei (nicht null)
* @param timestamp Zeitstempel der Berichterstellung (nicht null)
* @param findings Liste der Befunde (nicht null, darf leer sein)
*/
public ValidationReport(String fileName, Instant timestamp, List<Finding> findings) {
this.fileName = Objects.requireNonNull(fileName, "fileName darf nicht null sein");
this.timestamp = Objects.requireNonNull(timestamp, "timestamp darf nicht null sein");
Objects.requireNonNull(findings, "findings darf nicht null sein");
this.findings = List.copyOf(findings);
this.operationalError = false;
}
/**
* Privater Konstruktor für den Bedienfehler-Fall.
*/
private ValidationReport(String fileName, Instant timestamp, List<Finding> findings,
boolean operationalError) {
this.fileName = Objects.requireNonNull(fileName, "fileName darf nicht null sein");
this.timestamp = Objects.requireNonNull(timestamp, "timestamp darf nicht null sein");
Objects.requireNonNull(findings, "findings darf nicht null sein");
this.findings = List.copyOf(findings);
this.operationalError = operationalError;
}
// ---------------------------------------------------------------------------
// Factory-Methoden
// ---------------------------------------------------------------------------
/**
* Erzeugt einen Bedienfehler-Bericht. Das Urteil ist immer {@link Verdict#OPERATIONAL_ERROR}.
*
* <p>Typische Anwendungsfälle: fehlendes Pflichtargument, nicht lesbare Eingabedatei.</p>
*
* @param fileName Dateiname oder Platzhalter (nicht null)
* @param ruleId interne Regel-ID des auslösenden Prüfschritts (kann null sein)
* @param message deutschsprachige Fehlerbeschreibung (nicht null)
* @return neuer {@code ValidationReport} mit {@link Verdict#OPERATIONAL_ERROR}
*/
public static ValidationReport operationalError(String fileName, String ruleId,
String message) {
Objects.requireNonNull(fileName, "fileName darf nicht null sein");
Objects.requireNonNull(message, "message darf nicht null sein");
Finding errorFinding = Finding.builder(
FindingKind.SPEC, Severity.ERROR, FindingLayer.ARTIFACT, message)
.ruleId(ruleId)
.build();
return new ValidationReport(fileName, Instant.now(), List.of(errorFinding), true);
}
// ---------------------------------------------------------------------------
// Kern-Methoden
// ---------------------------------------------------------------------------
/**
* Berechnet das Gesamturteil des Validierungslaufs.
*
* <p><strong>Invariante:</strong> Nur Befunde mit {@link FindingKind#SPEC} und
* {@link Severity#ERROR} führen zu {@link Verdict#INVALID}. Diagnostische Befunde —
* auch solche mit {@link Severity#ERROR} — beeinflussen das Urteil niemals.</p>
*
* @return {@link Verdict#OPERATIONAL_ERROR} bei Bedienfehler-Bericht,
* {@link Verdict#INVALID} bei mindestens einem SPEC-ERROR-Befund,
* {@link Verdict#VALID} sonst
*/
public Verdict computeVerdict() {
if (operationalError) {
return Verdict.OPERATIONAL_ERROR;
}
return hasSpecErrors() ? Verdict.INVALID : Verdict.VALID;
}
/**
* Gibt zurück, ob mindestens ein SPEC-ERROR-Befund vorhanden ist.
*
* @return {@code true} genau dann, wenn {@code findings} mindestens einen Befund mit
* {@code kind == SPEC && severity == ERROR} enthält
*/
public boolean hasSpecErrors() {
return findings.stream().anyMatch(Finding::isSpecError);
}
/**
* Gibt alle Befunde mit {@link FindingKind#SPEC} zurück.
*
* @return unveränderliche Liste aller Spec-Befunde (niemals null)
*/
public List<Finding> specFindings() {
return findings.stream()
.filter(f -> f.kind() == FindingKind.SPEC)
.toList();
}
/**
* Gibt alle Befunde mit {@link FindingKind#DIAGNOSTIC} zurück.
*
* @return unveränderliche Liste aller Diagnose-Befunde (niemals null)
*/
public List<Finding> diagnosticFindings() {
return findings.stream()
.filter(f -> f.kind() == FindingKind.DIAGNOSTIC)
.toList();
}
// ---------------------------------------------------------------------------
// Getter
// ---------------------------------------------------------------------------
/**
* Gibt den Dateinamen der validierten Eingabedatei zurück.
*
* @return Dateiname (nicht null)
*/
public String getFileName() {
return fileName;
}
/**
* Gibt den Zeitstempel der Berichterstellung zurück.
*
* @return Zeitstempel (nicht null, UTC)
*/
public Instant getTimestamp() {
return timestamp;
}
/**
* Gibt die unveränderliche Liste aller Befunde zurück.
*
* <p>Die zurückgegebene Liste kann nicht verändert werden — Versuche werfen
* {@link UnsupportedOperationException}.</p>
*
* @return unveränderliche Befundliste (nicht null)
*/
public List<Finding> getFindings() {
return findings;
}
}
@@ -0,0 +1,30 @@
package de.gecheckt.asv.domain.finding;
/**
* Gesamturteil eines Validierungslaufs.
*
* <p>Das Urteil wird durch {@link ValidationReport#computeVerdict()} berechnet und basiert
* ausschließlich auf {@link FindingKind#SPEC}-Befunden mit {@link Severity#ERROR}.
* Diagnostische Befunde beeinflussen das Urteil niemals.</p>
*
* <p>Entsprechung zu Exit-Codes gemäß Architekturvorgabe:</p>
* <ul>
* <li>{@link #VALID} → Exit-Code 0</li>
* <li>{@link #INVALID} → Exit-Code 1</li>
* <li>{@link #OPERATIONAL_ERROR} → Exit-Code 2</li>
* </ul>
*/
public enum Verdict {
/** Gültig — keine SPEC-ERROR-Befunde vorhanden. Exit-Code 0. */
VALID,
/** Ungültig — mindestens ein SPEC-ERROR-Befund vorhanden. Exit-Code 1. */
INVALID,
/**
* Bedienfehler — z.B. fehlendes Argument oder nicht lesbare Eingabedatei.
* Exit-Code 2. Wird über {@link ValidationReport#operationalError} erzeugt.
*/
OPERATIONAL_ERROR
}
+9 -1
View File
@@ -1,10 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!--
Log4j2-Konfiguration des ASV-Format-Validators.
HINWEIS (AP07): Der File-Appender hier ist ein FALLBACK-Default.
Er greift nur, wenn LoggingConfigurator.configureLogFile(Path) NICHT aufgerufen wurde
(z.B. in Unit-Tests). Bei produktiven Läufen wird der Dateipfad programmatisch gesetzt.
-->
<Configuration status="WARN"> <Configuration status="WARN">
<Appenders> <Appenders>
<Console name="Console" target="SYSTEM_ERR"> <Console name="Console" target="SYSTEM_ERR">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>
</Console> </Console>
<File name="File" fileName="logs/asv-format-validator.log" append="true"> <!-- Fallback: greift nur wenn configureLogFile(Path) nicht aufgerufen wurde -->
<File name="File" fileName="logs/asv-format-validator-fallback.log" append="true">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>
</File> </File>
</Appenders> </Appenders>
@@ -0,0 +1,93 @@
package de.gecheckt.asv;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
/**
* Automatisierte Architekturtests für den ASV-Format-Validator.
*
* <p>Sichert die in M1 etablierten Strukturregeln der hexagonalen Architektur dauerhaft ab.
* Jede Verletzung führt zu einem fehlschlagenden Build.</p>
*
* <p>Regeln:</p>
* <ul>
* <li>A Log4j2-Typen dürfen nur im Logging-Adapter und im Bootstrap sichtbar sein</li>
* <li>B Domain-Klassen dürfen keine Adapter- oder Bootstrap-Abhängigkeiten haben</li>
* <li>C Application-Klassen dürfen keine Adapter- oder Bootstrap-Abhängigkeiten haben</li>
* <li>D Preview-Validatoren werden in M1 nicht aus aktivem Adapter- oder Bootstrap-Code referenziert</li>
* </ul>
*/
@AnalyzeClasses(packages = "de.gecheckt.asv", importOptions = ImportOption.DoNotIncludeTests.class)
class ArchitectureTest {
/**
* Regel A — Log4j2-Sichtbarkeit.
*
* Log4j2-Typen ({@code org.apache.logging.log4j.*}) dürfen nur im Logging-Adapter
* ({@code adapter.out.logging}) und im Bootstrap sichtbar sein. Alle anderen Pakete
* verwenden ausschließlich die SLF4J-Fassade.
*/
@ArchTest
static final ArchRule log4j2_nur_in_logging_adapter_und_bootstrap =
noClasses()
.that().resideOutsideOfPackages(
"de.gecheckt.asv.adapter.out.logging..",
"de.gecheckt.asv.bootstrap..")
.should().dependOnClassesThat()
.resideInAPackage("org.apache.logging.log4j..")
.because("Log4j2 darf nur im Logging-Adapter und im Bootstrap sichtbar sein.");
/**
* Regel B — Domain-Reinheit.
*
* Domain-Klassen kennen keine Adapter-Implementierungen und kein Bootstrap.
* Nur die Domain selbst sowie die SLF4J-API und Java-Standardklassen dürfen
* aus dem Domain-Paket referenziert werden.
*/
@ArchTest
static final ArchRule domain_hat_keine_adapter_abhaengigkeit =
noClasses()
.that().resideInAPackage("de.gecheckt.asv.domain..")
.should().dependOnClassesThat()
.resideInAnyPackage(
"de.gecheckt.asv.adapter..",
"de.gecheckt.asv.bootstrap..");
/**
* Regel C — Application-Reinheit.
*
* Application-Klassen (Services, Ports) kennen keine Adapter-Implementierungen
* und kein Bootstrap. Abhängigkeiten gehen nur in Richtung Domain.
*/
@ArchTest
static final ArchRule application_kennt_keine_adapter_implementierungen =
noClasses()
.that().resideInAPackage("de.gecheckt.asv.application..")
.should().dependOnClassesThat()
.resideInAnyPackage(
"de.gecheckt.asv.adapter..",
"de.gecheckt.asv.bootstrap..");
/**
* Regel D — Preview-Isolation.
*
* Die in M1 eingefrorenen Preview-Validatoren ({@code DefaultStructureValidator} und
* {@code DefaultFieldValidator}) dürfen aus aktivem Adapter- und Bootstrap-Code nicht
* direkt referenziert werden. Sie werden erst ab M3 wieder aktiv eingesetzt.
*/
@ArchTest
static final ArchRule preview_wird_nicht_aus_aktivem_code_referenziert =
noClasses()
.that().resideInAnyPackage(
"de.gecheckt.asv.adapter..",
"de.gecheckt.asv.bootstrap..")
.should().dependOnClassesThat()
.haveSimpleNameContaining("DefaultStructureValidator")
.orShould().dependOnClassesThat()
.haveSimpleNameContaining("DefaultFieldValidator")
.because("Preview-Validatoren sind in M1 eingefroren und werden erst ab M3 aktiv verwendet.");
}
@@ -1,219 +0,0 @@
package de.gecheckt.asv.adapter.in.cli;
import de.gecheckt.asv.domain.model.Field;
import de.gecheckt.asv.domain.model.InputFile;
import de.gecheckt.asv.domain.model.Message;
import de.gecheckt.asv.domain.model.Segment;
import de.gecheckt.asv.adapter.out.parsing.InputFileParseException;
import de.gecheckt.asv.adapter.out.parsing.InputFileParser;
import de.gecheckt.asv.application.InputFileValidator;
import de.gecheckt.asv.application.model.ValidationError;
import de.gecheckt.asv.application.model.ValidationResult;
import de.gecheckt.asv.application.model.ValidationSeverity;
import de.gecheckt.asv.adapter.out.reporting.ValidationResultPrinter;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;
/**
* Zusätzliche Unittests für AsvValidatorApplication.
*/
class AsvValidatorApplicationAdditionalTest {
@TempDir
Path tempDir;
@Test
void testRunWithValidFileShouldReturnSuccessExitCode() throws InputFileParseException, IOException {
// Given
InputFileParser parser = mock(InputFileParser.class);
InputFileValidator validator = mock(InputFileValidator.class);
ValidationResultPrinter printer = mock(ValidationResultPrinter.class);
AsvValidatorApplication app = new AsvValidatorApplication(parser, validator, printer);
// Create a test file
Path testFile = tempDir.resolve("valid-file.txt");
String validContent = "HDR|TestHeader\n" +
"DTL|TestData|MoreData\n" +
"TRL|3";
Files.writeString(testFile, validContent);
String[] args = {testFile.toString()};
// Create real objects instead of mocks for final classes
Field hdrField1 = new Field(1, "HDR");
Field hdrField2 = new Field(2, "TestHeader");
Segment hdrSegment = new Segment("HDR", 1, List.of(hdrField1, hdrField2));
Field dtlField1 = new Field(1, "DTL");
Field dtlField2 = new Field(2, "TestData");
Field dtlField3 = new Field(3, "MoreData");
Segment dtlSegment = new Segment("DTL", 2, List.of(dtlField1, dtlField2, dtlField3));
Field trlField1 = new Field(1, "TRL");
Field trlField2 = new Field(2, "3");
Segment trlSegment = new Segment("TRL", 3, List.of(trlField1, trlField2));
Message message = new Message(1, List.of(hdrSegment, dtlSegment, trlSegment));
InputFile inputFile = new InputFile("valid-file.txt", List.of(message));
// Mock the parser and validator behavior
when(parser.parse(anyString(), anyString())).thenReturn(inputFile);
// Create a real ValidationResult with no errors
ValidationResult validationResult = new ValidationResult(List.of());
when(validator.validate(inputFile)).thenReturn(validationResult);
// Capture System.out
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
PrintStream originalOut = System.out;
System.setOut(new PrintStream(outContent));
try {
// When
int exitCode = app.run(args);
// Then
assertEquals(0, exitCode);
// Verify that the printer was called
verify(printer).printToConsole(validationResult);
} finally {
// Restore System.out
System.setOut(originalOut);
}
}
@Test
void testRunWithInvalidFileShouldReturnValidationErrorsExitCode() throws InputFileParseException, IOException {
// Given
InputFileParser parser = mock(InputFileParser.class);
InputFileValidator validator = mock(InputFileValidator.class);
ValidationResultPrinter printer = mock(ValidationResultPrinter.class);
AsvValidatorApplication app = new AsvValidatorApplication(parser, validator, printer);
// Create an invalid test file (missing required segments)
Path testFile = tempDir.resolve("invalid-file.txt");
String invalidContent = "DTL|TestData|MoreData\n" + // Missing HDR
"DTL|MoreData|EvenMoreData\n"; // Missing TRL
Files.writeString(testFile, invalidContent);
String[] args = {testFile.toString()};
// Create real objects instead of mocks for final classes
Field dtlField1 = new Field(1, "DTL");
Field dtlField2 = new Field(2, "TestData");
Field dtlField3 = new Field(3, "MoreData");
Segment dtlSegment1 = new Segment("DTL", 1, List.of(dtlField1, dtlField2, dtlField3));
Field dtlField4 = new Field(1, "DTL");
Field dtlField5 = new Field(2, "MoreData");
Field dtlField6 = new Field(3, "EvenMoreData");
Segment dtlSegment2 = new Segment("DTL", 2, List.of(dtlField4, dtlField5, dtlField6));
Message message = new Message(1, List.of(dtlSegment1, dtlSegment2));
InputFile inputFile = new InputFile("invalid-file.txt", List.of(message));
// Mock the parser and validator behavior
when(parser.parse(anyString(), anyString())).thenReturn(inputFile);
// Create a real ValidationResult with errors
ValidationError error = new ValidationError(
"MISSING_SEGMENT",
"Required segment HDR is missing",
ValidationSeverity.ERROR,
"HDR",
1,
"HDR",
1,
null,
"HDR segment is required"
);
ValidationResult validationResult = new ValidationResult(List.of(error));
when(validator.validate(inputFile)).thenReturn(validationResult);
// Capture System.out
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
PrintStream originalOut = System.out;
System.setOut(new PrintStream(outContent));
try {
// When
int exitCode = app.run(args);
// Then
assertEquals(3, exitCode); // Validation errors exit code
// Verify that the printer was called
verify(printer).printToConsole(validationResult);
} finally {
// Restore System.out
System.setOut(originalOut);
}
}
/**
* Spezialisierter Test für den Fall, dass ein technisch lesbares/parstabares Dokument
* Validierungsfehler enthält und der CLI Exit-Code 3 zurückgibt.
*
* Dieser Test konzentriert sich explizit auf:
* 1. Parser liefert ein InputFile
* 2. Validator liefert ein ValidationResult mit mindestens einem ERROR
* 3. CLI gibt daraufhin Exit-Code 3 zurück
*/
@Test
void testParserReturnsInputFileAndValidatorReturnsErrorsShouldReturnExitCodeThree() throws InputFileParseException, IOException {
// Given
InputFileParser parser = mock(InputFileParser.class);
InputFileValidator validator = mock(InputFileValidator.class);
ValidationResultPrinter printer = mock(ValidationResultPrinter.class);
AsvValidatorApplication app = new AsvValidatorApplication(parser, validator, printer);
// Create a dummy test file (content doesn't matter since we're mocking the parser)
Path testFile = tempDir.resolve("dummy-file.txt");
Files.writeString(testFile, "dummy content");
String[] args = {testFile.toString()};
// Mock: Parser liefert ein gültiges InputFile
InputFile inputFile = mock(InputFile.class);
when(parser.parse(anyString(), anyString())).thenReturn(inputFile);
// Mock: Validator liefert ein ValidationResult mit mindestens einem ERROR
ValidationError validationError = new ValidationError(
"TEST_ERROR_CODE",
"Test error message",
ValidationSeverity.ERROR, // Wichtig: ValidationSeverity.ERROR
"TEST_SEGMENT",
1,
"TEST_FIELD",
1,
null,
"Test error description"
);
ValidationResult validationResultWithErrors = new ValidationResult(List.of(validationError));
when(validator.validate(inputFile)).thenReturn(validationResultWithErrors);
// When
int exitCode = app.run(args);
// Then
// Prüfe explizit, dass der Exit-Code 3 ist
assertEquals(3, exitCode, "CLI should return exit code 3 when validation errors occur");
// Verify that the printer was called with the validation result
verify(printer).printToConsole(validationResultWithErrors);
}
}
@@ -1,104 +0,0 @@
package de.gecheckt.asv.adapter.in.cli;
import de.gecheckt.asv.adapter.out.parsing.InputFileParser;
import de.gecheckt.asv.application.InputFileValidator;
import de.gecheckt.asv.adapter.out.reporting.ValidationResultPrinter;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;
/**
* Unittests für AsvValidatorApplication.
*/
class AsvValidatorApplicationTest {
@TempDir
Path tempDir;
@Test
void testRunWithNoArgumentsShouldPrintUsageAndReturnInvalidArgumentsExitCode() {
// Given
InputFileParser parser = mock(InputFileParser.class);
InputFileValidator validator = mock(InputFileValidator.class);
ValidationResultPrinter printer = mock(ValidationResultPrinter.class);
AsvValidatorApplication app = new AsvValidatorApplication(parser, validator, printer);
String[] args = {};
// Capture System.out
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
PrintStream originalOut = System.out;
System.setOut(new PrintStream(outContent));
try {
// When
int exitCode = app.run(args);
// Then
assertEquals(1, exitCode);
assertEquals(true, outContent.toString().contains("Verwendung:"), "Output should contain usage information");
} finally {
// Restore System.out
System.setOut(originalOut);
}
}
@Test
void testRunWithTooManyArgumentsShouldPrintUsageAndReturnInvalidArgumentsExitCode() {
// Given
InputFileParser parser = mock(InputFileParser.class);
InputFileValidator validator = mock(InputFileValidator.class);
ValidationResultPrinter printer = mock(ValidationResultPrinter.class);
AsvValidatorApplication app = new AsvValidatorApplication(parser, validator, printer);
String[] args = {"file1.txt", "file2.txt"};
// Capture System.out
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
PrintStream originalOut = System.out;
System.setOut(new PrintStream(outContent));
try {
// When
int exitCode = app.run(args);
// Then
assertEquals(1, exitCode);
assertEquals(true, outContent.toString().contains("Verwendung:"), "Output should contain usage information");
} finally {
// Restore System.out
System.setOut(originalOut);
}
}
@Test
void testRunWithNonExistentFileShouldReturnFileErrorExitCode() {
// Given
InputFileParser parser = mock(InputFileParser.class);
InputFileValidator validator = mock(InputFileValidator.class);
ValidationResultPrinter printer = mock(ValidationResultPrinter.class);
AsvValidatorApplication app = new AsvValidatorApplication(parser, validator, printer);
String[] args = {"/non/existent/file.txt"};
// Capture System.err
ByteArrayOutputStream errContent = new ByteArrayOutputStream();
PrintStream originalErr = System.err;
System.setErr(new PrintStream(errContent));
try {
// When
int exitCode = app.run(args);
// Then
assertEquals(2, exitCode);
assertEquals(true, errContent.toString().contains("File does not exist"), "Error output should contain file not found message");
} finally {
// Restore System.err
System.setErr(originalErr);
}
}
}
@@ -0,0 +1,403 @@
package de.gecheckt.asv.adapter.in.cli;
import de.gecheckt.asv.adapter.out.filesystem.SuffixResolver;
import de.gecheckt.asv.adapter.out.logging.LoggingConfigurator;
import de.gecheckt.asv.adapter.out.reporting.ReportFileWriter;
import de.gecheckt.asv.application.FileValidationService;
import de.gecheckt.asv.domain.finding.Finding;
import de.gecheckt.asv.domain.finding.ValidationReport;
import de.gecheckt.asv.domain.finding.Verdict;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* Unit-Tests für die Bedienfehler-Behandlung (Exit-Code 2, Minimalbericht) in {@link CliRunner}.
*
* <p>Abgedeckte Abnahmekriterien aus AP08:</p>
* <ul>
* <li>Fall 1: Kein Argument → Exit 2, nur Konsole (STDERR), kein Verzeichnis</li>
* <li>Fall 2: Mehr als ein Argument → Exit 2, nur Konsole</li>
* <li>Fall 3: Datei existiert nicht → Exit 2, Berichtdatei im übergeordneten Verzeichnis</li>
* <li>Fall 4: Pfad ist kein regulärer Dateityp → Exit 2, nur Konsole</li>
* <li>Fall 5: Datei nicht lesbar → Exit 2, Berichtdatei im übergeordneten Verzeichnis</li>
* <li>Verdict OPERATIONAL_ERROR wird korrekt gesetzt</li>
* <li>Kein Stack-Trace in STDERR</li>
* </ul>
*
* <p>Hinweis: Der {@link LoggingConfigurator} wird in allen Tests als No-Op-Mock eingesetzt,
* um Windows-seitiges Dateisperr-Verhalten durch geöffnete Log4j2-Appender zu vermeiden.</p>
*/
class CliRunnerOperationalErrorTest {
@TempDir
Path tempDir;
private PrintStream originalStderr;
private ByteArrayOutputStream stderrBuf;
private PrintStream originalStdout;
private ByteArrayOutputStream stdoutBuf;
@BeforeEach
void captureStreams() {
originalStderr = System.err;
stderrBuf = new ByteArrayOutputStream();
System.setErr(new PrintStream(stderrBuf, true, StandardCharsets.UTF_8));
originalStdout = System.out;
stdoutBuf = new ByteArrayOutputStream();
System.setOut(new PrintStream(stdoutBuf, true, StandardCharsets.UTF_8));
}
@AfterEach
void restoreStreams() {
System.setErr(originalStderr);
System.setOut(originalStdout);
}
/** Gibt den bisher auf STDERR geschriebenen Text zurück. */
private String stderr() {
return stderrBuf.toString(StandardCharsets.UTF_8);
}
/** Gibt den bisher auf STDOUT geschriebenen Text zurück. */
private String stdout() {
return stdoutBuf.toString(StandardCharsets.UTF_8);
}
/**
* Erzeugt einen {@link CliRunner} mit echten Adaptern und No-Op-LoggingConfigurator.
*
* @param service der zu verwendende {@link FileValidationService}
*/
private CliRunner runnerWith(FileValidationService service) {
LoggingConfigurator noOpLogging = mock(LoggingConfigurator.class);
doNothing().when(noOpLogging).configureLogFile(any(Path.class));
SuffixResolver sr = new SuffixResolver();
return new CliRunner(service, noOpLogging, sr, new ReportFileWriter(sr));
}
// -----------------------------------------------------------------------
// Fall 1: Kein Argument
// -----------------------------------------------------------------------
@Test
@DisplayName("Fall 1: Kein Argument → Exit 2, ruleId OPERATIONAL-MISSING-ARG")
void fall1_keinArgument_exitCode2_undRuleId() {
FileValidationService service = mock(FileValidationService.class);
CliRunner runner = runnerWith(service);
int exitCode = runner.run(new String[]{});
assertEquals(ExitCode.OPERATIONAL_ERROR, exitCode,
"Kein Argument muss Exit-Code 2 liefern");
verifyNoInteractions(service);
}
@Test
@DisplayName("Fall 1: Kein Argument → STDERR enthält Fehlermeldung, keine Berichtdatei")
void fall1_keinArgument_nurKonsole() {
FileValidationService service = mock(FileValidationService.class);
CliRunner runner = runnerWith(service);
runner.run(new String[]{});
String err = stderr();
assertFalse(err.isBlank(), "STDERR muss eine Fehlermeldung enthalten");
// STDOUT (normale Berichtdatei) bleibt leer — nur STDERR
assertTrue(stdout().isBlank(), "STDOUT darf bei kein-Argument-Fehler keine Ausgabe enthalten");
// Keine Berichtdatei im TempDir
long txtFiles = countTxtFiles(tempDir);
assertEquals(0, txtFiles, "Bei 'kein Argument' darf keine Berichtdatei entstehen");
}
@Test
@DisplayName("Fall 1: Kein Argument → Minimalbericht im STDERR enthält BEDIENFEHLER")
void fall1_keinArgument_minimalberichtEnthaeltBedienfehler() {
FileValidationService service = mock(FileValidationService.class);
CliRunner runner = runnerWith(service);
runner.run(new String[]{});
assertTrue(stderr().contains("BEDIENFEHLER"),
"Der Minimalbericht muss das Urteil BEDIENFEHLER enthalten");
}
// -----------------------------------------------------------------------
// Fall 2: Mehr als ein Argument
// -----------------------------------------------------------------------
@Test
@DisplayName("Fall 2: Mehr als ein Argument → Exit 2, ruleId OPERATIONAL-TOO-MANY-ARGS")
void fall2_zuVieleArgumente_exitCode2() {
FileValidationService service = mock(FileValidationService.class);
CliRunner runner = runnerWith(service);
int exitCode = runner.run(new String[]{"datei1.auf", "datei2.auf"});
assertEquals(ExitCode.OPERATIONAL_ERROR, exitCode,
"Zu viele Argumente müssen Exit-Code 2 liefern");
verifyNoInteractions(service);
}
@Test
@DisplayName("Fall 2: Mehr als ein Argument → nur STDERR, keine Datei")
void fall2_zuVieleArgumente_nurKonsole() {
FileValidationService service = mock(FileValidationService.class);
CliRunner runner = runnerWith(service);
runner.run(new String[]{"datei1.auf", "datei2.auf", "datei3.auf"});
assertFalse(stderr().isBlank(), "STDERR muss Fehlermeldung enthalten");
assertTrue(stdout().isBlank(), "STDOUT darf keine Ausgabe enthalten");
assertEquals(0, countTxtFiles(tempDir), "Keine Berichtdatei bei zu vielen Argumenten");
}
// -----------------------------------------------------------------------
// Fall 3: Datei existiert nicht
// -----------------------------------------------------------------------
@Test
@DisplayName("Fall 3: Datei existiert nicht → Exit 2, ruleId OPERATIONAL-FILE-NOT-FOUND")
void fall3_dateiExistiertNicht_exitCode2() {
Path nichtVorhanden = tempDir.resolve("nicht-vorhanden.auf");
FileValidationService service = mock(FileValidationService.class);
CliRunner runner = runnerWith(service);
int exitCode = runner.run(new String[]{nichtVorhanden.toString()});
assertEquals(ExitCode.OPERATIONAL_ERROR, exitCode,
"Nicht vorhandene Datei muss Exit-Code 2 liefern");
verifyNoInteractions(service);
}
@Test
@DisplayName("Fall 3: Datei existiert nicht → Berichtdatei im übergeordneten Verzeichnis")
void fall3_dateiExistiertNicht_berichtdateiImUebergeordnetenVerzeichnis() {
Path nichtVorhanden = tempDir.resolve("nicht-vorhanden.auf");
FileValidationService service = mock(FileValidationService.class);
CliRunner runner = runnerWith(service);
runner.run(new String[]{nichtVorhanden.toString()});
// Im tempDir (übergeordnetes Verzeichnis der nicht-vorhandenen Datei) soll eine .txt entstehen
Path erwarteterBericht = tempDir.resolve("nicht-vorhanden.auf.txt");
assertTrue(Files.exists(erwarteterBericht),
"Berichtdatei soll im übergeordneten Verzeichnis liegen: " + erwarteterBericht);
}
@Test
@DisplayName("Fall 3: Datei existiert nicht → Berichtdatei enthält BEDIENFEHLER-Urteil")
void fall3_dateiExistiertNicht_berichtdateiEnthaeltOpertionalError() throws IOException {
Path nichtVorhanden = tempDir.resolve("fehlt.auf");
FileValidationService service = mock(FileValidationService.class);
CliRunner runner = runnerWith(service);
runner.run(new String[]{nichtVorhanden.toString()});
Path bericht = tempDir.resolve("fehlt.auf.txt");
assertTrue(Files.exists(bericht), "Berichtdatei muss existieren");
String inhalt = Files.readString(bericht, StandardCharsets.UTF_8);
assertTrue(inhalt.contains("BEDIENFEHLER"),
"Bericht muss BEDIENFEHLER-Urteil enthalten");
assertTrue(inhalt.contains("OPERATIONAL-FILE-NOT-FOUND"),
"Bericht muss ruleId OPERATIONAL-FILE-NOT-FOUND enthalten");
}
// -----------------------------------------------------------------------
// Fall 4: Pfad ist kein regulärer Dateityp (Verzeichnis)
// -----------------------------------------------------------------------
@Test
@DisplayName("Fall 4: Pfad ist ein Verzeichnis → Exit 2, ruleId OPERATIONAL-NOT-REGULAR")
void fall4_pfadIstVerzeichnis_exitCode2() {
FileValidationService service = mock(FileValidationService.class);
CliRunner runner = runnerWith(service);
// tempDir selbst ist ein Verzeichnis
int exitCode = runner.run(new String[]{tempDir.toString()});
assertEquals(ExitCode.OPERATIONAL_ERROR, exitCode,
"Verzeichnis als Eingabe muss Exit-Code 2 liefern");
verifyNoInteractions(service);
}
@Test
@DisplayName("Fall 4: Pfad ist ein Verzeichnis → nur STDERR, keine Berichtdatei")
void fall4_pfadIstVerzeichnis_nurKonsole() {
FileValidationService service = mock(FileValidationService.class);
CliRunner runner = runnerWith(service);
runner.run(new String[]{tempDir.toString()});
assertFalse(stderr().isBlank(), "STDERR muss Fehlermeldung enthalten");
assertTrue(stdout().isBlank(), "STDOUT darf keine Ausgabe enthalten");
}
// -----------------------------------------------------------------------
// Fall 5: Datei nicht lesbar — nur auf Nicht-Windows testbar
// Hinweis: Auf Windows gibt es keine zuverlässige Möglichkeit, eine Datei
// per setReadable(false) für den eigenen Prozess unlesbar zu machen.
// Dieser Test wird daher nur auf Unix-ähnlichen Systemen ausgeführt.
// -----------------------------------------------------------------------
@Test
@DisplayName("Fall 5: Datei nicht lesbar → Exit 2, ruleId OPERATIONAL-NOT-READABLE")
void fall5_dateiNichtLesbar_exitCode2() throws IOException {
// Nur auf Unix-ähnlichen Systemen ausführen (Windows ignoriert setReadable)
org.junit.jupiter.api.Assumptions.assumeTrue(
!System.getProperty("os.name", "").toLowerCase().contains("windows"),
"Test wird auf Windows übersprungen (setReadable nicht zuverlässig)");
Path nichtLesbar = tempDir.resolve("gesperrt.auf");
Files.writeString(nichtLesbar, "Inhalt");
boolean ok = nichtLesbar.toFile().setReadable(false);
org.junit.jupiter.api.Assumptions.assumeTrue(ok,
"setReadable(false) nicht anwendbar — Test wird übersprungen");
try {
FileValidationService service = mock(FileValidationService.class);
CliRunner runner = runnerWith(service);
int exitCode = runner.run(new String[]{nichtLesbar.toString()});
assertEquals(ExitCode.OPERATIONAL_ERROR, exitCode,
"Nicht lesbare Datei muss Exit-Code 2 liefern");
verifyNoInteractions(service);
} finally {
nichtLesbar.toFile().setReadable(true);
}
}
// -----------------------------------------------------------------------
// Verdict OPERATIONAL_ERROR verifizieren
// -----------------------------------------------------------------------
@Test
@DisplayName("operationalError-Report: Verdict ist OPERATIONAL_ERROR")
void operationalErrorReport_verdictIstOPERATIONAL_ERROR() {
ValidationReport report = ValidationReport.operationalError(
"<kein Argument>", "OPERATIONAL-MISSING-ARG",
"Kein Dateipfad angegeben.");
assertEquals(Verdict.OPERATIONAL_ERROR, report.computeVerdict(),
"operationalError-Factory muss Verdict OPERATIONAL_ERROR liefern");
List<Finding> findings = report.getFindings();
assertFalse(findings.isEmpty(), "Mindestens ein Finding erwartet");
Finding finding = findings.get(0);
assertEquals("OPERATIONAL-MISSING-ARG", finding.ruleId(),
"ruleId muss OPERATIONAL-MISSING-ARG sein");
assertNotNull(finding.germanMessage(), "germanMessage darf nicht null sein");
}
@Test
@DisplayName("Alle ruleIds der 5 Bedienfehler-Fälle sind korrekt definiert")
void alleRuleIds_sindKorrektDefiniert() {
String[] ruleIds = {
"OPERATIONAL-MISSING-ARG",
"OPERATIONAL-TOO-MANY-ARGS",
"OPERATIONAL-FILE-NOT-FOUND",
"OPERATIONAL-NOT-REGULAR",
"OPERATIONAL-NOT-READABLE"
};
for (String ruleId : ruleIds) {
ValidationReport report = ValidationReport.operationalError(
"test.auf", ruleId, "Testmeldung für " + ruleId);
assertEquals(Verdict.OPERATIONAL_ERROR, report.computeVerdict(),
"Report mit ruleId " + ruleId + " muss OPERATIONAL_ERROR liefern");
assertEquals(ruleId, report.getFindings().get(0).ruleId(),
"ruleId muss korrekt gesetzt sein: " + ruleId);
}
}
// -----------------------------------------------------------------------
// Negativ-Test: Kein Stack-Trace in STDERR
// -----------------------------------------------------------------------
@Test
@DisplayName("Kein Stack-Trace in STDERR bei Bedienfehler 'Kein Argument'")
void keinArgument_keinStackTraceInStderr() {
FileValidationService service = mock(FileValidationService.class);
CliRunner runner = runnerWith(service);
runner.run(new String[]{});
String err = stderr();
// Stack-Traces enthalten "at " gefolgt von Java-Paketnamen
assertFalse(err.contains("\tat "),
"STDERR darf keinen Stack-Trace enthalten (kein '\\tat '). Gefunden: " + err);
assertFalse(err.contains("Exception"),
"STDERR darf keinen Exception-Klassennamen enthalten. Gefunden: " + err);
}
@Test
@DisplayName("Kein Stack-Trace in STDERR bei Bedienfehler 'Datei nicht gefunden'")
void dateiNichtGefunden_keinStackTraceInStderr() {
Path nichtVorhanden = tempDir.resolve("nicht-da.auf");
FileValidationService service = mock(FileValidationService.class);
CliRunner runner = runnerWith(service);
runner.run(new String[]{nichtVorhanden.toString()});
String err = stderr();
assertFalse(err.contains("\tat "),
"STDERR darf keinen Stack-Trace enthalten. Gefunden: " + err);
assertFalse(err.contains("Exception"),
"STDERR darf keinen Exception-Klassennamen enthalten. Gefunden: " + err);
}
@Test
@DisplayName("Kein Stack-Trace in STDERR bei Bedienfehler 'Pfad ist Verzeichnis'")
void pfadIstVerzeichnis_keinStackTraceInStderr() {
FileValidationService service = mock(FileValidationService.class);
CliRunner runner = runnerWith(service);
runner.run(new String[]{tempDir.toString()});
String err = stderr();
assertFalse(err.contains("\tat "),
"STDERR darf keinen Stack-Trace enthalten. Gefunden: " + err);
assertFalse(err.contains("Exception"),
"STDERR darf keinen Exception-Klassennamen enthalten. Gefunden: " + err);
}
// -----------------------------------------------------------------------
// Hilfsmethoden
// -----------------------------------------------------------------------
/** Zählt .txt-Dateien direkt im angegebenen Verzeichnis. */
private long countTxtFiles(Path dir) {
try {
return Files.list(dir)
.filter(p -> p.toString().endsWith(".txt"))
.count();
} catch (IOException e) {
return 0;
}
}
}
@@ -0,0 +1,195 @@
package de.gecheckt.asv.adapter.in.cli;
import de.gecheckt.asv.adapter.out.filesystem.SuffixResolver;
import de.gecheckt.asv.adapter.out.logging.LoggingConfigurator;
import de.gecheckt.asv.adapter.out.reporting.ReportFileWriter;
import de.gecheckt.asv.application.DummyFileValidationService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
/**
* End-to-End-Integrationstests für die Ausgabeartefakte (AP07).
*
* <p>Prüft, dass nach einem Lauf beide Ausgabedateien (Berichtdatei {@code .txt}
* und Log-Datei {@code .log}) im Verzeichnis der Eingabedatei entstehen, dass die
* Suffix-Logik bei Folgeläufen greift und dass beide Dateien UTF-8 kodiert sind.</p>
*
* <p>Hinweis: Der {@link LoggingConfigurator} wird als No-Op-Mock eingesetzt, um das
* TempDir-Locking durch geöffnete Log4j2-FileAppender auf Windows zu vermeiden.
* Ein separater manueller End-to-End-Test mit dem Uber-JAR belegt, dass die echte
* Log-Datei-Umkonfiguration funktioniert.</p>
*/
class CliRunnerOutputArtifactsTest {
/**
* Erzeugt einen CliRunner mit DummyFileValidationService und No-Op-LoggingConfigurator.
*/
private CliRunner buildRunner() {
LoggingConfigurator noOpLogging = mock(LoggingConfigurator.class);
doNothing().when(noOpLogging).configureLogFile(any(Path.class));
SuffixResolver suffixResolver = new SuffixResolver();
ReportFileWriter reportFileWriter = new ReportFileWriter(suffixResolver);
return new CliRunner(
new DummyFileValidationService(),
noOpLogging,
suffixResolver,
reportFileWriter);
}
@Test
@DisplayName("Lauf 1: foo.auf → foo.auf.txt wird erzeugt")
void lauf1_erzeugtBerichtdateiOhneSuffix(@TempDir Path tempDir) throws IOException {
Path inputFile = tempDir.resolve("foo.auf");
Files.createFile(inputFile);
suppressStdout();
try {
buildRunner().run(new String[]{inputFile.toString()});
} finally {
restoreStdout();
}
Path expectedReport = tempDir.resolve("foo.auf.txt");
assertTrue(Files.exists(expectedReport),
"Nach Lauf 1 soll foo.auf.txt existieren");
}
@Test
@DisplayName("Lauf 2: foo.auf → foo.auf_v1.txt bei vorhandener foo.auf.txt")
void lauf2_erzeugtBerichtdateiMitV1Suffix(@TempDir Path tempDir) throws IOException {
Path inputFile = tempDir.resolve("foo.auf");
Files.createFile(inputFile);
suppressStdout();
try {
// Lauf 1
buildRunner().run(new String[]{inputFile.toString()});
// Lauf 2
buildRunner().run(new String[]{inputFile.toString()});
} finally {
restoreStdout();
}
assertTrue(Files.exists(tempDir.resolve("foo.auf.txt")),
"Lauf 1 soll foo.auf.txt erzeugt haben");
assertTrue(Files.exists(tempDir.resolve("foo.auf_v1.txt")),
"Lauf 2 soll foo.auf_v1.txt erzeugt haben");
}
@Test
@DisplayName("Lauf 3: foo.auf → foo.auf_v2.txt bei vorhandenen foo.auf.txt und foo.auf_v1.txt")
void lauf3_erzeugtBerichtdateiMitV2Suffix(@TempDir Path tempDir) throws IOException {
Path inputFile = tempDir.resolve("foo.auf");
Files.createFile(inputFile);
suppressStdout();
try {
buildRunner().run(new String[]{inputFile.toString()});
buildRunner().run(new String[]{inputFile.toString()});
buildRunner().run(new String[]{inputFile.toString()});
} finally {
restoreStdout();
}
assertTrue(Files.exists(tempDir.resolve("foo.auf_v2.txt")),
"Lauf 3 soll foo.auf_v2.txt erzeugt haben");
}
@Test
@DisplayName("Suffix-Zählung für .txt und .log ist unabhängig")
void suffixZaehlung_istProExtensionUnabhaengig(@TempDir Path tempDir) throws IOException {
Path inputFile = tempDir.resolve("bar.auf");
Files.createFile(inputFile);
// .txt voranlegen (simuliert bereits vorhandenen Bericht ohne Log)
Files.createFile(tempDir.resolve("bar.auf.txt"));
suppressStdout();
try {
buildRunner().run(new String[]{inputFile.toString()});
} finally {
restoreStdout();
}
// .txt bereits vorhanden → _v1.txt erzeugt
assertTrue(Files.exists(tempDir.resolve("bar.auf_v1.txt")),
"Vorhandene bar.auf.txt soll zu bar.auf_v1.txt führen");
// .log war nicht vorhanden → kein Suffix (wird vom No-Op-Logger nicht erzeugt;
// der SuffixResolver würde bar.auf.log zurückgeben, wenn configureLogFile aufgerufen wird)
}
@Test
@DisplayName("Berichtdatei ist in UTF-8 kodiert (enthält Umlaute korrekt)")
void berichtdatei_istInUtf8(@TempDir Path tempDir) throws IOException {
Path inputFile = tempDir.resolve("encoding.auf");
Files.createFile(inputFile);
suppressStdout();
try {
buildRunner().run(new String[]{inputFile.toString()});
} finally {
restoreStdout();
}
Path reportFile = tempDir.resolve("encoding.auf.txt");
assertTrue(Files.exists(reportFile));
byte[] bytes = Files.readAllBytes(reportFile);
String content = new String(bytes, StandardCharsets.UTF_8);
// Das Urteil "GÜLTIG" enthält das Umlaut Ü — wenn UTF-8 korrekt, dann lesbar
assertTrue(content.contains("GÜLTIG"),
"UTF-8-Datei soll 'GÜLTIG' mit Umlaut enthalten");
}
@Test
@DisplayName("Konsolenausgabe ist identisch zum Berichtdatei-Inhalt")
void konsolenausgabe_identischZumBerichtinhalt(@TempDir Path tempDir) throws IOException {
Path inputFile = tempDir.resolve("console.auf");
Files.createFile(inputFile);
PrintStream originalOut = System.out;
java.io.ByteArrayOutputStream outBuf = new java.io.ByteArrayOutputStream();
System.setOut(new PrintStream(outBuf, true, StandardCharsets.UTF_8));
try {
buildRunner().run(new String[]{inputFile.toString()});
} finally {
System.setOut(originalOut);
}
Path reportFile = tempDir.resolve("console.auf.txt");
String fileContent = Files.readString(reportFile, StandardCharsets.UTF_8);
String consoleContent = outBuf.toString(StandardCharsets.UTF_8);
assertEquals(fileContent, consoleContent,
"Konsolenausgabe soll identisch zum Berichtdatei-Inhalt sein");
}
// -----------------------------------------------------------------------
// Hilfsmethoden für stdout-Unterdrückung
// -----------------------------------------------------------------------
private PrintStream originalOut;
private void suppressStdout() {
originalOut = System.out;
System.setOut(new PrintStream(new java.io.ByteArrayOutputStream()));
}
private void restoreStdout() {
System.setOut(originalOut);
}
}
@@ -0,0 +1,248 @@
package de.gecheckt.asv.adapter.in.cli;
import de.gecheckt.asv.adapter.out.filesystem.SuffixResolver;
import de.gecheckt.asv.adapter.out.logging.LoggingConfigurator;
import de.gecheckt.asv.adapter.out.reporting.ReportFileWriter;
import de.gecheckt.asv.application.FileValidationService;
import de.gecheckt.asv.domain.finding.Finding;
import de.gecheckt.asv.domain.finding.FindingKind;
import de.gecheckt.asv.domain.finding.FindingLayer;
import de.gecheckt.asv.domain.finding.Severity;
import de.gecheckt.asv.domain.finding.ValidationReport;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
/**
* Unit-Tests für {@link CliRunner}.
*
* <p>Abgedeckte Abnahmekriterien aus AP06/AP07:</p>
* <ul>
* <li>Aufruf ohne Argument → Exit-Code 2</li>
* <li>Aufruf mit ≥ 2 Argumenten → Exit-Code 2</li>
* <li>Aufruf mit nicht existierender Datei → Exit-Code 2</li>
* <li>Aufruf mit leerer, lesbarer Datei → Exit-Code 0</li>
* <li>Konsolenausgabe enthält Berichtinhalt</li>
* <li>Berichtdatei wird im Verzeichnis der Eingabedatei erzeugt</li>
* </ul>
*
* <p>Hinweis: Der {@link LoggingConfigurator} wird in diesen Tests als Mockito-Mock
* verwendet (No-Op für {@code configureLogFile}), um zu verhindern, dass ein echter
* Log4j2-File-Appender im TempDir geöffnet bleibt und die TempDir-Bereinigung durch
* JUnit blockiert (Windows-spezifisches Dateisperrproblem).</p>
*/
class CliRunnerTest {
@TempDir
Path tempDir;
// -----------------------------------------------------------------------
// Hilfsmethode: CliRunner mit No-Op-LoggingConfigurator erzeugen
// -----------------------------------------------------------------------
/**
* Erzeugt einen {@link CliRunner} mit echten Adaptern (SuffixResolver, ReportFileWriter)
* und einem Mockito-Mock für LoggingConfigurator (No-Op für configureLogFile).
*/
private CliRunner runnerWith(FileValidationService service) {
LoggingConfigurator noOpLogging = mock(LoggingConfigurator.class);
doNothing().when(noOpLogging).configureLogFile(any(Path.class));
return new CliRunner(
service,
noOpLogging,
new SuffixResolver(),
new ReportFileWriter(new SuffixResolver()));
}
// -----------------------------------------------------------------------
// Argument-Validierung
// -----------------------------------------------------------------------
@Test
@DisplayName("Kein Argument → Exit-Code 2 (OPERATIONAL_ERROR)")
void keineArgumente_liefernExitCode2() {
FileValidationService service = mock(FileValidationService.class);
CliRunner runner = runnerWith(service);
ByteArrayOutputStream err = captureStderr();
int exitCode = runner.run(new String[]{});
restoreStderr();
assertEquals(ExitCode.OPERATIONAL_ERROR, exitCode);
assertTrue(err.toString().contains("Fehler"),
"STDERR soll eine deutsche Fehlermeldung enthalten");
verifyNoInteractions(service);
}
@Test
@DisplayName("Zwei Argumente → Exit-Code 2 (OPERATIONAL_ERROR)")
void zweiArgumente_liefernExitCode2() {
FileValidationService service = mock(FileValidationService.class);
CliRunner runner = runnerWith(service);
ByteArrayOutputStream err = captureStderr();
int exitCode = runner.run(new String[]{"datei1.txt", "datei2.txt"});
restoreStderr();
assertEquals(ExitCode.OPERATIONAL_ERROR, exitCode);
verifyNoInteractions(service);
}
// -----------------------------------------------------------------------
// Datei-Vorabprüfung
// -----------------------------------------------------------------------
@Test
@DisplayName("Nicht existierende Datei → Exit-Code 2 (OPERATIONAL_ERROR)")
void nichtExistierendeDatei_liefertExitCode2() {
FileValidationService service = mock(FileValidationService.class);
CliRunner runner = runnerWith(service);
ByteArrayOutputStream err = captureStderr();
int exitCode = runner.run(new String[]{"/nicht/vorhanden/datei.txt"});
restoreStderr();
assertEquals(ExitCode.OPERATIONAL_ERROR, exitCode);
assertTrue(err.toString().contains("Fehler"),
"STDERR soll eine deutsche Fehlermeldung enthalten");
verifyNoInteractions(service);
}
// -----------------------------------------------------------------------
// Erfolgreicher Lauf
// -----------------------------------------------------------------------
@Test
@DisplayName("Leere, lesbare Datei ohne Spec-Fehler → Exit-Code 0 (VALID)")
void leereLesbareDatei_liefertExitCode0() throws IOException {
Path testFile = tempDir.resolve("leer.auf");
Files.createFile(testFile);
ValidationReport emptyReport = new ValidationReport("leer.auf", Instant.now(), List.of());
FileValidationService service = mock(FileValidationService.class);
when(service.validate(any(Path.class))).thenReturn(emptyReport);
CliRunner runner = runnerWith(service);
int exitCode = runner.run(new String[]{testFile.toString()});
assertEquals(ExitCode.VALID, exitCode);
verify(service).validate(any(Path.class));
}
@Test
@DisplayName("Datei mit SPEC-ERROR-Befund → Exit-Code 1 (INVALID)")
void dateimitSpecFehler_liefertExitCode1() throws IOException {
Path testFile = tempDir.resolve("fehlerhaft.auf");
Files.writeString(testFile, "irgendein Inhalt");
Finding specError = Finding.builder(
FindingKind.SPEC, Severity.ERROR, FindingLayer.ARTIFACT, "Testfehler").build();
ValidationReport reportWithError = new ValidationReport(
"fehlerhaft.auf", Instant.now(), List.of(specError));
FileValidationService service = mock(FileValidationService.class);
when(service.validate(any(Path.class))).thenReturn(reportWithError);
CliRunner runner = runnerWith(service);
int exitCode = runner.run(new String[]{testFile.toString()});
assertEquals(ExitCode.INVALID, exitCode);
}
@Test
@DisplayName("ValidationReport mit operationalError → Exit-Code 2 (OPERATIONAL_ERROR)")
void operationalErrorReport_liefertExitCode2() throws IOException {
Path testFile = tempDir.resolve("bedien.auf");
Files.writeString(testFile, "Inhalt");
ValidationReport errReport = ValidationReport.operationalError(
"bedien.auf", "CLI-001", "Bedienfehler");
FileValidationService service = mock(FileValidationService.class);
when(service.validate(any(Path.class))).thenReturn(errReport);
CliRunner runner = runnerWith(service);
int exitCode = runner.run(new String[]{testFile.toString()});
assertEquals(ExitCode.OPERATIONAL_ERROR, exitCode);
}
@Test
@DisplayName("Konsolenausgabe (stdout) enthält Berichtinhalt mit GÜLTIG-Urteil")
void konsolenausgabe_enthaeltBerichtinhalt() throws IOException {
Path testFile = tempDir.resolve("konsole.auf");
Files.createFile(testFile);
ValidationReport emptyReport = new ValidationReport("konsole.auf", Instant.now(), List.of());
FileValidationService service = mock(FileValidationService.class);
when(service.validate(any(Path.class))).thenReturn(emptyReport);
PrintStream originalOut = System.out;
ByteArrayOutputStream outBuf = new ByteArrayOutputStream();
System.setOut(new PrintStream(outBuf));
try {
CliRunner runner = runnerWith(service);
runner.run(new String[]{testFile.toString()});
} finally {
System.setOut(originalOut);
}
String output = outBuf.toString();
assertTrue(output.contains("GÜLTIG"),
"Konsolenausgabe soll 'GÜLTIG' enthalten");
assertTrue(output.contains("ASV-Format-Validator"),
"Konsolenausgabe soll Berichtkopf enthalten");
}
@Test
@DisplayName("Berichtdatei wird nach Lauf im Verzeichnis der Eingabedatei erzeugt")
void berichtdatei_wirdNachLaufErzeugt() throws IOException {
Path testFile = tempDir.resolve("bericht.auf");
Files.createFile(testFile);
ValidationReport emptyReport = new ValidationReport("bericht.auf", Instant.now(), List.of());
FileValidationService service = mock(FileValidationService.class);
when(service.validate(any(Path.class))).thenReturn(emptyReport);
CliRunner runner = runnerWith(service);
runner.run(new String[]{testFile.toString()});
Path expectedReport = tempDir.resolve("bericht.auf.txt");
assertTrue(Files.exists(expectedReport),
"Berichtdatei soll existieren: " + expectedReport);
}
// -----------------------------------------------------------------------
// Hilfsmethoden
// -----------------------------------------------------------------------
private PrintStream originalStderr;
private ByteArrayOutputStream captureStderr() {
ByteArrayOutputStream buf = new ByteArrayOutputStream();
originalStderr = System.err;
System.setErr(new PrintStream(buf));
return buf;
}
private void restoreStderr() {
System.setErr(originalStderr);
}
}
@@ -0,0 +1,130 @@
package de.gecheckt.asv.adapter.out.filesystem;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
/**
* Unit-Tests für {@link SuffixResolver}.
*/
class SuffixResolverTest {
private final SuffixResolver resolver = new SuffixResolver();
// -------------------------------------------------------------------------
// Normalfälle
// -------------------------------------------------------------------------
@Test
@DisplayName("Keine Datei vorhanden → Basisname ohne Suffix")
void keineDateiVorhanden_gibtBasisnameOhneSuffix(@TempDir Path tempDir) {
Path result = resolver.resolveNextFreePath(tempDir, "foo.auf", "txt");
assertEquals(tempDir.resolve("foo.auf.txt"), result);
assertFalse(Files.exists(result), "Ergebnis darf noch nicht existieren");
}
@Test
@DisplayName("Basisname.txt vorhanden → _v1.txt")
void txtVorhanden_gibtV1Suffix(@TempDir Path tempDir) throws IOException {
Files.createFile(tempDir.resolve("foo.auf.txt"));
Path result = resolver.resolveNextFreePath(tempDir, "foo.auf", "txt");
assertEquals(tempDir.resolve("foo.auf_v1.txt"), result);
assertFalse(Files.exists(result), "Ergebnis darf noch nicht existieren");
}
@Test
@DisplayName("Basisname.txt und _v1.txt vorhanden → _v2.txt")
void txtUndV1Vorhanden_gibtV2Suffix(@TempDir Path tempDir) throws IOException {
Files.createFile(tempDir.resolve("foo.auf.txt"));
Files.createFile(tempDir.resolve("foo.auf_v1.txt"));
Path result = resolver.resolveNextFreePath(tempDir, "foo.auf", "txt");
assertEquals(tempDir.resolve("foo.auf_v2.txt"), result);
assertFalse(Files.exists(result), "Ergebnis darf noch nicht existieren");
}
@Test
@DisplayName("Suffix-Zählung ist pro Extension unabhängig: .txt zählt nicht für .log")
void txtZaehltNichtFuerLog(@TempDir Path tempDir) throws IOException {
// .txt vorhanden → für txt wäre _v1.txt nötig
Files.createFile(tempDir.resolve("foo.auf.txt"));
Files.createFile(tempDir.resolve("foo.auf_v1.txt"));
// .log ist unberührt → kein Suffix nötig
Path logResult = resolver.resolveNextFreePath(tempDir, "foo.auf", "log");
assertEquals(tempDir.resolve("foo.auf.log"), logResult);
assertFalse(Files.exists(logResult), "Ergebnis darf noch nicht existieren");
}
@Test
@DisplayName("Drei aufeinanderfolgende Läufe erzeugen korrekte Suffixfolge")
void dreiLaeufe_erzeugenKorrekteSuffixfolge(@TempDir Path tempDir) throws IOException {
// Lauf 1 → kein Suffix
Path first = resolver.resolveNextFreePath(tempDir, "bar.auf", "txt");
assertEquals(tempDir.resolve("bar.auf.txt"), first);
Files.createFile(first);
// Lauf 2 → _v1
Path second = resolver.resolveNextFreePath(tempDir, "bar.auf", "txt");
assertEquals(tempDir.resolve("bar.auf_v1.txt"), second);
Files.createFile(second);
// Lauf 3 → _v2
Path third = resolver.resolveNextFreePath(tempDir, "bar.auf", "txt");
assertEquals(tempDir.resolve("bar.auf_v2.txt"), third);
}
@Test
@DisplayName("Basisname mit Punkt (z.B. 'foo.auf') wird korrekt behandelt")
void baseName_mitPunkt_wirdKorrektBehandelt(@TempDir Path tempDir) {
Path result = resolver.resolveNextFreePath(tempDir, "foo.auf", "txt");
// Dateiname muss sein: foo.auf.txt (Punkt aus baseName + Punkt + ext)
assertEquals("foo.auf.txt", result.getFileName().toString());
}
// -------------------------------------------------------------------------
// Fehlerfälle
// -------------------------------------------------------------------------
@Test
@DisplayName("Null directory → IllegalArgumentException")
void nullDirectory_wirft_IllegalArgumentException() {
assertThrows(IllegalArgumentException.class,
() -> resolver.resolveNextFreePath(null, "foo", "txt"));
}
@Test
@DisplayName("Null baseName → IllegalArgumentException")
void nullBaseName_wirft_IllegalArgumentException(@TempDir Path tempDir) {
assertThrows(IllegalArgumentException.class,
() -> resolver.resolveNextFreePath(tempDir, null, "txt"));
}
@Test
@DisplayName("Leerer baseName → IllegalArgumentException")
void leererBaseName_wirft_IllegalArgumentException(@TempDir Path tempDir) {
assertThrows(IllegalArgumentException.class,
() -> resolver.resolveNextFreePath(tempDir, " ", "txt"));
}
@Test
@DisplayName("Null extension → IllegalArgumentException")
void nullExtension_wirft_IllegalArgumentException(@TempDir Path tempDir) {
assertThrows(IllegalArgumentException.class,
() -> resolver.resolveNextFreePath(tempDir, "foo", null));
}
}
@@ -0,0 +1,206 @@
package de.gecheckt.asv.adapter.out.reporting;
import de.gecheckt.asv.adapter.out.filesystem.SuffixResolver;
import de.gecheckt.asv.domain.finding.Finding;
import de.gecheckt.asv.domain.finding.FindingKind;
import de.gecheckt.asv.domain.finding.FindingLayer;
import de.gecheckt.asv.domain.finding.Severity;
import de.gecheckt.asv.domain.finding.ValidationReport;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Unit-Tests für {@link ReportFileWriter}.
*/
class ReportFileWriterTest {
private final SuffixResolver suffixResolver = new SuffixResolver();
private final ReportFileWriter writer = new ReportFileWriter(suffixResolver);
// -------------------------------------------------------------------------
// Grundfunktion: Datei wird erzeugt
// -------------------------------------------------------------------------
@Test
@DisplayName("Leerer Report → Berichtdatei wird im Verzeichnis der Eingabedatei erzeugt")
void leererReport_erzeugtBerichtdateiImEingabeverzeichnis(@TempDir Path tempDir) throws IOException {
Path inputFile = tempDir.resolve("test.auf");
Files.createFile(inputFile);
ValidationReport report = new ValidationReport("test.auf", Instant.now(), List.of());
ReportFileWriter.ReportWriteResult result = writer.write(report, inputFile);
assertTrue(result.isSuccess(), "Schreibvorgang soll erfolgreich sein");
assertEquals(tempDir.resolve("test.auf.txt"), result.reportPath());
assertTrue(Files.exists(result.reportPath()), "Berichtdatei soll existieren");
}
@Test
@DisplayName("Berichtdatei ist in UTF-8 kodiert (Sonderzeichen äöü߀)")
void berichtdatei_istInUtf8(@TempDir Path tempDir) throws IOException {
Path inputFile = tempDir.resolve("test.auf");
Files.createFile(inputFile);
Finding finding = Finding.builder(FindingKind.SPEC, Severity.ERROR,
FindingLayer.ARTIFACT, "Ungültiges Feld — Sonderzeichen: äöü߀").build();
ValidationReport report = new ValidationReport("test.auf", Instant.now(), List.of(finding));
ReportFileWriter.ReportWriteResult result = writer.write(report, inputFile);
assertTrue(result.isSuccess());
byte[] bytes = Files.readAllBytes(result.reportPath());
String content = new String(bytes, StandardCharsets.UTF_8);
assertTrue(content.contains("äöü߀"),
"UTF-8-dekodierter Inhalt soll Sonderzeichen enthalten");
}
@Test
@DisplayName("Kopfzeile enthält Zeitstempel, Eingabedatei und Urteil GÜLTIG")
void kopfzeile_enthaeltZeitstempelEingabedateiUrteil(@TempDir Path tempDir) throws IOException {
Path inputFile = tempDir.resolve("bar.auf");
Files.createFile(inputFile);
Instant now = Instant.parse("2026-04-20T10:30:00Z");
ValidationReport report = new ValidationReport("bar.auf", now, List.of());
ReportFileWriter.ReportWriteResult result = writer.write(report, inputFile);
String content = result.reportContent();
assertTrue(content.contains("2026-04-20T10:30:00Z"),
"Kopfzeile soll ISO-Zeitstempel enthalten");
assertTrue(content.contains("bar.auf"),
"Kopfzeile soll Dateinamen enthalten");
assertTrue(content.contains("GÜLTIG"),
"Kopfzeile soll Urteil 'GÜLTIG' enthalten");
}
@Test
@DisplayName("Pro Finding wird eine Zeile mit Severity, Kind, Layer und Meldung ausgegeben")
void proFinding_wirdEineZeileAusgegeben(@TempDir Path tempDir) throws IOException {
Path inputFile = tempDir.resolve("foo.auf");
Files.createFile(inputFile);
Finding finding = Finding.builder(FindingKind.SPEC, Severity.ERROR,
FindingLayer.TECHNICAL_STRUCTURE, "Pflichtfeld fehlt")
.fieldId("UNB_0020")
.build();
ValidationReport report = new ValidationReport("foo.auf", Instant.now(), List.of(finding));
ReportFileWriter.ReportWriteResult result = writer.write(report, inputFile);
String content = result.reportContent();
assertTrue(content.contains("[ERROR]"), "Zeile soll Severity enthalten");
assertTrue(content.contains("[SPEC]"), "Zeile soll Kind enthalten");
assertTrue(content.contains("[TECHNICAL_STRUCTURE]"), "Zeile soll Layer enthalten");
assertTrue(content.contains("UNB_0020"), "Zeile soll Feld-ID enthalten");
assertTrue(content.contains("Pflichtfeld fehlt"), "Zeile soll deutsche Meldung enthalten");
}
@Test
@DisplayName("Fußzeile enthält Hinweis auf M1-Platzhalter-Validator")
void fuszeile_enthaeltHinweisAufM1Platzhalter(@TempDir Path tempDir) throws IOException {
Path inputFile = tempDir.resolve("foo.auf");
Files.createFile(inputFile);
ValidationReport report = new ValidationReport("foo.auf", Instant.now(), List.of());
ReportFileWriter.ReportWriteResult result = writer.write(report, inputFile);
assertTrue(result.reportContent().contains("M1-Platzhalter"),
"Fußzeile soll Hinweis auf M1-Platzhalter enthalten");
}
@Test
@DisplayName("Zweiter Lauf → Suffix _v1.txt")
void zweiterLauf_gibtV1Suffix(@TempDir Path tempDir) throws IOException {
Path inputFile = tempDir.resolve("foo.auf");
Files.createFile(inputFile);
ValidationReport report = new ValidationReport("foo.auf", Instant.now(), List.of());
// Erster Lauf
ReportFileWriter.ReportWriteResult first = writer.write(report, inputFile);
assertEquals(tempDir.resolve("foo.auf.txt"), first.reportPath(),
"Erster Lauf soll keinen Suffix erzeugen");
// Zweiter Lauf
ValidationReport report2 = new ValidationReport("foo.auf", Instant.now(), List.of());
ReportFileWriter.ReportWriteResult second = writer.write(report2, inputFile);
assertEquals(tempDir.resolve("foo.auf_v1.txt"), second.reportPath(),
"Zweiter Lauf soll _v1-Suffix erzeugen");
}
@Test
@DisplayName("UNGÜLTIG-Urteil erscheint im Bericht")
void ungueltigUrteil_erscheintImBericht(@TempDir Path tempDir) throws IOException {
Path inputFile = tempDir.resolve("invalid.auf");
Files.createFile(inputFile);
Finding specError = Finding.builder(FindingKind.SPEC, Severity.ERROR,
FindingLayer.ARTIFACT, "Kritischer Fehler").build();
ValidationReport report = new ValidationReport(
"invalid.auf", Instant.now(), List.of(specError));
ReportFileWriter.ReportWriteResult result = writer.write(report, inputFile);
assertTrue(result.reportContent().contains("UNGÜLTIG"),
"Bericht soll 'UNGÜLTIG' bei SPEC-ERROR enthalten");
}
@Test
@DisplayName("Keine Befunde → Zeile 'Keine Befunde' erscheint im Bericht")
void keineBefunde_zeigtPlatzhaltertext(@TempDir Path tempDir) throws IOException {
Path inputFile = tempDir.resolve("leer.auf");
Files.createFile(inputFile);
ValidationReport report = new ValidationReport("leer.auf", Instant.now(), List.of());
ReportFileWriter.ReportWriteResult result = writer.write(report, inputFile);
assertTrue(result.reportContent().contains("Keine Befunde"),
"Bericht soll 'Keine Befunde' bei leerem Report enthalten");
}
// -------------------------------------------------------------------------
// Fehlerfälle
// -------------------------------------------------------------------------
@Test
@DisplayName("Null report → IllegalArgumentException")
void nullReport_wirft_IllegalArgumentException(@TempDir Path tempDir) {
Path inputFile = tempDir.resolve("foo.auf");
assertThrows(IllegalArgumentException.class,
() -> writer.write(null, inputFile));
}
@Test
@DisplayName("Null inputFilePath → IllegalArgumentException")
void nullInputFilePath_wirft_IllegalArgumentException() {
ValidationReport report = new ValidationReport("foo.auf", Instant.now(), List.of());
assertThrows(IllegalArgumentException.class,
() -> writer.write(report, null));
}
@Test
@DisplayName("ReportWriteResult.isSuccess gibt true zurück wenn reportPath gesetzt")
void reportWriteResult_isSuccess_true_wenn_Pfad_gesetzt() {
ReportFileWriter.ReportWriteResult result =
new ReportFileWriter.ReportWriteResult("inhalt", Path.of("foo.txt"), null);
assertTrue(result.isSuccess());
assertNotNull(result.reportPath());
assertNull(result.writeException());
}
}
@@ -0,0 +1,75 @@
package de.gecheckt.asv.application;
import de.gecheckt.asv.domain.finding.ValidationReport;
import de.gecheckt.asv.domain.finding.Verdict;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/**
* Tests für {@link DummyFileValidationService}.
*
* <p>Schwerpunkt: Nachweis der korrekten ISO-8859-15-Dekodierung gemäß AP06-Abnahmekriterium
* „Byte {@code 0xA4} ergibt Euro-Zeichen €".</p>
*/
class DummyFileValidationServiceTest {
@TempDir
Path tempDir;
@Test
@DisplayName("Leere Datei liefert leeren ValidationReport mit Verdict VALID")
void leereDatei_liefertLeerenReport() throws IOException {
Path leereDatei = tempDir.resolve("leer.txt");
Files.createFile(leereDatei);
DummyFileValidationService service = new DummyFileValidationService();
ValidationReport report = service.validate(leereDatei);
assertNotNull(report);
assertEquals(Verdict.VALID, report.computeVerdict());
assertEquals("leer.txt", report.getFileName());
assertNotNull(report.getTimestamp());
}
@Test
@DisplayName("Datei mit Inhalt liefert ValidationReport mit Verdict VALID (kein Spec-Fehler in M1)")
void dateiMitInhalt_liefertVALID() throws IOException {
Path datei = tempDir.resolve("inhalt.txt");
Files.writeString(datei, "Test-Inhalt");
DummyFileValidationService service = new DummyFileValidationService();
ValidationReport report = service.validate(datei);
assertEquals(Verdict.VALID, report.computeVerdict());
}
@Test
@DisplayName("Byte 0xA4 in ISO-8859-15 wird als Euro-Zeichen € dekodiert")
void byte0xA4_wirdAlsEuroZeichenDekodiert() throws IOException {
// Byte 0xA4 ist in ISO-8859-15 dem Euro-Zeichen € zugewiesen.
// (In ISO-8859-1 wäre 0xA4 das Währungszeichen ¤ — daher dieser Test.)
byte[] inhalt = new byte[]{0x54, 0x65, 0x73, 0x74, (byte) 0xA4}; // "Test" + 0xA4
Path datei = tempDir.resolve("euro.bin");
Files.write(datei, inhalt);
// Dekodierung mit dem in DummyFileValidationService verwendeten Charset
String dekodiert = new String(inhalt, DummyFileValidationService.INPUT_CHARSET);
assertEquals("Test€", dekodiert,
"Byte 0xA4 muss in ISO-8859-15 als Euro-Zeichen € dekodiert werden");
}
@Test
@DisplayName("INPUT_CHARSET ist ISO-8859-15 (nicht UTF-8, nicht Plattform-Default)")
void inputCharset_istISO8859_15() {
assertEquals("ISO-8859-15", DummyFileValidationService.INPUT_CHARSET.name());
}
}
@@ -0,0 +1,117 @@
package de.gecheckt.asv.bootstrap;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import de.gecheckt.asv.application.DefaultInputFileValidator;
import de.gecheckt.asv.application.model.ValidationResult;
import de.gecheckt.asv.domain.model.Field;
import de.gecheckt.asv.domain.model.InputFile;
import de.gecheckt.asv.domain.model.Message;
import de.gecheckt.asv.domain.model.Segment;
/**
* Integrationstest für den M1-Einfrierzustand der Preview-Validatoren.
*
* <p>Belegt, dass ein Lauf mit {@link NoOpStructureValidator} und {@link NoOpFieldValidator}
* keinerlei ASVREC-/ASVFEH-Segmentbefunde erzeugt — unabhängig davon, ob die Eingabedatei
* ASV-Strukturmerkmale enthält oder nicht.</p>
*
* <p>Dieser Test ist der formale Nachweis für AP09-Abnahmekriterium
* „Integrationstest: Lauf mit Testdatei erzeugt keine ASVREC-/ASVFEH-Segmentbefunde".</p>
*/
class NoOpValidatorsIntegrationTest {
/**
* Erstellt einen {@link DefaultInputFileValidator} mit den M1-NoOp-Implementierungen.
*/
private DefaultInputFileValidator buildValidator() {
return new DefaultInputFileValidator(
new NoOpStructureValidator(),
new NoOpFieldValidator()
);
}
@Test
@DisplayName("KRITISCH: NoOp-Validatoren erzeugen keine Befunde für ASVREC-Struktur")
void asvrecStruktur_erzeugtKeineBefunde() {
// Given: eine vollständige ASVREC-Nachricht (die DefaultStructureValidator prüfen würde)
Segment unh = new Segment("UNH", 1, List.of(new Field(1, "12345"), new Field(2, "ASVREC:D:03B:UN:EAN008")));
Segment ifa = new Segment("IFA", 2, List.of(new Field(1, "IFA-Wert")));
Segment rea = new Segment("REA", 3, List.of(new Field(1, "100,00"), new Field(2, "X"), new Field(3, "Y"), new Field(4, "0")));
Segment dgn = new Segment("DGN", 4, List.of(new Field(1, "DGN-Wert")));
Segment lea = new Segment("LEA", 5, List.of(new Field(1, "LEA-Wert")));
Segment iva = new Segment("IVA", 6, List.of(new Field(1, "IVA-Wert")));
Segment unt = new Segment("UNT", 7, List.of(new Field(1, "7"), new Field(2, "12345")));
Message message = new Message(1, List.of(unh, ifa, rea, dgn, lea, iva, unt));
InputFile inputFile = new InputFile("testdatei.asv", List.of(message));
// When
ValidationResult result = buildValidator().validate(inputFile);
// Then: keinerlei Befunde — NoOp-Validatoren produzieren nie Findings
assertTrue(result.getAllErrors().isEmpty(),
"NoOp-Validatoren dürfen keinerlei Befunde erzeugen, gefunden: " + result.getAllErrors());
assertFalse(result.hasErrors(), "Keine Fehler erwartet");
assertFalse(result.hasWarnings(), "Keine Warnungen erwartet");
}
@Test
@DisplayName("NoOp-Validatoren erzeugen keine Befunde für ASVFEH-Struktur")
void asvfehStruktur_erzeugtKeineBefunde() {
// Given: eine ASVFEH-Nachricht mit FHL-Segment
Segment unh = new Segment("UNH", 1, List.of(new Field(1, "99999"), new Field(2, "ASVFEH:D:03B:UN:EAN008")));
Segment fhl = new Segment("FHL", 2, List.of(new Field(1, "Fehler-Hinweis")));
Segment unt = new Segment("UNT", 3, List.of(new Field(1, "3"), new Field(2, "99999")));
Message message = new Message(1, List.of(unh, fhl, unt));
InputFile inputFile = new InputFile("testdatei-feh.asv", List.of(message));
// When
ValidationResult result = buildValidator().validate(inputFile);
// Then: keinerlei Befunde
assertTrue(result.getAllErrors().isEmpty(),
"NoOp-Validatoren dürfen keinerlei Befunde erzeugen, gefunden: " + result.getAllErrors());
}
@Test
@DisplayName("NoOp-Validatoren erzeugen keine Befunde für leere Eingabedatei")
void leereEingabedatei_erzeugtKeineBefunde() {
// Given: eine Eingabedatei ohne Nachrichten
InputFile inputFile = new InputFile("leer.asv", List.of());
// When
ValidationResult result = buildValidator().validate(inputFile);
// Then: keinerlei Befunde — auch STRUCTURE_001 (fehlende Nachricht) wird nicht gemeldet
assertTrue(result.getAllErrors().isEmpty(),
"NoOp-Validatoren dürfen auch für leere Eingabedatei keine Befunde erzeugen");
}
@Test
@DisplayName("NoOpStructureValidator wirft IllegalArgumentException bei null-Eingabe")
void noOpStructureValidator_wirftExceptionBeiNull() {
NoOpStructureValidator validator = new NoOpStructureValidator();
org.junit.jupiter.api.Assertions.assertThrows(
IllegalArgumentException.class,
() -> validator.validate(null),
"Null-Eingabe muss IllegalArgumentException auslösen"
);
}
@Test
@DisplayName("NoOpFieldValidator wirft IllegalArgumentException bei null-Eingabe")
void noOpFieldValidator_wirftExceptionBeiNull() {
NoOpFieldValidator validator = new NoOpFieldValidator();
org.junit.jupiter.api.Assertions.assertThrows(
IllegalArgumentException.class,
() -> validator.validate(null),
"Null-Eingabe muss IllegalArgumentException auslösen"
);
}
}
@@ -0,0 +1,124 @@
package de.gecheckt.asv.domain.finding;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit-Tests für {@link Finding}.
*/
class FindingTest {
@Test
@DisplayName("Finding-Record mit allen Feldern ist korrekt befüllbar")
void findingRecordAllFields() {
Finding f = new Finding(
FindingKind.SPEC,
Severity.ERROR,
FindingLayer.TECHNICAL_STRUCTURE,
"RULE-001",
"1A001",
"UNB",
0,
"UNB_0020",
"FALSCH",
42,
"00001",
"Referenznummer stimmt nicht überein."
);
assertEquals(FindingKind.SPEC, f.kind());
assertEquals(Severity.ERROR, f.severity());
assertEquals(FindingLayer.TECHNICAL_STRUCTURE, f.layer());
assertEquals("RULE-001", f.ruleId());
assertEquals("1A001", f.officialErrorCode());
assertEquals("UNB", f.segmentType());
assertEquals(0, f.segmentIndex());
assertEquals("UNB_0020", f.fieldId());
assertEquals("FALSCH", f.rawValue());
assertEquals(42, f.position());
assertEquals("00001", f.messageReference());
assertEquals("Referenznummer stimmt nicht überein.", f.germanMessage());
}
@Test
@DisplayName("Finding-Builder befüllt Pflichtfelder korrekt, optionale Felder null")
void findingBuilderMindestfelder() {
Finding f = Finding.builder(
FindingKind.DIAGNOSTIC, Severity.HINT, FindingLayer.ARTIFACT,
"Hinweis auf mögliche Anomalie.")
.build();
assertEquals(FindingKind.DIAGNOSTIC, f.kind());
assertEquals(Severity.HINT, f.severity());
assertEquals(FindingLayer.ARTIFACT, f.layer());
assertEquals("Hinweis auf mögliche Anomalie.", f.germanMessage());
assertNull(f.ruleId());
assertNull(f.officialErrorCode());
assertNull(f.segmentType());
assertNull(f.segmentIndex());
assertNull(f.fieldId());
assertNull(f.rawValue());
assertNull(f.position());
assertNull(f.messageReference());
}
@Test
@DisplayName("Finding-Builder befüllt optionale Felder korrekt")
void findingBuilderOptionalFelder() {
Finding f = Finding.builder(
FindingKind.SPEC, Severity.WARNING, FindingLayer.DOMAIN_MODEL,
"Segment fehlt.")
.ruleId("RULE-042")
.officialErrorCode("2A001")
.segmentType("REA")
.segmentIndex(5)
.fieldId("REA_0010")
.rawValue("")
.position(1024)
.messageReference("MSG00001")
.build();
assertEquals("RULE-042", f.ruleId());
assertEquals("2A001", f.officialErrorCode());
assertEquals("REA", f.segmentType());
assertEquals(5, f.segmentIndex());
assertEquals("REA_0010", f.fieldId());
assertEquals("", f.rawValue());
assertEquals(1024, f.position());
assertEquals("MSG00001", f.messageReference());
}
@Test
@DisplayName("isSpecError() gibt true nur bei SPEC+ERROR")
void isSpecErrorNurBeiSpecUndError() {
Finding specError = Finding.builder(FindingKind.SPEC, Severity.ERROR,
FindingLayer.ARTIFACT, "Fehler.").build();
Finding specWarning = Finding.builder(FindingKind.SPEC, Severity.WARNING,
FindingLayer.ARTIFACT, "Warnung.").build();
Finding diagError = Finding.builder(FindingKind.DIAGNOSTIC, Severity.ERROR,
FindingLayer.ARTIFACT, "Diagnose-Fehler.").build();
assertTrue(specError.isSpecError(), "SPEC+ERROR muss isSpecError() = true ergeben.");
assertFalse(specWarning.isSpecError(), "SPEC+WARNING muss isSpecError() = false ergeben.");
assertFalse(diagError.isSpecError(), "DIAGNOSTIC+ERROR muss isSpecError() = false ergeben.");
}
@Test
@DisplayName("Finding-Konstruktor wirft NullPointerException bei null-germanMessage")
void konstruktorWirftNPEBeiNullGermanMessage() {
assertThrows(NullPointerException.class,
() -> new Finding(FindingKind.SPEC, Severity.ERROR, FindingLayer.ARTIFACT,
null, null, null, null, null, null, null, null, null));
}
@Test
@DisplayName("Finding-Konstruktor wirft NullPointerException bei null-kind")
void konstruktorWirftNPEBeiNullKind() {
assertThrows(NullPointerException.class,
() -> new Finding(null, Severity.ERROR, FindingLayer.ARTIFACT,
null, null, null, null, null, null, null, null, "Meldung."));
}
}
@@ -0,0 +1,212 @@
package de.gecheckt.asv.domain.finding;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit-Tests für {@link ValidationReport}.
*
* <p>Die kritische Invariante lautet: Nur SPEC-ERROR-Befunde führen zu {@link Verdict#INVALID}.
* Diagnostische Befunde — auch solche mit {@link Severity#ERROR} — dürfen das Urteil
* niemals auf {@link Verdict#INVALID} setzen.</p>
*/
class ValidationReportTest {
// ---------------------------------------------------------------------------
// Hilfsmethoden
// ---------------------------------------------------------------------------
private static Finding specError() {
return Finding.builder(FindingKind.SPEC, Severity.ERROR, FindingLayer.ARTIFACT,
"Pflichtfeld fehlt.").build();
}
private static Finding specWarning() {
return Finding.builder(FindingKind.SPEC, Severity.WARNING, FindingLayer.ARTIFACT,
"Dateiname weicht vom Schema ab.").build();
}
private static Finding diagnosticError() {
return Finding.builder(FindingKind.DIAGNOSTIC, Severity.ERROR, FindingLayer.TECHNICAL_STRUCTURE,
"Diagnostischer Fehler — beeinflusst das Urteil nicht.").build();
}
private static Finding diagnosticWarning() {
return Finding.builder(FindingKind.DIAGNOSTIC, Severity.WARNING, FindingLayer.DOMAIN_MODEL,
"Diagnostische Warnung.").build();
}
private static ValidationReport emptyReport() {
return new ValidationReport("testdatei.txt", Instant.now(), List.of());
}
// ---------------------------------------------------------------------------
// Test 1: Leerer Report → VALID
// ---------------------------------------------------------------------------
@Test
@DisplayName("Leerer Report liefert Verdict VALID")
void leeremReportLiefertVALID() {
ValidationReport report = emptyReport();
assertEquals(Verdict.VALID, report.computeVerdict(),
"Ein leerer Report ohne Befunde muss VALID liefern.");
}
// ---------------------------------------------------------------------------
// Test 2: Ein SPEC-ERROR → INVALID
// ---------------------------------------------------------------------------
@Test
@DisplayName("Ein SPEC-ERROR-Befund liefert Verdict INVALID")
void specErrorLiefertINVALID() {
ValidationReport report = new ValidationReport(
"testdatei.txt", Instant.now(), List.of(specError()));
assertEquals(Verdict.INVALID, report.computeVerdict(),
"Ein SPEC-ERROR-Befund muss zu INVALID führen.");
}
// ---------------------------------------------------------------------------
// Test 3 (kritisch): Ein DIAGNOSTIC-ERROR → VALID
// ---------------------------------------------------------------------------
@Test
@DisplayName("KRITISCH: Ein DIAGNOSTIC-ERROR-Befund liefert Verdict VALID (niemals INVALID)")
void diagnosticErrorLiefertVALID() {
ValidationReport report = new ValidationReport(
"testdatei.txt", Instant.now(), List.of(diagnosticError()));
assertEquals(Verdict.VALID, report.computeVerdict(),
"Ein DIAGNOSTIC-ERROR-Befund darf das Urteil NIEMALS auf INVALID setzen. "
+ "Nur SPEC+ERROR zählt für das Gesamturteil.");
}
// ---------------------------------------------------------------------------
// Test 4: SPEC-WARNING → VALID
// ---------------------------------------------------------------------------
@Test
@DisplayName("Ein SPEC-WARNING-Befund liefert Verdict VALID")
void specWarningLiefertVALID() {
ValidationReport report = new ValidationReport(
"testdatei.txt", Instant.now(), List.of(specWarning()));
assertEquals(Verdict.VALID, report.computeVerdict(),
"Nur SPEC+ERROR macht eine Datei ungültig — Warnungen nicht.");
}
// ---------------------------------------------------------------------------
// Test 5: specFindings() / diagnosticFindings() filtern korrekt
// ---------------------------------------------------------------------------
@Test
@DisplayName("specFindings() und diagnosticFindings() filtern korrekt")
void findingsFilterungKorrekt() {
Finding spec1 = specError();
Finding spec2 = specWarning();
Finding diag1 = diagnosticError();
Finding diag2 = diagnosticWarning();
ValidationReport report = new ValidationReport(
"testdatei.txt", Instant.now(), List.of(spec1, diag1, spec2, diag2));
List<Finding> specList = report.specFindings();
List<Finding> diagList = report.diagnosticFindings();
assertEquals(2, specList.size(), "specFindings() muss genau 2 SPEC-Befunde zurückliefern.");
assertEquals(2, diagList.size(), "diagnosticFindings() muss genau 2 DIAGNOSTIC-Befunde zurückliefern.");
assertTrue(specList.stream().allMatch(f -> f.kind() == FindingKind.SPEC),
"specFindings() darf nur SPEC-Befunde enthalten.");
assertTrue(diagList.stream().allMatch(f -> f.kind() == FindingKind.DIAGNOSTIC),
"diagnosticFindings() darf nur DIAGNOSTIC-Befunde enthalten.");
}
// ---------------------------------------------------------------------------
// Test 6: findings-Liste ist nicht von außen modifizierbar
// ---------------------------------------------------------------------------
@Test
@DisplayName("Die findings-Liste ist von außen nicht modifizierbar")
void findingsListeNichtModifizierbar() {
List<Finding> mutableList = new ArrayList<>();
mutableList.add(specError());
ValidationReport report = new ValidationReport("testdatei.txt", Instant.now(), mutableList);
// Versuch 1: Originalliste nach Übergabe verändern
mutableList.add(diagnosticError());
assertEquals(1, report.getFindings().size(),
"Änderungen an der Eingabeliste dürfen den Report nicht beeinflussen.");
// Versuch 2: Zurückgegebene Liste direkt verändern
assertThrows(UnsupportedOperationException.class,
() -> report.getFindings().add(specWarning()),
"getFindings() muss eine unveränderliche Liste zurückliefern.");
}
// ---------------------------------------------------------------------------
// Test 7: operationalError(...) → OPERATIONAL_ERROR
// ---------------------------------------------------------------------------
@Test
@DisplayName("operationalError-Factory liefert Verdict OPERATIONAL_ERROR")
void operationalErrorFactoryLiefertOPERATIONAL_ERROR() {
ValidationReport report = ValidationReport.operationalError(
"nichtvorhanden.txt", "SYS-001", "Eingabedatei nicht lesbar.");
assertEquals(Verdict.OPERATIONAL_ERROR, report.computeVerdict(),
"operationalError() muss immer OPERATIONAL_ERROR liefern.");
assertFalse(report.getFindings().isEmpty(),
"Der Bedienfehler-Bericht muss mindestens einen Befund enthalten.");
assertEquals("nichtvorhanden.txt", report.getFileName());
}
// ---------------------------------------------------------------------------
// Zusätzliche Absicherung: DIAGNOSTIC-ERROR gemeinsam mit SPEC-ERROR
// ---------------------------------------------------------------------------
@Test
@DisplayName("SPEC-ERROR + DIAGNOSTIC-ERROR: Verdict ist INVALID (wegen SPEC-ERROR)")
void specErrorUndDiagnosticError_liefertINVALID() {
ValidationReport report = new ValidationReport(
"testdatei.txt", Instant.now(), List.of(specError(), diagnosticError()));
assertEquals(Verdict.INVALID, report.computeVerdict(),
"Wenn sowohl SPEC-ERROR als auch DIAGNOSTIC-ERROR vorliegen, muss INVALID gelten "
+ "— aber nur wegen des SPEC-ERROR.");
}
// ---------------------------------------------------------------------------
// Zusätzliche Absicherung: hasSpecErrors()
// ---------------------------------------------------------------------------
@Test
@DisplayName("hasSpecErrors() gibt false zurück bei leerem Report")
void hasSpecErrors_leerReport() {
assertFalse(emptyReport().hasSpecErrors());
}
@Test
@DisplayName("hasSpecErrors() gibt true zurück bei SPEC-ERROR")
void hasSpecErrors_mitSpecError() {
ValidationReport report = new ValidationReport(
"testdatei.txt", Instant.now(), List.of(specError()));
assertTrue(report.hasSpecErrors());
}
@Test
@DisplayName("hasSpecErrors() gibt false zurück bei nur DIAGNOSTIC-ERROR")
void hasSpecErrors_nurDiagnosticError() {
ValidationReport report = new ValidationReport(
"testdatei.txt", Instant.now(), List.of(diagnosticError()));
assertFalse(report.hasSpecErrors(),
"DIAGNOSTIC-ERROR darf hasSpecErrors() nicht auf true setzen.");
}
}
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_ERR">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="WARN">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
+5
View File
@@ -0,0 +1,5 @@
ASV-Format-Validator Testdatei M1
Minimale Eingabedatei fuer End-to-End-Abnahme
Kein gueltiges EDIFACT, keine echten ASV-Daten
Zeichensatz: ISO-8859-15 kompatibel (keine Sonderzeichen)
Lauf 1 erwartet Exit-Code 0 (GUELTIG mangels Pruefregel)
+4
View File
@@ -0,0 +1,4 @@
2026-04-20 09:41:04 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - ASV-Format-Validator gestartet. Eingabedatei: D:\Dev\Projects\asv-format-validator\test-artefakte\m1\minimal.txt
2026-04-20 09:41:04 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'minimal.txt' gelesen (242 Bytes, 242 Zeichen, Encoding: ISO-8859-15)
2026-04-20 09:41:04 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: minimal.txt, Urteil: VALID
2026-04-20 09:41:04 [main] INFO de.gecheckt.asv.adapter.out.reporting.ReportFileWriter - Berichtdatei geschrieben: D:\Dev\Projects\asv-format-validator\test-artefakte\m1\minimal.txt.txt
+13
View File
@@ -0,0 +1,13 @@
================================================================
ASV-Format-Validator Prüfbericht
================================================================
Zeitstempel : 2026-04-20T07:41:04.8214296Z
Eingabedatei: D:\Dev\Projects\asv-format-validator\test-artefakte\m1\minimal.txt
Urteil : GÜLTIG
----------------------------------------------------------------
Keine Befunde.
----------------------------------------------------------------
Hinweis: Dieser Bericht wurde mit dem M1-Platzhalter-Validator
erzeugt. Viele Prüfbereiche (Fachmodell, Inhalt, Referenzdaten)
werden erst ab M3 aktiv geprüft.
================================================================
+4
View File
@@ -0,0 +1,4 @@
2026-04-20 09:41:09 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - ASV-Format-Validator gestartet. Eingabedatei: D:\Dev\Projects\asv-format-validator\test-artefakte\m1\minimal.txt
2026-04-20 09:41:09 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'minimal.txt' gelesen (242 Bytes, 242 Zeichen, Encoding: ISO-8859-15)
2026-04-20 09:41:09 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: minimal.txt, Urteil: VALID
2026-04-20 09:41:09 [main] INFO de.gecheckt.asv.adapter.out.reporting.ReportFileWriter - Berichtdatei geschrieben: D:\Dev\Projects\asv-format-validator\test-artefakte\m1\minimal.txt_v1.txt
+13
View File
@@ -0,0 +1,13 @@
================================================================
ASV-Format-Validator Prüfbericht
================================================================
Zeitstempel : 2026-04-20T07:41:09.3312576Z
Eingabedatei: D:\Dev\Projects\asv-format-validator\test-artefakte\m1\minimal.txt
Urteil : GÜLTIG
----------------------------------------------------------------
Keine Befunde.
----------------------------------------------------------------
Hinweis: Dieser Bericht wurde mit dem M1-Platzhalter-Validator
erzeugt. Viele Prüfbereiche (Fachmodell, Inhalt, Referenzdaten)
werden erst ab M3 aktiv geprüft.
================================================================