1
0

M3-AP-006: Fehlerklassifikation vereinfacht und Logging auf korrekte

Ergebnisfälle ausgerichtet
This commit is contained in:
2026-04-01 21:45:06 +02:00
parent d60d050948
commit 4d769643d4
13 changed files with 557 additions and 42 deletions

View File

@@ -0,0 +1,33 @@
package de.gecheckt.pdf.umbenenner.domain.model;
/**
* Sealed interface representing the complete outcome of M3 document processing.
* <p>
* This interface models all four possible M3 document outcomes:
* <ul>
* <li>{@link M3PreCheckPassed}: Document passed all M3 pre-checks</li>
* <li>{@link M3PreCheckFailed}: Document failed a pre-check (deterministic content error: no usable text or page limit exceeded)</li>
* <li>{@link M3TechnicalDocumentError}: Technical failure during candidate access or PDF extraction (I/O, access, parsing, etc.)</li>
* </ul>
* <p>
* Design principles:
* <ul>
* <li>Exhaustive: All M3 document processing outcomes are covered (exactly four cases)</li>
* <li>Document-centric: Each outcome carries the source candidate for correlation and traceability</li>
* <li>No exceptions: Results are encoded in the type system</li>
* <li>Clear distinction: Deterministic content errors (M3PreCheckFailed) vs. technical failures (M3TechnicalDocumentError)</li>
* </ul>
* <p>
* Error classification:
* <ul>
* <li>M3PreCheckPassed: Extraction succeeded and all pre-checks passed (ready for M4+)</li>
* <li>M3PreCheckFailed: Extraction succeeded but deterministic content check failed (no usable text, page limit exceeded)</li>
* <li>M3TechnicalDocumentError: Extraction failed due to technical issue (I/O, file access, PDF parsing, etc.)</li>
* </ul>
*
* @since M3-AP-006
*/
public sealed interface M3DocumentProcessingOutcome
permits M3PreCheckPassed, M3PreCheckFailed, M3TechnicalDocumentError {
// Marker interface; concrete implementations define structure
}

View File

@@ -27,7 +27,7 @@ import java.util.Objects;
public record M3PreCheckFailed(
SourceDocumentCandidate candidate,
String failureReason
) implements M3ProcessingDecision {
) implements M3ProcessingDecision, M3DocumentProcessingOutcome {
/**
* Constructor with validation.
*

View File

@@ -21,7 +21,7 @@ import java.util.Objects;
public record M3PreCheckPassed(
SourceDocumentCandidate candidate,
PdfExtractionSuccess extraction
) implements M3ProcessingDecision {
) implements M3ProcessingDecision, M3DocumentProcessingOutcome {
/**
* Constructor with validation.
*

View File

@@ -0,0 +1,52 @@
package de.gecheckt.pdf.umbenenner.domain.model;
import java.util.Objects;
/**
* Represents a technical (infrastructure) failure during candidate access or PDF extraction.
* <p>
* This outcome indicates that a document could not be processed due to technical infrastructure failures,
* such as I/O errors, file access problems, or extraction engine failures.
* <p>
* These are typically retryable conditions, as they may be transient issues that could succeed
* in a later batch run.
* <p>
* Examples:
* <ul>
* <li>File not readable due to permissions</li>
* <li>File disappeared between discovery and extraction</li>
* <li>I/O error during file read</li>
* <li>Extraction engine (PDFBox) internal failure</li>
* <li>Out of memory during extraction</li>
* </ul>
* <p>
* This is distinct from {@link M3ExtractedContentError}, which represents problems with the document
* content itself rather than infrastructure failures.
*
* @param candidate the source document metadata
* @param errorMessage a description of the technical failure
* @param cause the underlying exception, if any (may be null)
* @since M3-AP-006
*/
public record M3TechnicalDocumentError(
SourceDocumentCandidate candidate,
String errorMessage,
Throwable cause
) implements M3DocumentProcessingOutcome {
/**
* Constructor with validation.
*
* @param candidate must be non-null
* @param errorMessage must be non-null and non-empty
* @param cause may be null
* @throws NullPointerException if candidate or errorMessage is null
* @throws IllegalArgumentException if errorMessage is empty
*/
public M3TechnicalDocumentError {
Objects.requireNonNull(candidate, "candidate must not be null");
Objects.requireNonNull(errorMessage, "errorMessage must not be null");
if (errorMessage.isEmpty()) {
throw new IllegalArgumentException("errorMessage must not be empty");
}
}
}

View File

@@ -16,12 +16,23 @@
* Additional classes introduced in M3:
* <ul>
* <li>{@link de.gecheckt.pdf.umbenenner.domain.model.M3PreCheckFailureReason} — enumeration of M3 pre-check failure reasons (M3-AP-004)</li>
* <li>{@link de.gecheckt.pdf.umbenenner.domain.model.M3DocumentProcessingOutcome} — sealed interface for all M3 document processing outcomes (M3-AP-006)</li>
* <li>{@link de.gecheckt.pdf.umbenenner.domain.model.M3TechnicalDocumentError} — technical failure during extraction (M3-AP-006)</li>
* </ul>
*
* Implementation classes:
* <ul>
* <li>{@link de.gecheckt.pdf.umbenenner.domain.model.M3PreCheckPassed} — document passed M3 pre-checks (M3-AP-001, M3-AP-004)</li>
* <li>{@link de.gecheckt.pdf.umbenenner.domain.model.M3PreCheckFailed} — document failed M3 pre-check (M3-AP-001, M3-AP-004)</li>
* <li>{@link de.gecheckt.pdf.umbenenner.domain.model.M3PreCheckPassed} — document passed M3 pre-checks (M3-AP-001, M3-AP-004, M3-AP-006)</li>
* <li>{@link de.gecheckt.pdf.umbenenner.domain.model.M3PreCheckFailed} — document failed M3 pre-check (M3-AP-001, M3-AP-004, M3-AP-006)</li>
* </ul>
*
* M3 Document Processing Outcome Model (M3-AP-006):
* The complete M3 document processing pipeline results in one of three outcomes, all implementing
* {@link de.gecheckt.pdf.umbenenner.domain.model.M3DocumentProcessingOutcome}:
* <ul>
* <li>Pre-check passed: Document text extracted and validated successfully (ready for M4+)</li>
* <li>Pre-check failed: Deterministic content error (no usable text, page limit exceeded)</li>
* <li>Technical document error: Infrastructure or parsing failure (I/O, access, PDF parsing)</li>
* </ul>
*
* All classes in this package are:

View File

@@ -0,0 +1,113 @@
package de.gecheckt.pdf.umbenenner.domain.model;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests for M3 document processing outcome types.
* <p>
* Verifies that all four outcome types are properly created and validated.
*/
class M3DocumentProcessingOutcomeTest {
@TempDir
Path tempDir;
private SourceDocumentCandidate candidate;
@BeforeEach
void setUp() throws Exception {
Path pdfFile = tempDir.resolve("doc.pdf");
Files.createFile(pdfFile);
SourceDocumentLocator locator = new SourceDocumentLocator(pdfFile.toString());
candidate = new SourceDocumentCandidate("doc.pdf", 1024L, locator);
}
@Test
void testM3TechnicalDocumentError_ValidConstruction() {
// Act
var error = new M3TechnicalDocumentError(candidate, "I/O error", null);
// Assert
assertEquals(candidate, error.candidate());
assertEquals("I/O error", error.errorMessage());
assertNull(error.cause());
}
@Test
void testM3TechnicalDocumentError_WithCause() {
// Arrange
var cause = new RuntimeException("File not found");
// Act
var error = new M3TechnicalDocumentError(candidate, "I/O error", cause);
// Assert
assertEquals(cause, error.cause());
}
@Test
void testM3TechnicalDocumentError_WithNullCandidate_ThrowsException() {
assertThrows(NullPointerException.class,
() -> new M3TechnicalDocumentError(null, "Error", null));
}
@Test
void testM3TechnicalDocumentError_WithNullErrorMessage_ThrowsException() {
assertThrows(NullPointerException.class,
() -> new M3TechnicalDocumentError(candidate, null, null));
}
@Test
void testM3TechnicalDocumentError_WithEmptyErrorMessage_ThrowsException() {
assertThrows(IllegalArgumentException.class,
() -> new M3TechnicalDocumentError(candidate, "", null));
}
@Test
void testM3TechnicalDocumentError_IsM3DocumentProcessingOutcome() {
// Verify type relationship
var error = new M3TechnicalDocumentError(candidate, "Error", null);
assertInstanceOf(M3DocumentProcessingOutcome.class, error);
}
@Test
void testM3PreCheckPassed_IsM3DocumentProcessingOutcome() {
// Verify type relationship
var extraction = new PdfExtractionSuccess("text", new PdfPageCount(1));
var passed = new M3PreCheckPassed(candidate, extraction);
assertInstanceOf(M3DocumentProcessingOutcome.class, passed);
}
@Test
void testM3PreCheckFailed_IsM3DocumentProcessingOutcome() {
// Verify type relationship
var failed = new M3PreCheckFailed(candidate, "Test failure reason");
assertInstanceOf(M3DocumentProcessingOutcome.class, failed);
}
@Test
void testAllThreeOutcomesAreExhaustive() {
// This test verifies that the three outcome types are the only implementations
// M3 has exactly three outcome types: passed, failed (deterministic), and technical error
var extraction = new PdfExtractionSuccess("text", new PdfPageCount(1));
M3DocumentProcessingOutcome[] outcomes = {
new M3PreCheckPassed(candidate, extraction),
new M3PreCheckFailed(candidate, "Deterministic content failure"),
new M3TechnicalDocumentError(candidate, "Technical extraction error", null)
};
for (M3DocumentProcessingOutcome outcome : outcomes) {
assertInstanceOf(M3DocumentProcessingOutcome.class, outcome);
}
}
}