1
0

Meilenstein-Präfixe aus Klassennamen entfernt

This commit is contained in:
2026-04-02 09:11:52 +02:00
parent c0cdd0ed6e
commit 7d5c21f14c
21 changed files with 501 additions and 455 deletions

View File

@@ -0,0 +1,68 @@
pdf-umbenenner-adapter-in-cli/src/main/java/de/gecheckt/pdf/umbenenner/adapter/inbound/cli/package-info.java | de.gecheckt.pdf.umbenenner.adapter.inbound.cli | |
pdf-umbenenner-adapter-in-cli/src/main/java/de/gecheckt/pdf/umbenenner/adapter/inbound/cli/SchedulerBatchCommand.java | de.gecheckt.pdf.umbenenner.adapter.inbound.cli | class | SchedulerBatchCommand
pdf-umbenenner-adapter-in-cli/src/test/java/de/gecheckt/pdf/umbenenner/adapter/inbound/cli/SchedulerBatchCommandTest.java | de.gecheckt.pdf.umbenenner.adapter.inbound.cli | class | SchedulerBatchCommandTest
pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/outbound/configuration/package-info.java | de.gecheckt.pdf.umbenenner.adapter.outbound.configuration | |
pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/outbound/configuration/PropertiesConfigurationPortAdapter.java | de.gecheckt.pdf.umbenenner.adapter.outbound.configuration | class | PropertiesConfigurationPortAdapter
pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/outbound/lock/FilesystemRunLockPortAdapter.java | de.gecheckt.pdf.umbenenner.adapter.outbound.lock | class | FilesystemRunLockPortAdapter
pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/outbound/lock/package-info.java | de.gecheckt.pdf.umbenenner.adapter.outbound.lock | |
pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/outbound/package-info.java | de.gecheckt.pdf.umbenenner.adapter.outbound | |
pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/outbound/pdfextraction/package-info.java | de.gecheckt.pdf.umbenenner.adapter.outbound.pdfextraction | |
pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/outbound/pdfextraction/PdfTextExtractionPortAdapter.java | de.gecheckt.pdf.umbenenner.adapter.outbound.pdfextraction | class | PdfTextExtractionPortAdapter
pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/outbound/sourcedocument/package-info.java | de.gecheckt.pdf.umbenenner.adapter.outbound.sourcedocument | |
pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/outbound/sourcedocument/SourceDocumentCandidatesPortAdapter.java | de.gecheckt.pdf.umbenenner.adapter.outbound.sourcedocument | class | SourceDocumentCandidatesPortAdapter
pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/outbound/configuration/PropertiesConfigurationPortAdapterTest.java | de.gecheckt.pdf.umbenenner.adapter.outbound.configuration | class | PropertiesConfigurationPortAdapterTest
pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/outbound/lock/FilesystemRunLockPortAdapterTest.java | de.gecheckt.pdf.umbenenner.adapter.outbound.lock | class | FilesystemRunLockPortAdapterTest
pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/outbound/pdfextraction/PdfTextExtractionPortAdapterTest.java | de.gecheckt.pdf.umbenenner.adapter.outbound.pdfextraction | class | PdfTextExtractionPortAdapterTest
pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/outbound/sourcedocument/SourceDocumentCandidatesPortAdapterTest.java | de.gecheckt.pdf.umbenenner.adapter.outbound.sourcedocument | class | SourceDocumentCandidatesPortAdapterTest
pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/config/InvalidStartConfigurationException.java | de.gecheckt.pdf.umbenenner.application.config | class | InvalidStartConfigurationException
pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/config/package-info.java | de.gecheckt.pdf.umbenenner.application.config | |
pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/config/StartConfiguration.java | de.gecheckt.pdf.umbenenner.application.config | record | StartConfiguration
pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/config/StartConfigurationValidator.java | de.gecheckt.pdf.umbenenner.application.config | class | StartConfigurationValidator
pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/package-info.java | de.gecheckt.pdf.umbenenner.application | |
pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/BatchRunOutcome.java | de.gecheckt.pdf.umbenenner.application.port.in | enum | BatchRunOutcome
pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/package-info.java | de.gecheckt.pdf.umbenenner.application.port.in | |
pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/RunBatchProcessingUseCase.java | de.gecheckt.pdf.umbenenner.application.port.in | interface | RunBatchProcessingUseCase
pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/ClockPort.java | de.gecheckt.pdf.umbenenner.application.port.out | interface | ClockPort
pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/ConfigurationPort.java | de.gecheckt.pdf.umbenenner.application.port.out | interface | ConfigurationPort
pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/package-info.java | de.gecheckt.pdf.umbenenner.application.port.out | |
pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/PdfTextExtractionPort.java | de.gecheckt.pdf.umbenenner.application.port.out | interface | PdfTextExtractionPort
pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/RunLockPort.java | de.gecheckt.pdf.umbenenner.application.port.out | interface | RunLockPort
pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/RunLockUnavailableException.java | de.gecheckt.pdf.umbenenner.application.port.out | class | RunLockUnavailableException
pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/SourceDocumentAccessException.java | de.gecheckt.pdf.umbenenner.application.port.out | |
pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/SourceDocumentCandidatesPort.java | de.gecheckt.pdf.umbenenner.application.port.out | interface | SourceDocumentCandidatesPort
pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/M3DocumentProcessingService.java | de.gecheckt.pdf.umbenenner.application.service | class | M3DocumentProcessingService
pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/M3PreCheckEvaluator.java | de.gecheckt.pdf.umbenenner.application.service | class | M3PreCheckEvaluator
pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/package-info.java | de.gecheckt.pdf.umbenenner.application.service | |
pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/M2BatchRunProcessingUseCase.java | de.gecheckt.pdf.umbenenner.application.usecase | class | M2BatchRunProcessingUseCase
pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/package-info.java | de.gecheckt.pdf.umbenenner.application.usecase | |
pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/config/StartConfigurationValidatorTest.java | de.gecheckt.pdf.umbenenner.application.config | class | StartConfigurationValidatorTest
pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/M3DocumentProcessingServiceTest.java | de.gecheckt.pdf.umbenenner.application.service | class | M3DocumentProcessingServiceTest
pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/M3PreCheckEvaluatorTest.java | de.gecheckt.pdf.umbenenner.application.service | class | M3PreCheckEvaluatorTest
pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/M2BatchRunProcessingUseCaseTest.java | de.gecheckt.pdf.umbenenner.application.usecase | class | M2BatchRunProcessingUseCaseTest
pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java | de.gecheckt.pdf.umbenenner.bootstrap | class | BootstrapRunner
pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/package-info.java | de.gecheckt.pdf.umbenenner.bootstrap | |
pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/PdfUmbenennerApplication.java | de.gecheckt.pdf.umbenenner.bootstrap | class | PdfUmbenennerApplication
pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerTest.java | de.gecheckt.pdf.umbenenner.bootstrap | class | BootstrapRunnerTest
pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/ExecutableJarSmokeTestIT.java | de.gecheckt.pdf.umbenenner.bootstrap | class | ExecutableJarSmokeTestIT
pdf-umbenenner-domain/src/main/java/de/gecheckt/pdf/umbenenner/domain/model/BatchRunContext.java | de.gecheckt.pdf.umbenenner.domain.model | |
pdf-umbenenner-domain/src/main/java/de/gecheckt/pdf/umbenenner/domain/model/M3DocumentProcessingOutcome.java | de.gecheckt.pdf.umbenenner.domain.model | |
pdf-umbenenner-domain/src/main/java/de/gecheckt/pdf/umbenenner/domain/model/M3PreCheckFailed.java | de.gecheckt.pdf.umbenenner.domain.model | record | M3PreCheckFailed
pdf-umbenenner-domain/src/main/java/de/gecheckt/pdf/umbenenner/domain/model/M3PreCheckFailureReason.java | de.gecheckt.pdf.umbenenner.domain.model | enum | M3PreCheckFailureReason
pdf-umbenenner-domain/src/main/java/de/gecheckt/pdf/umbenenner/domain/model/M3PreCheckPassed.java | de.gecheckt.pdf.umbenenner.domain.model | record | M3PreCheckPassed
pdf-umbenenner-domain/src/main/java/de/gecheckt/pdf/umbenenner/domain/model/M3ProcessingDecision.java | de.gecheckt.pdf.umbenenner.domain.model | |
pdf-umbenenner-domain/src/main/java/de/gecheckt/pdf/umbenenner/domain/model/M3TechnicalDocumentError.java | de.gecheckt.pdf.umbenenner.domain.model | record | M3TechnicalDocumentError
pdf-umbenenner-domain/src/main/java/de/gecheckt/pdf/umbenenner/domain/model/package-info.java | de.gecheckt.pdf.umbenenner.domain.model | |
pdf-umbenenner-domain/src/main/java/de/gecheckt/pdf/umbenenner/domain/model/PdfExtractionContentError.java | de.gecheckt.pdf.umbenenner.domain.model | record | PdfExtractionContentError
pdf-umbenenner-domain/src/main/java/de/gecheckt/pdf/umbenenner/domain/model/PdfExtractionResult.java | de.gecheckt.pdf.umbenenner.domain.model | |
pdf-umbenenner-domain/src/main/java/de/gecheckt/pdf/umbenenner/domain/model/PdfExtractionSuccess.java | de.gecheckt.pdf.umbenenner.domain.model | record | PdfExtractionSuccess
pdf-umbenenner-domain/src/main/java/de/gecheckt/pdf/umbenenner/domain/model/PdfExtractionTechnicalError.java | de.gecheckt.pdf.umbenenner.domain.model | record | PdfExtractionTechnicalError
pdf-umbenenner-domain/src/main/java/de/gecheckt/pdf/umbenenner/domain/model/PdfPageCount.java | de.gecheckt.pdf.umbenenner.domain.model | record | PdfPageCount
pdf-umbenenner-domain/src/main/java/de/gecheckt/pdf/umbenenner/domain/model/ProcessingStatus.java | de.gecheckt.pdf.umbenenner.domain.model | enum | ProcessingStatus
pdf-umbenenner-domain/src/main/java/de/gecheckt/pdf/umbenenner/domain/model/RunId.java | de.gecheckt.pdf.umbenenner.domain.model | |
pdf-umbenenner-domain/src/main/java/de/gecheckt/pdf/umbenenner/domain/model/SourceDocumentCandidate.java | de.gecheckt.pdf.umbenenner.domain.model | record | SourceDocumentCandidate
pdf-umbenenner-domain/src/main/java/de/gecheckt/pdf/umbenenner/domain/model/SourceDocumentLocator.java | de.gecheckt.pdf.umbenenner.domain.model | record | SourceDocumentLocator
pdf-umbenenner-domain/src/main/java/de/gecheckt/pdf/umbenenner/domain/package-info.java | de.gecheckt.pdf.umbenenner.domain | |
pdf-umbenenner-domain/src/test/java/de/gecheckt/pdf/umbenenner/domain/model/BatchRunContextTest.java | de.gecheckt.pdf.umbenenner.domain.model | class | BatchRunContextTest
pdf-umbenenner-domain/src/test/java/de/gecheckt/pdf/umbenenner/domain/model/M3DocumentProcessingOutcomeTest.java | de.gecheckt.pdf.umbenenner.domain.model | class | M3DocumentProcessingOutcomeTest
pdf-umbenenner-domain/src/test/java/de/gecheckt/pdf/umbenenner/domain/model/ProcessingStatusTest.java | de.gecheckt.pdf.umbenenner.domain.model | class | ProcessingStatusTest
pdf-umbenenner-domain/src/test/java/de/gecheckt/pdf/umbenenner/domain/model/RunIdTest.java | de.gecheckt.pdf.umbenenner.domain.model | class | RunIdTest

View File

@@ -1,8 +1,8 @@
package de.gecheckt.pdf.umbenenner.application.service; package de.gecheckt.pdf.umbenenner.application.service;
import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration; import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration;
import de.gecheckt.pdf.umbenenner.domain.model.M3DocumentProcessingOutcome; import de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome;
import de.gecheckt.pdf.umbenenner.domain.model.M3TechnicalDocumentError; import de.gecheckt.pdf.umbenenner.domain.model.TechnicalDocumentError;
import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionContentError; import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionContentError;
import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionResult; import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionResult;
import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionSuccess; import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionSuccess;
@@ -12,45 +12,43 @@ import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate;
import java.util.Objects; import java.util.Objects;
/** /**
* Orchestrates M3 document processing pipeline: extraction pre-checks outcome classification. * Orchestrates document processing pipeline: extraction pre-checks outcome classification.
* <p> * <p>
* Converts technical extraction results into M3 processing outcomes through this pipeline: * Converts technical extraction results into processing outcomes through this pipeline:
* <ol> * <ol>
* <li>If extraction fails (content or technical): {@link M3TechnicalDocumentError}</li> * <li>If extraction fails (content or technical): {@link TechnicalDocumentError}</li>
* <li>If extraction succeeds: Evaluate M3 pre-checks via {@link M3PreCheckEvaluator}</li> * <li>If extraction succeeds: Evaluate pre-checks via {@link PreCheckEvaluator}</li>
* </ol> * </ol>
* <p> * <p>
* This service produces {@link M3DocumentProcessingOutcome}, a sealed interface that covers * This service produces {@link DocumentProcessingOutcome}, a sealed interface that covers
* all four M3 document processing outcomes: * all document processing outcomes:
* <ul> * <ul>
* <li>Pre-check passed (document ready for M4+)</li> * <li>Pre-check passed (document ready for further processing)</li>
* <li>Pre-check failed (deterministic content error: no usable text, page limit exceeded)</li> * <li>Pre-check failed (deterministic content error: no usable text, page limit exceeded)</li>
* <li>Technical document error (I/O, access, PDF parsing, etc.)</li> * <li>Technical document error (I/O, access, PDF parsing, etc.)</li>
* </ul> * </ul>
* <p> * <p>
* This service is stateless and thread-safe. * This service is stateless and thread-safe.
*
* @since M3-AP-006
*/ */
public class M3DocumentProcessingService { public class DocumentProcessingService {
/** /**
* Processes a document candidate through the complete M3 pipeline. * Processes a document candidate through the complete pipeline.
* <p> * <p>
* Pipeline: * Pipeline:
* <ol> * <ol>
* <li>Extract text and page count from the PDF candidate</li> * <li>Extract text and page count from the PDF candidate</li>
* <li>If extraction fails (technical or content): classify as technical document error</li> * <li>If extraction fails (technical or content): classify as technical document error</li>
* <li>If extraction succeeds: evaluate M3 pre-checks</li> * <li>If extraction succeeds: evaluate pre-checks</li>
* </ol> * </ol>
* *
* @param candidate the document candidate to process * @param candidate the document candidate to process
* @param extractionResult the result from PDF extraction (from {@link de.gecheckt.pdf.umbenenner.application.port.out.PdfTextExtractionPort}) * @param extractionResult the result from PDF extraction (from {@link de.gecheckt.pdf.umbenenner.application.port.out.PdfTextExtractionPort})
* @param configuration the startup configuration (used for pre-check validation) * @param configuration the startup configuration (used for pre-check validation)
* @return the complete M3 processing outcome (one of four possibilities, all implementing {@link M3DocumentProcessingOutcome}) * @return the complete processing outcome (implementing {@link DocumentProcessingOutcome})
* @throws NullPointerException if any parameter is null * @throws NullPointerException if any parameter is null
*/ */
public static M3DocumentProcessingOutcome processDocument( public static DocumentProcessingOutcome processDocument(
SourceDocumentCandidate candidate, SourceDocumentCandidate candidate,
PdfExtractionResult extractionResult, PdfExtractionResult extractionResult,
StartConfiguration configuration) { StartConfiguration configuration) {
@@ -61,20 +59,20 @@ public class M3DocumentProcessingService {
return switch (extractionResult) { return switch (extractionResult) {
case PdfExtractionSuccess success -> case PdfExtractionSuccess success ->
// Extraction succeeded: evaluate M3 pre-checks // Extraction succeeded: evaluate pre-checks
M3PreCheckEvaluator.evaluate(candidate, success, configuration); PreCheckEvaluator.evaluate(candidate, success, configuration);
case PdfExtractionContentError contentError -> case PdfExtractionContentError contentError ->
// PDF content not extractable: classify as technical document error // PDF content not extractable: classify as technical document error
new M3TechnicalDocumentError(candidate, "PDF content not extractable: " + contentError.reason(), null); new TechnicalDocumentError(candidate, "PDF content not extractable: " + contentError.reason(), null);
case PdfExtractionTechnicalError technicalError -> case PdfExtractionTechnicalError technicalError ->
// Technical failure during extraction: potentially retryable // Technical failure during extraction: potentially retryable
new M3TechnicalDocumentError(candidate, technicalError.errorMessage(), technicalError.cause()); new TechnicalDocumentError(candidate, technicalError.errorMessage(), technicalError.cause());
}; };
} }
private M3DocumentProcessingService() { private DocumentProcessingService() {
// Static utility class no instances // Static utility class no instances
} }
} }

View File

@@ -1,36 +1,34 @@
package de.gecheckt.pdf.umbenenner.application.service; package de.gecheckt.pdf.umbenenner.application.service;
import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration; import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration;
import de.gecheckt.pdf.umbenenner.domain.model.M3DocumentProcessingOutcome; import de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome;
import de.gecheckt.pdf.umbenenner.domain.model.M3PreCheckFailureReason; import de.gecheckt.pdf.umbenenner.domain.model.PreCheckFailureReason;
import de.gecheckt.pdf.umbenenner.domain.model.M3PreCheckFailed; import de.gecheckt.pdf.umbenenner.domain.model.PreCheckFailed;
import de.gecheckt.pdf.umbenenner.domain.model.M3PreCheckPassed; import de.gecheckt.pdf.umbenenner.domain.model.PreCheckPassed;
import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionSuccess; import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionSuccess;
import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate; import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate;
import java.util.Objects; import java.util.Objects;
/** /**
* Evaluates whether a successfully extracted PDF passes M3 pre-checks. * Evaluates whether a successfully extracted PDF passes pre-checks.
* <p> * <p>
* M3 Pre-checks verify that: * Pre-checks verify that:
* <ul> * <ul>
* <li>The extracted text contains at least one meaningful character after normalization</li> * <li>The extracted text contains at least one meaningful character after normalization</li>
* <li>The document's page count does not exceed the configured limit</li> * <li>The document's page count does not exceed the configured limit</li>
* </ul> * </ul>
* <p> * <p>
* A document that passes both pre-checks is ready to proceed to M4 and later milestones. * A document that passes both pre-checks is ready to proceed to further processing steps.
* A document that fails a pre-check is classified with a specific deterministic failure reason * A document that fails a pre-check is classified with a specific deterministic failure reason
* and will not proceed further in the current batch run. * and will not proceed further in the current batch run.
* <p> * <p>
* This service is stateless and thread-safe. * This service is stateless and thread-safe.
*
* @since M3-AP-004
*/ */
public class M3PreCheckEvaluator { public class PreCheckEvaluator {
/** /**
* Evaluates M3 pre-checks for a successfully extracted PDF document. * Evaluates pre-checks for a successfully extracted PDF document.
* <p> * <p>
* Pre-check logic: * Pre-check logic:
* <ol> * <ol>
@@ -38,19 +36,19 @@ public class M3PreCheckEvaluator {
* <li>Check if document page count does not exceed the configured limit</li> * <li>Check if document page count does not exceed the configured limit</li>
* </ol> * </ol>
* <p> * <p>
* Returns {@link M3PreCheckPassed} if both checks pass, or {@link M3PreCheckFailed} * Returns {@link PreCheckPassed} if both checks pass, or {@link PreCheckFailed}
* with a specific reason if any check fails. * with a specific reason if any check fails.
* <p> * <p>
* Note: Returns {@link M3DocumentProcessingOutcome} to integrate cleanly with the complete * Note: Returns {@link DocumentProcessingOutcome} to integrate cleanly with the complete
* M3 document processing pipeline. * document processing pipeline.
* *
* @param candidate the source document metadata * @param candidate the source document metadata
* @param extraction the successfully extracted PDF content * @param extraction the successfully extracted PDF content
* @param configuration the startup configuration (used for maxPages limit) * @param configuration the startup configuration (used for maxPages limit)
* @return the pre-check outcome: passed or failed with reason (both implement {@link M3DocumentProcessingOutcome}) * @return the pre-check outcome: passed or failed with reason (both implement {@link DocumentProcessingOutcome})
* @throws NullPointerException if any parameter is null * @throws NullPointerException if any parameter is null
*/ */
public static M3DocumentProcessingOutcome evaluate( public static DocumentProcessingOutcome evaluate(
SourceDocumentCandidate candidate, SourceDocumentCandidate candidate,
PdfExtractionSuccess extraction, PdfExtractionSuccess extraction,
StartConfiguration configuration) { StartConfiguration configuration) {
@@ -61,28 +59,28 @@ public class M3PreCheckEvaluator {
// Pre-check 1: Verify document has usable text // Pre-check 1: Verify document has usable text
if (!hasUsableText(extraction.extractedText())) { if (!hasUsableText(extraction.extractedText())) {
return new M3PreCheckFailed( return new PreCheckFailed(
candidate, candidate,
M3PreCheckFailureReason.NO_USABLE_TEXT.getDescription() PreCheckFailureReason.NO_USABLE_TEXT.getDescription()
); );
} }
// Pre-check 2: Verify document page count does not exceed configured limit // Pre-check 2: Verify document page count does not exceed configured limit
if (extraction.pageCount().exceedsLimit(configuration.maxPages())) { if (extraction.pageCount().exceedsLimit(configuration.maxPages())) {
return new M3PreCheckFailed( return new PreCheckFailed(
candidate, candidate,
M3PreCheckFailureReason.PAGE_LIMIT_EXCEEDED.getDescription() PreCheckFailureReason.PAGE_LIMIT_EXCEEDED.getDescription()
); );
} }
// All pre-checks passed // All pre-checks passed
return new M3PreCheckPassed(candidate, extraction); return new PreCheckPassed(candidate, extraction);
} }
/** /**
* Determines whether the extracted text contains at least one meaningful character. * Determines whether the extracted text contains at least one meaningful character.
* <p> * <p>
* Definition of "usable text" for M3: * Definition of "usable text":
* <ul> * <ul>
* <li>After normalization (trimming whitespace), at least one letter or digit remains</li> * <li>After normalization (trimming whitespace), at least one letter or digit remains</li>
* <li>Pure whitespace or only special characters do not qualify as usable text</li> * <li>Pure whitespace or only special characters do not qualify as usable text</li>
@@ -116,7 +114,7 @@ public class M3PreCheckEvaluator {
return false; return false;
} }
private M3PreCheckEvaluator() { private PreCheckEvaluator() {
// Static utility class no instances // Static utility class no instances
} }
} }

View File

@@ -12,19 +12,17 @@
* *
* Current services: * Current services:
* <ul> * <ul>
* <li>{@link de.gecheckt.pdf.umbenenner.application.service.M3PreCheckEvaluator} — M3 pre-check evaluation (M3-AP-004)</li> * <li>{@link de.gecheckt.pdf.umbenenner.application.service.PreCheckEvaluator} — Pre-check evaluation</li>
* <li>{@link de.gecheckt.pdf.umbenenner.application.service.M3DocumentProcessingService} — complete M3 document processing pipeline orchestration (M3-AP-006)</li> * <li>{@link de.gecheckt.pdf.umbenenner.application.service.DocumentProcessingService} — Complete document processing pipeline orchestration</li>
* </ul> * </ul>
* *
* M3 Document Processing Pipeline (M3-AP-006): * Document Processing Pipeline:
* The {@link de.gecheckt.pdf.umbenenner.application.service.M3DocumentProcessingService} coordinates * The {@link de.gecheckt.pdf.umbenenner.application.service.DocumentProcessingService} coordinates
* the complete M3 processing workflow: * the complete processing workflow:
* <ol> * <ol>
* <li>Convert technical PDF extraction results to M3 processing outcomes</li> * <li>Convert technical PDF extraction results to processing outcomes</li>
* <li>Route successful extractions through M3 pre-check validation</li> * <li>Route successful extractions through pre-check validation</li>
* <li>Classify extraction and pre-check failures with appropriate error types</li> * <li>Classify extraction and pre-check failures with appropriate error types</li>
* </ol> * </ol>
*
* @since M3-AP-004
*/ */
package de.gecheckt.pdf.umbenenner.application.service; package de.gecheckt.pdf.umbenenner.application.service;

View File

@@ -8,12 +8,12 @@ import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort;
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockUnavailableException; import de.gecheckt.pdf.umbenenner.application.port.out.RunLockUnavailableException;
import de.gecheckt.pdf.umbenenner.application.port.out.SourceDocumentAccessException; import de.gecheckt.pdf.umbenenner.application.port.out.SourceDocumentAccessException;
import de.gecheckt.pdf.umbenenner.application.port.out.SourceDocumentCandidatesPort; import de.gecheckt.pdf.umbenenner.application.port.out.SourceDocumentCandidatesPort;
import de.gecheckt.pdf.umbenenner.application.service.M3DocumentProcessingService; import de.gecheckt.pdf.umbenenner.application.service.DocumentProcessingService;
import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext; import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext;
import de.gecheckt.pdf.umbenenner.domain.model.M3DocumentProcessingOutcome; import de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome;
import de.gecheckt.pdf.umbenenner.domain.model.M3PreCheckFailed; import de.gecheckt.pdf.umbenenner.domain.model.PreCheckFailed;
import de.gecheckt.pdf.umbenenner.domain.model.M3PreCheckPassed; import de.gecheckt.pdf.umbenenner.domain.model.PreCheckPassed;
import de.gecheckt.pdf.umbenenner.domain.model.M3TechnicalDocumentError; import de.gecheckt.pdf.umbenenner.domain.model.TechnicalDocumentError;
import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionContentError; import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionContentError;
import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionResult; import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionResult;
import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionSuccess; import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionSuccess;
@@ -26,38 +26,36 @@ import org.apache.logging.log4j.Logger;
import java.util.List; import java.util.List;
/** /**
* M3 batch processing implementation of {@link RunBatchProcessingUseCase}. * Batch processing implementation of {@link RunBatchProcessingUseCase}.
* <p> * <p>
* Orchestrates the complete M3 batch processing workflow: * Orchestrates the complete batch processing workflow:
* <ol> * <ol>
* <li>Acquire exclusive run lock to prevent concurrent instances</li> * <li>Acquire exclusive run lock to prevent concurrent instances</li>
* <li>Scan source folder for PDF candidates</li> * <li>Scan source folder for PDF candidates</li>
* <li>For each candidate: extract text and page count, run M3 pre-checks</li> * <li>For each candidate: extract text and page count, run pre-checks</li>
* <li>Log per-document M3 decision; end each document controlled without KI or target copy</li> * <li>Log per-document decision; end each document controlled without KI or target copy</li>
* <li>Release lock and return structured outcome for Bootstrap exit code mapping</li> * <li>Release lock and return structured outcome for Bootstrap exit code mapping</li>
* </ol> * </ol>
* <p> * <p>
* M3 processing boundary: * Processing boundary:
* <ul> * <ul>
* <li>Documents that pass M3 pre-checks end controlled and are ready for M4+ (KI, persistence, copy)</li> * <li>Documents that pass pre-checks end controlled and are ready for further processing (KI, persistence, copy)</li>
* <li>Documents with deterministic content errors (no usable text, page limit exceeded) end controlled</li> * <li>Documents with deterministic content errors (no usable text, page limit exceeded) end controlled</li>
* <li>Documents with technical extraction errors end controlled; they do not abort the overall run</li> * <li>Documents with technical extraction errors end controlled; they do not abort the overall run</li>
* <li>If the source folder itself is inaccessible, the run fails with {@link BatchRunOutcome#FAILURE}</li> * <li>If the source folder itself is inaccessible, the run fails with {@link BatchRunOutcome#FAILURE}</li>
* </ul> * </ul>
* <p> * <p>
* M3 Non-Goals (not implemented): * Non-Goals (not implemented):
* <ul> * <ul>
* <li>No fingerprinting or SQLite persistence</li> * <li>No fingerprinting or SQLite persistence</li>
* <li>No KI/AI integration or prompt loading</li> * <li>No KI/AI integration or prompt loading</li>
* <li>No filename generation or target file copy</li> * <li>No filename generation or target file copy</li>
* <li>No cross-run retry logic</li> * <li>No cross-run retry logic</li>
* </ul> * </ul>
*
* @since M2-AP-004 (extended in M3-AP-005)
*/ */
public class M2BatchRunProcessingUseCase implements RunBatchProcessingUseCase { public class BatchRunProcessingUseCase implements RunBatchProcessingUseCase {
private static final Logger LOG = LogManager.getLogger(M2BatchRunProcessingUseCase.class); private static final Logger LOG = LogManager.getLogger(BatchRunProcessingUseCase.class);
private final StartConfiguration configuration; private final StartConfiguration configuration;
private final RunLockPort runLockPort; private final RunLockPort runLockPort;
@@ -76,7 +74,7 @@ public class M2BatchRunProcessingUseCase implements RunBatchProcessingUseCase {
* @param pdfTextExtractionPort for extracting text and page count from a single PDF * @param pdfTextExtractionPort for extracting text and page count from a single PDF
* @throws NullPointerException if any parameter is null * @throws NullPointerException if any parameter is null
*/ */
public M2BatchRunProcessingUseCase( public BatchRunProcessingUseCase(
StartConfiguration configuration, StartConfiguration configuration,
RunLockPort runLockPort, RunLockPort runLockPort,
SourceDocumentCandidatesPort sourceDocumentCandidatesPort, SourceDocumentCandidatesPort sourceDocumentCandidatesPort,
@@ -116,7 +114,7 @@ public class M2BatchRunProcessingUseCase implements RunBatchProcessingUseCase {
} }
LOG.info("Found {} PDF candidate(s) in source folder.", candidates.size()); LOG.info("Found {} PDF candidate(s) in source folder.", candidates.size());
// Step 3: Process each candidate through the M3 pipeline // Step 3: Process each candidate through the pipeline
for (SourceDocumentCandidate candidate : candidates) { for (SourceDocumentCandidate candidate : candidates) {
processCandidate(candidate); processCandidate(candidate);
} }
@@ -143,20 +141,20 @@ public class M2BatchRunProcessingUseCase implements RunBatchProcessingUseCase {
} }
/** /**
* Processes a single PDF candidate through the complete M3 pipeline. * Processes a single PDF candidate through the complete pipeline.
* <p> * <p>
* M3 processing steps per document: * Processing steps per document:
* <ol> * <ol>
* <li>Log candidate recognition</li> * <li>Log candidate recognition</li>
* <li>Extract text and page count from the PDF via {@link PdfTextExtractionPort}</li> * <li>Extract text and page count from the PDF via {@link PdfTextExtractionPort}</li>
* <li>Process extraction result through M3 pre-checks via {@link M3DocumentProcessingService}</li> * <li>Process extraction result through pre-checks via {@link DocumentProcessingService}</li>
* <li>Log extraction outcome and final M3 decision</li> * <li>Log extraction outcome and final decision</li>
* </ol> * </ol>
* <p> * <p>
* Per-document errors (extraction failure, technical error, pre-check failure) do not abort the overall * Per-document errors (extraction failure, technical error, pre-check failure) do not abort the overall
* batch run. Each candidate ends controlled regardless of its outcome. * batch run. Each candidate ends controlled regardless of its outcome.
* <p> * <p>
* M3 processing boundary: no KI call, no persistence, no filename generation, * Processing boundary: no KI call, no persistence, no filename generation,
* no target file copy is initiated here, even for candidates that pass all pre-checks. * no target file copy is initiated here, even for candidates that pass all pre-checks.
* *
* @param candidate the candidate to process * @param candidate the candidate to process
@@ -179,19 +177,19 @@ public class M2BatchRunProcessingUseCase implements RunBatchProcessingUseCase {
candidate.uniqueIdentifier(), technicalError.errorMessage()); candidate.uniqueIdentifier(), technicalError.errorMessage());
} }
// Process through complete M3 pipeline // Process through complete pipeline
var m3Outcome = M3DocumentProcessingService.processDocument(candidate, extractionResult, configuration); var outcome = DocumentProcessingService.processDocument(candidate, extractionResult, configuration);
// Log M3 processing outcome // Log processing outcome
switch (m3Outcome) { switch (outcome) {
case M3PreCheckPassed passed -> case PreCheckPassed passed ->
LOG.info("M3 pre-checks PASSED for '{}'. Candidate ready for further processing (M4+).", LOG.info("Pre-checks PASSED for '{}'. Candidate ready for further processing.",
candidate.uniqueIdentifier()); candidate.uniqueIdentifier());
case M3PreCheckFailed failed -> case PreCheckFailed failed ->
LOG.info("M3 pre-checks FAILED for '{}': {} (Deterministic content error may retry in later run).", LOG.info("Pre-checks FAILED for '{}': {} (Deterministic content error may retry in later run).",
candidate.uniqueIdentifier(), failed.failureReason()); candidate.uniqueIdentifier(), failed.failureReason());
case M3TechnicalDocumentError technicalError -> case TechnicalDocumentError technicalError ->
LOG.warn("M3 processing FAILED for '{}': {} (Technical error may retry in later run).", LOG.warn("Processing FAILED for '{}': {} (Technical error may retry in later run).",
candidate.uniqueIdentifier(), technicalError.errorMessage()); candidate.uniqueIdentifier(), technicalError.errorMessage());
} }
} }

View File

@@ -5,8 +5,8 @@
* <ul> * <ul>
* <li>{@link de.gecheckt.pdf.umbenenner.application.usecase.NoOpRunBatchProcessingUseCase} * <li>{@link de.gecheckt.pdf.umbenenner.application.usecase.NoOpRunBatchProcessingUseCase}
* — Minimal no-op for technical validation without start protection</li> * — Minimal no-op for technical validation without start protection</li>
* <li>{@link de.gecheckt.pdf.umbenenner.application.usecase.M2BatchRunProcessingUseCase} * <li>{@link de.gecheckt.pdf.umbenenner.application.usecase.BatchRunProcessingUseCase}
* — M2 production implementation with run lock and controlled batch cycle (AP-004)</li> * — Production implementation with run lock and controlled batch cycle</li>
* </ul> * </ul>
* <p> * <p>
* All implementations are infrastructure-agnostic and interact only through ports. * All implementations are infrastructure-agnostic and interact only through ports.

View File

@@ -1,10 +1,10 @@
package de.gecheckt.pdf.umbenenner.application.service; package de.gecheckt.pdf.umbenenner.application.service;
import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration; import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration;
import de.gecheckt.pdf.umbenenner.domain.model.M3DocumentProcessingOutcome; import de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome;
import de.gecheckt.pdf.umbenenner.domain.model.M3PreCheckFailed; import de.gecheckt.pdf.umbenenner.domain.model.PreCheckFailed;
import de.gecheckt.pdf.umbenenner.domain.model.M3PreCheckPassed; import de.gecheckt.pdf.umbenenner.domain.model.PreCheckPassed;
import de.gecheckt.pdf.umbenenner.domain.model.M3TechnicalDocumentError; import de.gecheckt.pdf.umbenenner.domain.model.TechnicalDocumentError;
import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionContentError; import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionContentError;
import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionSuccess; import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionSuccess;
import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionTechnicalError; import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionTechnicalError;
@@ -23,11 +23,11 @@ import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
/** /**
* Tests for {@link M3DocumentProcessingService}. * Tests for {@link DocumentProcessingService}.
* <p> * <p>
* Verifies that all four M3 document processing outcomes are correctly classified. * Verifies that all document processing outcomes are correctly classified.
*/ */
class M3DocumentProcessingServiceTest { class DocumentProcessingServiceTest {
@TempDir @TempDir
Path tempDir; Path tempDir;
@@ -75,12 +75,12 @@ class M3DocumentProcessingServiceTest {
var extraction = new PdfExtractionSuccess("This is valid PDF text", new PdfPageCount(5)); var extraction = new PdfExtractionSuccess("This is valid PDF text", new PdfPageCount(5));
// Act // Act
M3DocumentProcessingOutcome outcome = M3DocumentProcessingService.processDocument( DocumentProcessingOutcome outcome = DocumentProcessingService.processDocument(
candidate, extraction, configuration); candidate, extraction, configuration);
// Assert: Should produce M3PreCheckPassed // Assert: Should produce PreCheckPassed
assertInstanceOf(M3PreCheckPassed.class, outcome); assertInstanceOf(PreCheckPassed.class, outcome);
M3PreCheckPassed passed = (M3PreCheckPassed) outcome; PreCheckPassed passed = (PreCheckPassed) outcome;
assertEquals(candidate, passed.candidate()); assertEquals(candidate, passed.candidate());
assertEquals(extraction, passed.extraction()); assertEquals(extraction, passed.extraction());
} }
@@ -91,12 +91,12 @@ class M3DocumentProcessingServiceTest {
var extraction = new PdfExtractionSuccess(" \n \t ", new PdfPageCount(1)); var extraction = new PdfExtractionSuccess(" \n \t ", new PdfPageCount(1));
// Act // Act
M3DocumentProcessingOutcome outcome = M3DocumentProcessingService.processDocument( DocumentProcessingOutcome outcome = DocumentProcessingService.processDocument(
candidate, extraction, configuration); candidate, extraction, configuration);
// Assert: Should produce M3PreCheckFailed with appropriate reason // Assert: Should produce PreCheckFailed with appropriate reason
assertInstanceOf(M3PreCheckFailed.class, outcome); assertInstanceOf(PreCheckFailed.class, outcome);
M3PreCheckFailed failed = (M3PreCheckFailed) outcome; PreCheckFailed failed = (PreCheckFailed) outcome;
assertEquals(candidate, failed.candidate()); assertEquals(candidate, failed.candidate());
assertTrue(failed.failureReason().toLowerCase().contains("usable")); assertTrue(failed.failureReason().toLowerCase().contains("usable"));
} }
@@ -107,28 +107,28 @@ class M3DocumentProcessingServiceTest {
var extraction = new PdfExtractionSuccess("Valid text content", new PdfPageCount(50)); var extraction = new PdfExtractionSuccess("Valid text content", new PdfPageCount(50));
// Act // Act
M3DocumentProcessingOutcome outcome = M3DocumentProcessingService.processDocument( DocumentProcessingOutcome outcome = DocumentProcessingService.processDocument(
candidate, extraction, configuration); candidate, extraction, configuration);
// Assert: Should produce M3PreCheckFailed with page limit reason // Assert: Should produce PreCheckFailed with page limit reason
assertInstanceOf(M3PreCheckFailed.class, outcome); assertInstanceOf(PreCheckFailed.class, outcome);
M3PreCheckFailed failed = (M3PreCheckFailed) outcome; PreCheckFailed failed = (PreCheckFailed) outcome;
assertEquals(candidate, failed.candidate()); assertEquals(candidate, failed.candidate());
assertTrue(failed.failureReason().toLowerCase().contains("page")); assertTrue(failed.failureReason().toLowerCase().contains("page"));
} }
@Test @Test
void testProcessDocument_WithContentError() { void testProcessDocument_WithContentError() {
// Arrange: PDF content not extractable (classified as technical document error in M3) // Arrange: PDF content not extractable (classified as technical document error)
var contentError = new PdfExtractionContentError("PDF is corrupted"); var contentError = new PdfExtractionContentError("PDF is corrupted");
// Act // Act
M3DocumentProcessingOutcome outcome = M3DocumentProcessingService.processDocument( DocumentProcessingOutcome outcome = DocumentProcessingService.processDocument(
candidate, contentError, configuration); candidate, contentError, configuration);
// Assert: Should produce M3TechnicalDocumentError // Assert: Should produce TechnicalDocumentError
assertInstanceOf(M3TechnicalDocumentError.class, outcome); assertInstanceOf(TechnicalDocumentError.class, outcome);
M3TechnicalDocumentError result = (M3TechnicalDocumentError) outcome; TechnicalDocumentError result = (TechnicalDocumentError) outcome;
assertEquals(candidate, result.candidate()); assertEquals(candidate, result.candidate());
assertTrue(result.errorMessage().contains("PDF is corrupted")); assertTrue(result.errorMessage().contains("PDF is corrupted"));
} }
@@ -140,12 +140,12 @@ class M3DocumentProcessingServiceTest {
new RuntimeException("File not found")); new RuntimeException("File not found"));
// Act // Act
M3DocumentProcessingOutcome outcome = M3DocumentProcessingService.processDocument( DocumentProcessingOutcome outcome = DocumentProcessingService.processDocument(
candidate, technicalError, configuration); candidate, technicalError, configuration);
// Assert: Should produce M3TechnicalDocumentError // Assert: Should produce TechnicalDocumentError
assertInstanceOf(M3TechnicalDocumentError.class, outcome); assertInstanceOf(TechnicalDocumentError.class, outcome);
M3TechnicalDocumentError result = (M3TechnicalDocumentError) outcome; TechnicalDocumentError result = (TechnicalDocumentError) outcome;
assertEquals(candidate, result.candidate()); assertEquals(candidate, result.candidate());
assertEquals("I/O error reading file", result.errorMessage()); assertEquals("I/O error reading file", result.errorMessage());
assertNotNull(result.cause()); assertNotNull(result.cause());
@@ -157,12 +157,12 @@ class M3DocumentProcessingServiceTest {
var technicalError = new PdfExtractionTechnicalError("Unknown error", null); var technicalError = new PdfExtractionTechnicalError("Unknown error", null);
// Act // Act
M3DocumentProcessingOutcome outcome = M3DocumentProcessingService.processDocument( DocumentProcessingOutcome outcome = DocumentProcessingService.processDocument(
candidate, technicalError, configuration); candidate, technicalError, configuration);
// Assert // Assert
assertInstanceOf(M3TechnicalDocumentError.class, outcome); assertInstanceOf(TechnicalDocumentError.class, outcome);
M3TechnicalDocumentError result = (M3TechnicalDocumentError) outcome; TechnicalDocumentError result = (TechnicalDocumentError) outcome;
assertNull(result.cause()); assertNull(result.cause());
} }
@@ -173,14 +173,14 @@ class M3DocumentProcessingServiceTest {
// Act & Assert // Act & Assert
assertThrows(NullPointerException.class, assertThrows(NullPointerException.class,
() -> M3DocumentProcessingService.processDocument(null, extraction, configuration)); () -> DocumentProcessingService.processDocument(null, extraction, configuration));
} }
@Test @Test
void testProcessDocument_WithNullExtractionResult_ThrowsException() { void testProcessDocument_WithNullExtractionResult_ThrowsException() {
// Act & Assert // Act & Assert
assertThrows(NullPointerException.class, assertThrows(NullPointerException.class,
() -> M3DocumentProcessingService.processDocument(candidate, null, configuration)); () -> DocumentProcessingService.processDocument(candidate, null, configuration));
} }
@Test @Test
@@ -190,6 +190,6 @@ class M3DocumentProcessingServiceTest {
// Act & Assert // Act & Assert
assertThrows(NullPointerException.class, assertThrows(NullPointerException.class,
() -> M3DocumentProcessingService.processDocument(candidate, extraction, null)); () -> DocumentProcessingService.processDocument(candidate, extraction, null));
} }
} }

View File

@@ -1,10 +1,10 @@
package de.gecheckt.pdf.umbenenner.application.service; package de.gecheckt.pdf.umbenenner.application.service;
import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration; import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration;
import de.gecheckt.pdf.umbenenner.domain.model.M3DocumentProcessingOutcome; import de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome;
import de.gecheckt.pdf.umbenenner.domain.model.M3PreCheckFailed; import de.gecheckt.pdf.umbenenner.domain.model.PreCheckFailed;
import de.gecheckt.pdf.umbenenner.domain.model.M3PreCheckFailureReason; import de.gecheckt.pdf.umbenenner.domain.model.PreCheckFailureReason;
import de.gecheckt.pdf.umbenenner.domain.model.M3PreCheckPassed; import de.gecheckt.pdf.umbenenner.domain.model.PreCheckPassed;
import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionSuccess; import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionSuccess;
import de.gecheckt.pdf.umbenenner.domain.model.PdfPageCount; import de.gecheckt.pdf.umbenenner.domain.model.PdfPageCount;
import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate; import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate;
@@ -20,11 +20,11 @@ import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
/** /**
* Tests for {@link M3PreCheckEvaluator}. * Tests for {@link PreCheckEvaluator}.
* <p> * <p>
* Verifies correct M3 pre-check logic for usable text and page limit validation. * Verifies correct pre-check logic for usable text and page limit validation.
*/ */
class M3PreCheckEvaluatorTest { class PreCheckEvaluatorTest {
@TempDir @TempDir
Path tempDir; Path tempDir;
@@ -35,10 +35,10 @@ class M3PreCheckEvaluatorTest {
SourceDocumentCandidate candidate = buildCandidate(); SourceDocumentCandidate candidate = buildCandidate();
PdfExtractionSuccess extraction = new PdfExtractionSuccess("Some meaningful text", new PdfPageCount(5)); PdfExtractionSuccess extraction = new PdfExtractionSuccess("Some meaningful text", new PdfPageCount(5));
M3DocumentProcessingOutcome result = M3PreCheckEvaluator.evaluate(candidate, extraction, config); DocumentProcessingOutcome result = PreCheckEvaluator.evaluate(candidate, extraction, config);
assertTrue(result instanceof M3PreCheckPassed, "Should pass when text is usable and page count is valid"); assertTrue(result instanceof PreCheckPassed, "Should pass when text is usable and page count is valid");
M3PreCheckPassed passed = (M3PreCheckPassed) result; PreCheckPassed passed = (PreCheckPassed) result;
assertSame(passed.candidate(), candidate, "Candidate should be preserved"); assertSame(passed.candidate(), candidate, "Candidate should be preserved");
assertSame(passed.extraction(), extraction, "Extraction should be preserved"); assertSame(passed.extraction(), extraction, "Extraction should be preserved");
} }
@@ -49,11 +49,11 @@ class M3PreCheckEvaluatorTest {
SourceDocumentCandidate candidate = buildCandidate(); SourceDocumentCandidate candidate = buildCandidate();
PdfExtractionSuccess extraction = new PdfExtractionSuccess("", new PdfPageCount(1)); PdfExtractionSuccess extraction = new PdfExtractionSuccess("", new PdfPageCount(1));
M3DocumentProcessingOutcome result = M3PreCheckEvaluator.evaluate(candidate, extraction, config); DocumentProcessingOutcome result = PreCheckEvaluator.evaluate(candidate, extraction, config);
assertTrue(result instanceof M3PreCheckFailed, "Should fail with empty text"); assertTrue(result instanceof PreCheckFailed, "Should fail with empty text");
M3PreCheckFailed failed = (M3PreCheckFailed) result; PreCheckFailed failed = (PreCheckFailed) result;
assertEquals(M3PreCheckFailureReason.NO_USABLE_TEXT.getDescription(), failed.failureReason()); assertEquals(PreCheckFailureReason.NO_USABLE_TEXT.getDescription(), failed.failureReason());
} }
@Test @Test
@@ -62,11 +62,11 @@ class M3PreCheckEvaluatorTest {
SourceDocumentCandidate candidate = buildCandidate(); SourceDocumentCandidate candidate = buildCandidate();
PdfExtractionSuccess extraction = new PdfExtractionSuccess(" \n\t \r\n ", new PdfPageCount(1)); PdfExtractionSuccess extraction = new PdfExtractionSuccess(" \n\t \r\n ", new PdfPageCount(1));
M3DocumentProcessingOutcome result = M3PreCheckEvaluator.evaluate(candidate, extraction, config); DocumentProcessingOutcome result = PreCheckEvaluator.evaluate(candidate, extraction, config);
assertTrue(result instanceof M3PreCheckFailed, "Should fail with whitespace-only text"); assertTrue(result instanceof PreCheckFailed, "Should fail with whitespace-only text");
M3PreCheckFailed failed = (M3PreCheckFailed) result; PreCheckFailed failed = (PreCheckFailed) result;
assertEquals(M3PreCheckFailureReason.NO_USABLE_TEXT.getDescription(), failed.failureReason()); assertEquals(PreCheckFailureReason.NO_USABLE_TEXT.getDescription(), failed.failureReason());
} }
@Test @Test
@@ -75,11 +75,11 @@ class M3PreCheckEvaluatorTest {
SourceDocumentCandidate candidate = buildCandidate(); SourceDocumentCandidate candidate = buildCandidate();
PdfExtractionSuccess extraction = new PdfExtractionSuccess("!@#$%^&*()_+-=[]{}|;:',.<>?/", new PdfPageCount(1)); PdfExtractionSuccess extraction = new PdfExtractionSuccess("!@#$%^&*()_+-=[]{}|;:',.<>?/", new PdfPageCount(1));
M3DocumentProcessingOutcome result = M3PreCheckEvaluator.evaluate(candidate, extraction, config); DocumentProcessingOutcome result = PreCheckEvaluator.evaluate(candidate, extraction, config);
assertTrue(result instanceof M3PreCheckFailed, "Should fail with special characters only"); assertTrue(result instanceof PreCheckFailed, "Should fail with special characters only");
M3PreCheckFailed failed = (M3PreCheckFailed) result; PreCheckFailed failed = (PreCheckFailed) result;
assertEquals(M3PreCheckFailureReason.NO_USABLE_TEXT.getDescription(), failed.failureReason()); assertEquals(PreCheckFailureReason.NO_USABLE_TEXT.getDescription(), failed.failureReason());
} }
@Test @Test
@@ -88,9 +88,9 @@ class M3PreCheckEvaluatorTest {
SourceDocumentCandidate candidate = buildCandidate(); SourceDocumentCandidate candidate = buildCandidate();
PdfExtractionSuccess extraction = new PdfExtractionSuccess("a", new PdfPageCount(1)); PdfExtractionSuccess extraction = new PdfExtractionSuccess("a", new PdfPageCount(1));
M3DocumentProcessingOutcome result = M3PreCheckEvaluator.evaluate(candidate, extraction, config); DocumentProcessingOutcome result = PreCheckEvaluator.evaluate(candidate, extraction, config);
assertTrue(result instanceof M3PreCheckPassed, "Should pass with single letter"); assertTrue(result instanceof PreCheckPassed, "Should pass with single letter");
} }
@Test @Test
@@ -99,9 +99,9 @@ class M3PreCheckEvaluatorTest {
SourceDocumentCandidate candidate = buildCandidate(); SourceDocumentCandidate candidate = buildCandidate();
PdfExtractionSuccess extraction = new PdfExtractionSuccess("5", new PdfPageCount(1)); PdfExtractionSuccess extraction = new PdfExtractionSuccess("5", new PdfPageCount(1));
M3DocumentProcessingOutcome result = M3PreCheckEvaluator.evaluate(candidate, extraction, config); DocumentProcessingOutcome result = PreCheckEvaluator.evaluate(candidate, extraction, config);
assertTrue(result instanceof M3PreCheckPassed, "Should pass with single digit"); assertTrue(result instanceof PreCheckPassed, "Should pass with single digit");
} }
@Test @Test
@@ -110,9 +110,9 @@ class M3PreCheckEvaluatorTest {
SourceDocumentCandidate candidate = buildCandidate(); SourceDocumentCandidate candidate = buildCandidate();
PdfExtractionSuccess extraction = new PdfExtractionSuccess("!@#a$%^&*", new PdfPageCount(1)); PdfExtractionSuccess extraction = new PdfExtractionSuccess("!@#a$%^&*", new PdfPageCount(1));
M3DocumentProcessingOutcome result = M3PreCheckEvaluator.evaluate(candidate, extraction, config); DocumentProcessingOutcome result = PreCheckEvaluator.evaluate(candidate, extraction, config);
assertTrue(result instanceof M3PreCheckPassed, "Should pass when letters/digits are present among special chars"); assertTrue(result instanceof PreCheckPassed, "Should pass when letters/digits are present among special chars");
} }
@Test @Test
@@ -121,9 +121,9 @@ class M3PreCheckEvaluatorTest {
SourceDocumentCandidate candidate = buildCandidate(); SourceDocumentCandidate candidate = buildCandidate();
PdfExtractionSuccess extraction = new PdfExtractionSuccess(" meaningful text ", new PdfPageCount(1)); PdfExtractionSuccess extraction = new PdfExtractionSuccess(" meaningful text ", new PdfPageCount(1));
M3DocumentProcessingOutcome result = M3PreCheckEvaluator.evaluate(candidate, extraction, config); DocumentProcessingOutcome result = PreCheckEvaluator.evaluate(candidate, extraction, config);
assertTrue(result instanceof M3PreCheckPassed, "Should pass when text has meaningful content despite whitespace"); assertTrue(result instanceof PreCheckPassed, "Should pass when text has meaningful content despite whitespace");
} }
@Test @Test
@@ -132,9 +132,9 @@ class M3PreCheckEvaluatorTest {
SourceDocumentCandidate candidate = buildCandidate(); SourceDocumentCandidate candidate = buildCandidate();
PdfExtractionSuccess extraction = new PdfExtractionSuccess("Valid text", new PdfPageCount(5)); PdfExtractionSuccess extraction = new PdfExtractionSuccess("Valid text", new PdfPageCount(5));
M3DocumentProcessingOutcome result = M3PreCheckEvaluator.evaluate(candidate, extraction, config); DocumentProcessingOutcome result = PreCheckEvaluator.evaluate(candidate, extraction, config);
assertTrue(result instanceof M3PreCheckPassed, "Should pass when page count equals limit (not exceeded)"); assertTrue(result instanceof PreCheckPassed, "Should pass when page count equals limit (not exceeded)");
} }
@Test @Test
@@ -143,11 +143,11 @@ class M3PreCheckEvaluatorTest {
SourceDocumentCandidate candidate = buildCandidate(); SourceDocumentCandidate candidate = buildCandidate();
PdfExtractionSuccess extraction = new PdfExtractionSuccess("Valid text", new PdfPageCount(6)); PdfExtractionSuccess extraction = new PdfExtractionSuccess("Valid text", new PdfPageCount(6));
M3DocumentProcessingOutcome result = M3PreCheckEvaluator.evaluate(candidate, extraction, config); DocumentProcessingOutcome result = PreCheckEvaluator.evaluate(candidate, extraction, config);
assertTrue(result instanceof M3PreCheckFailed, "Should fail when page count exceeds limit"); assertTrue(result instanceof PreCheckFailed, "Should fail when page count exceeds limit");
M3PreCheckFailed failed = (M3PreCheckFailed) result; PreCheckFailed failed = (PreCheckFailed) result;
assertEquals(M3PreCheckFailureReason.PAGE_LIMIT_EXCEEDED.getDescription(), failed.failureReason()); assertEquals(PreCheckFailureReason.PAGE_LIMIT_EXCEEDED.getDescription(), failed.failureReason());
} }
@Test @Test
@@ -156,11 +156,11 @@ class M3PreCheckEvaluatorTest {
SourceDocumentCandidate candidate = buildCandidate(); SourceDocumentCandidate candidate = buildCandidate();
PdfExtractionSuccess extraction = new PdfExtractionSuccess("Excellent meaningful text with lots of content", new PdfPageCount(100)); PdfExtractionSuccess extraction = new PdfExtractionSuccess("Excellent meaningful text with lots of content", new PdfPageCount(100));
M3DocumentProcessingOutcome result = M3PreCheckEvaluator.evaluate(candidate, extraction, config); DocumentProcessingOutcome result = PreCheckEvaluator.evaluate(candidate, extraction, config);
assertTrue(result instanceof M3PreCheckFailed, "Should fail with page limit exceeded even if text is good"); assertTrue(result instanceof PreCheckFailed, "Should fail with page limit exceeded even if text is good");
M3PreCheckFailed failed = (M3PreCheckFailed) result; PreCheckFailed failed = (PreCheckFailed) result;
assertEquals(M3PreCheckFailureReason.PAGE_LIMIT_EXCEEDED.getDescription(), failed.failureReason()); assertEquals(PreCheckFailureReason.PAGE_LIMIT_EXCEEDED.getDescription(), failed.failureReason());
} }
@Test @Test
@@ -171,11 +171,11 @@ class M3PreCheckEvaluatorTest {
SourceDocumentCandidate candidate = buildCandidate(); SourceDocumentCandidate candidate = buildCandidate();
PdfExtractionSuccess extraction = new PdfExtractionSuccess("", new PdfPageCount(10)); PdfExtractionSuccess extraction = new PdfExtractionSuccess("", new PdfPageCount(10));
M3DocumentProcessingOutcome result = M3PreCheckEvaluator.evaluate(candidate, extraction, config); DocumentProcessingOutcome result = PreCheckEvaluator.evaluate(candidate, extraction, config);
assertTrue(result instanceof M3PreCheckFailed, "Should fail when both checks fail"); assertTrue(result instanceof PreCheckFailed, "Should fail when both checks fail");
// The specific order of checks doesn't matter for M3; just verify one reason is returned // The specific order of checks doesn't matter; just verify one reason is returned
M3PreCheckFailed failed = (M3PreCheckFailed) result; PreCheckFailed failed = (PreCheckFailed) result;
assertNotNull(failed.failureReason()); assertNotNull(failed.failureReason());
assertFalse(failed.failureReason().isEmpty()); assertFalse(failed.failureReason().isEmpty());
} }
@@ -186,7 +186,7 @@ class M3PreCheckEvaluatorTest {
PdfExtractionSuccess extraction = new PdfExtractionSuccess("Valid text", new PdfPageCount(1)); PdfExtractionSuccess extraction = new PdfExtractionSuccess("Valid text", new PdfPageCount(1));
assertThrows(NullPointerException.class, () -> assertThrows(NullPointerException.class, () ->
M3PreCheckEvaluator.evaluate(null, extraction, config) PreCheckEvaluator.evaluate(null, extraction, config)
); );
} }
@@ -196,7 +196,7 @@ class M3PreCheckEvaluatorTest {
SourceDocumentCandidate candidate = buildCandidate(); SourceDocumentCandidate candidate = buildCandidate();
assertThrows(NullPointerException.class, () -> assertThrows(NullPointerException.class, () ->
M3PreCheckEvaluator.evaluate(candidate, null, config) PreCheckEvaluator.evaluate(candidate, null, config)
); );
} }
@@ -206,7 +206,7 @@ class M3PreCheckEvaluatorTest {
PdfExtractionSuccess extraction = new PdfExtractionSuccess("Valid text", new PdfPageCount(1)); PdfExtractionSuccess extraction = new PdfExtractionSuccess("Valid text", new PdfPageCount(1));
assertThrows(NullPointerException.class, () -> assertThrows(NullPointerException.class, () ->
M3PreCheckEvaluator.evaluate(candidate, extraction, null) PreCheckEvaluator.evaluate(candidate, extraction, null)
); );
} }
@@ -216,9 +216,9 @@ class M3PreCheckEvaluatorTest {
SourceDocumentCandidate candidate = buildCandidate(); SourceDocumentCandidate candidate = buildCandidate();
PdfExtractionSuccess extraction = new PdfExtractionSuccess("Äußerst äöüß Großes", new PdfPageCount(1)); PdfExtractionSuccess extraction = new PdfExtractionSuccess("Äußerst äöüß Großes", new PdfPageCount(1));
M3DocumentProcessingOutcome result = M3PreCheckEvaluator.evaluate(candidate, extraction, config); DocumentProcessingOutcome result = PreCheckEvaluator.evaluate(candidate, extraction, config);
assertTrue(result instanceof M3PreCheckPassed, "Should pass with German umlauts (ÄÖÜß)"); assertTrue(result instanceof PreCheckPassed, "Should pass with German umlauts (ÄÖÜß)");
} }
@Test @Test
@@ -227,9 +227,9 @@ class M3PreCheckEvaluatorTest {
SourceDocumentCandidate candidate = buildCandidate(); SourceDocumentCandidate candidate = buildCandidate();
PdfExtractionSuccess extraction = new PdfExtractionSuccess("Αβγδ 中文 καλημέρα", new PdfPageCount(1)); PdfExtractionSuccess extraction = new PdfExtractionSuccess("Αβγδ 中文 καλημέρα", new PdfPageCount(1));
M3DocumentProcessingOutcome result = M3PreCheckEvaluator.evaluate(candidate, extraction, config); DocumentProcessingOutcome result = PreCheckEvaluator.evaluate(candidate, extraction, config);
assertTrue(result instanceof M3PreCheckPassed, "Should pass with Greek, Chinese, and other Unicode letters"); assertTrue(result instanceof PreCheckPassed, "Should pass with Greek, Chinese, and other Unicode letters");
} }
// ========================================================================= // =========================================================================

View File

@@ -31,25 +31,25 @@ import java.util.Map;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
/** /**
* Tests for {@link M2BatchRunProcessingUseCase}. * Tests for {@link BatchRunProcessingUseCase}.
* <p> * <p>
* Covers: * Covers:
* <ul> * <ul>
* <li>Lock acquisition and release lifecycle (M2)</li> * <li>Lock acquisition and release lifecycle</li>
* <li>M3 source folder scanning and per-document processing loop</li> * <li>Source folder scanning and per-document processing loop</li>
* <li>M3 happy path: candidate passes pre-checks, ends controlled without KI or target copy</li> * <li>Happy path: candidate passes pre-checks, ends controlled without KI or target copy</li>
* <li>M3 deterministic content errors: no usable text, page limit exceeded</li> * <li>Deterministic content errors: no usable text, page limit exceeded</li>
* <li>M3 technical extraction errors: controlled per-document end, batch continues</li> * <li>Technical extraction errors: controlled per-document end, batch continues</li>
* <li>Source folder access failure: batch fails with FAILURE outcome</li> * <li>Source folder access failure: batch fails with FAILURE outcome</li>
* </ul> * </ul>
*/ */
class M2BatchRunProcessingUseCaseTest { class BatchRunProcessingUseCaseTest {
@TempDir @TempDir
Path tempDir; Path tempDir;
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// M2: Lock lifecycle tests (preserved, updated constructor) // Lock lifecycle tests
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@Test @Test
@@ -57,7 +57,7 @@ class M2BatchRunProcessingUseCaseTest {
MockRunLockPort lockPort = new MockRunLockPort(); MockRunLockPort lockPort = new MockRunLockPort();
StartConfiguration config = buildConfig(tempDir); StartConfiguration config = buildConfig(tempDir);
M2BatchRunProcessingUseCase useCase = new M2BatchRunProcessingUseCase( BatchRunProcessingUseCase useCase = new BatchRunProcessingUseCase(
config, lockPort, new EmptyCandidatesPort(), new NoOpExtractionPort()); config, lockPort, new EmptyCandidatesPort(), new NoOpExtractionPort());
BatchRunContext context = new BatchRunContext(new RunId("test-run-1"), Instant.now()); BatchRunContext context = new BatchRunContext(new RunId("test-run-1"), Instant.now());
@@ -73,7 +73,7 @@ class M2BatchRunProcessingUseCaseTest {
CountingRunLockPort lockPort = new CountingRunLockPort(true); CountingRunLockPort lockPort = new CountingRunLockPort(true);
StartConfiguration config = buildConfig(tempDir); StartConfiguration config = buildConfig(tempDir);
M2BatchRunProcessingUseCase useCase = new M2BatchRunProcessingUseCase( BatchRunProcessingUseCase useCase = new BatchRunProcessingUseCase(
config, lockPort, new EmptyCandidatesPort(), new NoOpExtractionPort()); config, lockPort, new EmptyCandidatesPort(), new NoOpExtractionPort());
BatchRunContext context = new BatchRunContext(new RunId("test-run-2"), Instant.now()); BatchRunContext context = new BatchRunContext(new RunId("test-run-2"), Instant.now());
@@ -85,14 +85,14 @@ class M2BatchRunProcessingUseCaseTest {
} }
/** /**
* Regression test for M2-F1: when acquire() fails, release() must NOT be called. * Regression test: when acquire() fails, release() must NOT be called.
*/ */
@Test @Test
void execute_doesNotReleaseLockWhenAcquireFails() throws Exception { void execute_doesNotReleaseLockWhenAcquireFails() throws Exception {
CountingRunLockPort lockPort = new CountingRunLockPort(true); CountingRunLockPort lockPort = new CountingRunLockPort(true);
StartConfiguration config = buildConfig(tempDir); StartConfiguration config = buildConfig(tempDir);
M2BatchRunProcessingUseCase useCase = new M2BatchRunProcessingUseCase( BatchRunProcessingUseCase useCase = new BatchRunProcessingUseCase(
config, lockPort, new EmptyCandidatesPort(), new NoOpExtractionPort()); config, lockPort, new EmptyCandidatesPort(), new NoOpExtractionPort());
BatchRunContext context = new BatchRunContext(new RunId("test-run-f1"), Instant.now()); BatchRunContext context = new BatchRunContext(new RunId("test-run-f1"), Instant.now());
@@ -108,7 +108,7 @@ class M2BatchRunProcessingUseCaseTest {
ErrorAfterAcquireLockPort lockPort = new ErrorAfterAcquireLockPort(); ErrorAfterAcquireLockPort lockPort = new ErrorAfterAcquireLockPort();
StartConfiguration config = buildConfig(tempDir); StartConfiguration config = buildConfig(tempDir);
M2BatchRunProcessingUseCase useCase = new M2BatchRunProcessingUseCase( BatchRunProcessingUseCase useCase = new BatchRunProcessingUseCase(
config, lockPort, new EmptyCandidatesPort(), new NoOpExtractionPort()); config, lockPort, new EmptyCandidatesPort(), new NoOpExtractionPort());
BatchRunContext context = new BatchRunContext(new RunId("test-run-3"), Instant.now()); BatchRunContext context = new BatchRunContext(new RunId("test-run-3"), Instant.now());
@@ -120,7 +120,7 @@ class M2BatchRunProcessingUseCaseTest {
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// M3: Source folder scanning and candidate processing // Source folder scanning and candidate processing
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@Test @Test
@@ -128,9 +128,9 @@ class M2BatchRunProcessingUseCaseTest {
MockRunLockPort lockPort = new MockRunLockPort(); MockRunLockPort lockPort = new MockRunLockPort();
StartConfiguration config = buildConfig(tempDir); StartConfiguration config = buildConfig(tempDir);
M2BatchRunProcessingUseCase useCase = new M2BatchRunProcessingUseCase( BatchRunProcessingUseCase useCase = new BatchRunProcessingUseCase(
config, lockPort, new EmptyCandidatesPort(), new NoOpExtractionPort()); config, lockPort, new EmptyCandidatesPort(), new NoOpExtractionPort());
BatchRunContext context = new BatchRunContext(new RunId("m3-empty"), Instant.now()); BatchRunContext context = new BatchRunContext(new RunId("empty"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context); BatchRunOutcome outcome = useCase.execute(context);
@@ -138,7 +138,7 @@ class M2BatchRunProcessingUseCaseTest {
} }
@Test @Test
void execute_m3HappyPath_candidatePassesPreChecks_endsControlledWithoutKiOrCopy() throws Exception { void execute_happyPath_candidatePassesPreChecks_endsControlledWithoutKiOrCopy() throws Exception {
MockRunLockPort lockPort = new MockRunLockPort(); MockRunLockPort lockPort = new MockRunLockPort();
StartConfiguration config = buildConfig(tempDir); StartConfiguration config = buildConfig(tempDir);
@@ -148,19 +148,19 @@ class M2BatchRunProcessingUseCaseTest {
FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate)); FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate));
FixedExtractionPort extractionPort = new FixedExtractionPort(success); FixedExtractionPort extractionPort = new FixedExtractionPort(success);
M2BatchRunProcessingUseCase useCase = new M2BatchRunProcessingUseCase( BatchRunProcessingUseCase useCase = new BatchRunProcessingUseCase(
config, lockPort, candidatesPort, extractionPort); config, lockPort, candidatesPort, extractionPort);
BatchRunContext context = new BatchRunContext(new RunId("m3-happy"), Instant.now()); BatchRunContext context = new BatchRunContext(new RunId("happy"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context); BatchRunOutcome outcome = useCase.execute(context);
// Batch run succeeds; document ended controlled at M3 boundary (no KI, no copy) // Batch run succeeds; document ended controlled at boundary (no KI, no copy)
assertTrue(outcome.isSuccess(), "M3 happy path should yield SUCCESS"); assertTrue(outcome.isSuccess(), "Happy path should yield SUCCESS");
assertEquals(1, extractionPort.callCount(), "Extraction should be called exactly once"); assertEquals(1, extractionPort.callCount(), "Extraction should be called exactly once");
} }
@Test @Test
void execute_m3NoUsableText_candidateEndsControlled_batchContinues() throws Exception { void execute_noUsableText_candidateEndsControlled_batchContinues() throws Exception {
MockRunLockPort lockPort = new MockRunLockPort(); MockRunLockPort lockPort = new MockRunLockPort();
StartConfiguration config = buildConfig(tempDir); StartConfiguration config = buildConfig(tempDir);
@@ -170,9 +170,9 @@ class M2BatchRunProcessingUseCaseTest {
FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate)); FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate));
FixedExtractionPort extractionPort = new FixedExtractionPort(emptySuccess); FixedExtractionPort extractionPort = new FixedExtractionPort(emptySuccess);
M2BatchRunProcessingUseCase useCase = new M2BatchRunProcessingUseCase( BatchRunProcessingUseCase useCase = new BatchRunProcessingUseCase(
config, lockPort, candidatesPort, extractionPort); config, lockPort, candidatesPort, extractionPort);
BatchRunContext context = new BatchRunContext(new RunId("m3-no-text"), Instant.now()); BatchRunContext context = new BatchRunContext(new RunId("no-text"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context); BatchRunOutcome outcome = useCase.execute(context);
@@ -181,7 +181,7 @@ class M2BatchRunProcessingUseCaseTest {
} }
@Test @Test
void execute_m3PageLimitExceeded_candidateEndsControlled_batchContinues() throws Exception { void execute_pageLimitExceeded_candidateEndsControlled_batchContinues() throws Exception {
MockRunLockPort lockPort = new MockRunLockPort(); MockRunLockPort lockPort = new MockRunLockPort();
// Config has maxPages=3; document has 10 pages // Config has maxPages=3; document has 10 pages
StartConfiguration config = buildConfig(tempDir); StartConfiguration config = buildConfig(tempDir);
@@ -191,9 +191,9 @@ class M2BatchRunProcessingUseCaseTest {
FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate)); FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate));
FixedExtractionPort extractionPort = new FixedExtractionPort(manyPages); FixedExtractionPort extractionPort = new FixedExtractionPort(manyPages);
M2BatchRunProcessingUseCase useCase = new M2BatchRunProcessingUseCase( BatchRunProcessingUseCase useCase = new BatchRunProcessingUseCase(
config, lockPort, candidatesPort, extractionPort); config, lockPort, candidatesPort, extractionPort);
BatchRunContext context = new BatchRunContext(new RunId("m3-page-limit"), Instant.now()); BatchRunContext context = new BatchRunContext(new RunId("page-limit"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context); BatchRunOutcome outcome = useCase.execute(context);
@@ -202,7 +202,7 @@ class M2BatchRunProcessingUseCaseTest {
} }
@Test @Test
void execute_m3ExtractionContentError_candidateEndsControlled_batchContinues() throws Exception { void execute_extractionContentError_candidateEndsControlled_batchContinues() throws Exception {
MockRunLockPort lockPort = new MockRunLockPort(); MockRunLockPort lockPort = new MockRunLockPort();
StartConfiguration config = buildConfig(tempDir); StartConfiguration config = buildConfig(tempDir);
@@ -211,9 +211,9 @@ class M2BatchRunProcessingUseCaseTest {
FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate)); FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate));
FixedExtractionPort extractionPort = new FixedExtractionPort(contentError); FixedExtractionPort extractionPort = new FixedExtractionPort(contentError);
M2BatchRunProcessingUseCase useCase = new M2BatchRunProcessingUseCase( BatchRunProcessingUseCase useCase = new BatchRunProcessingUseCase(
config, lockPort, candidatesPort, extractionPort); config, lockPort, candidatesPort, extractionPort);
BatchRunContext context = new BatchRunContext(new RunId("m3-content-error"), Instant.now()); BatchRunContext context = new BatchRunContext(new RunId("content-error"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context); BatchRunOutcome outcome = useCase.execute(context);
@@ -221,7 +221,7 @@ class M2BatchRunProcessingUseCaseTest {
} }
@Test @Test
void execute_m3ExtractionTechnicalError_candidateEndsControlled_batchContinues() throws Exception { void execute_extractionTechnicalError_candidateEndsControlled_batchContinues() throws Exception {
MockRunLockPort lockPort = new MockRunLockPort(); MockRunLockPort lockPort = new MockRunLockPort();
StartConfiguration config = buildConfig(tempDir); StartConfiguration config = buildConfig(tempDir);
@@ -230,9 +230,9 @@ class M2BatchRunProcessingUseCaseTest {
FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate)); FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate));
FixedExtractionPort extractionPort = new FixedExtractionPort(technicalError); FixedExtractionPort extractionPort = new FixedExtractionPort(technicalError);
M2BatchRunProcessingUseCase useCase = new M2BatchRunProcessingUseCase( BatchRunProcessingUseCase useCase = new BatchRunProcessingUseCase(
config, lockPort, candidatesPort, extractionPort); config, lockPort, candidatesPort, extractionPort);
BatchRunContext context = new BatchRunContext(new RunId("m3-tech-error"), Instant.now()); BatchRunContext context = new BatchRunContext(new RunId("tech-error"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context); BatchRunOutcome outcome = useCase.execute(context);
@@ -240,7 +240,7 @@ class M2BatchRunProcessingUseCaseTest {
} }
@Test @Test
void execute_m3SourceAccessException_returnsFailure() throws Exception { void execute_sourceAccessException_returnsFailure() throws Exception {
MockRunLockPort lockPort = new MockRunLockPort(); MockRunLockPort lockPort = new MockRunLockPort();
StartConfiguration config = buildConfig(tempDir); StartConfiguration config = buildConfig(tempDir);
@@ -248,9 +248,9 @@ class M2BatchRunProcessingUseCaseTest {
throw new SourceDocumentAccessException("Source folder not readable"); throw new SourceDocumentAccessException("Source folder not readable");
}; };
M2BatchRunProcessingUseCase useCase = new M2BatchRunProcessingUseCase( BatchRunProcessingUseCase useCase = new BatchRunProcessingUseCase(
config, lockPort, failingPort, new NoOpExtractionPort()); config, lockPort, failingPort, new NoOpExtractionPort());
BatchRunContext context = new BatchRunContext(new RunId("m3-access-fail"), Instant.now()); BatchRunContext context = new BatchRunContext(new RunId("access-fail"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context); BatchRunOutcome outcome = useCase.execute(context);
@@ -261,11 +261,11 @@ class M2BatchRunProcessingUseCaseTest {
} }
/** /**
* Mixed-batch test: one document per M3 outcome type in a single run. * Mixed-batch test: one document per outcome type in a single run.
* Proves that no individual outcome aborts the overall batch (AP-008 explicit contract). * Proves that no individual outcome aborts the overall batch.
*/ */
@Test @Test
void execute_m3MixedBatch_allOutcomeTypes_batchOverallSucceeds() throws Exception { void execute_mixedBatch_allOutcomeTypes_batchOverallSucceeds() throws Exception {
MockRunLockPort lockPort = new MockRunLockPort(); MockRunLockPort lockPort = new MockRunLockPort();
// maxPages=3 in buildConfig; pageLimitCandidate has 10 pages exceeds limit // maxPages=3 in buildConfig; pageLimitCandidate has 10 pages exceeds limit
StartConfiguration config = buildConfig(tempDir); StartConfiguration config = buildConfig(tempDir);
@@ -287,20 +287,20 @@ class M2BatchRunProcessingUseCaseTest {
.with(technicalErrorCandidate, new PdfExtractionTechnicalError("I/O error", null)) .with(technicalErrorCandidate, new PdfExtractionTechnicalError("I/O error", null))
.with(contentErrorCandidate, new PdfExtractionContentError("PDF is encrypted")); .with(contentErrorCandidate, new PdfExtractionContentError("PDF is encrypted"));
M2BatchRunProcessingUseCase useCase = new M2BatchRunProcessingUseCase( BatchRunProcessingUseCase useCase = new BatchRunProcessingUseCase(
config, lockPort, candidatesPort, extractionPort); config, lockPort, candidatesPort, extractionPort);
BatchRunContext context = new BatchRunContext(new RunId("m3-mixed"), Instant.now()); BatchRunContext context = new BatchRunContext(new RunId("mixed"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context); BatchRunOutcome outcome = useCase.execute(context);
assertTrue(outcome.isSuccess(), assertTrue(outcome.isSuccess(),
"Mixed batch with all M3 outcome types must yield batch SUCCESS"); "Mixed batch with all outcome types must yield batch SUCCESS");
assertEquals(5, extractionPort.callCount(), assertEquals(5, extractionPort.callCount(),
"Extraction must be attempted for each of the 5 candidates"); "Extraction must be attempted for each of the 5 candidates");
} }
@Test @Test
void execute_m3MultipleCandidates_allProcessed_batchSucceeds() throws Exception { void execute_multipleCandidates_allProcessed_batchSucceeds() throws Exception {
MockRunLockPort lockPort = new MockRunLockPort(); MockRunLockPort lockPort = new MockRunLockPort();
StartConfiguration config = buildConfig(tempDir); StartConfiguration config = buildConfig(tempDir);
@@ -313,9 +313,9 @@ class M2BatchRunProcessingUseCaseTest {
FixedCandidatesPort candidatesPort = new FixedCandidatesPort(candidates); FixedCandidatesPort candidatesPort = new FixedCandidatesPort(candidates);
FixedExtractionPort extractionPort = new FixedExtractionPort(success); FixedExtractionPort extractionPort = new FixedExtractionPort(success);
M2BatchRunProcessingUseCase useCase = new M2BatchRunProcessingUseCase( BatchRunProcessingUseCase useCase = new BatchRunProcessingUseCase(
config, lockPort, candidatesPort, extractionPort); config, lockPort, candidatesPort, extractionPort);
BatchRunContext context = new BatchRunContext(new RunId("m3-multi"), Instant.now()); BatchRunContext context = new BatchRunContext(new RunId("multi"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context); BatchRunOutcome outcome = useCase.execute(context);

View File

@@ -15,7 +15,7 @@ import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome;
import de.gecheckt.pdf.umbenenner.application.port.in.RunBatchProcessingUseCase; import de.gecheckt.pdf.umbenenner.application.port.in.RunBatchProcessingUseCase;
import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationPort; import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationPort;
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort; import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort;
import de.gecheckt.pdf.umbenenner.application.usecase.M2BatchRunProcessingUseCase; import de.gecheckt.pdf.umbenenner.application.usecase.BatchRunProcessingUseCase;
import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext; import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext;
import de.gecheckt.pdf.umbenenner.domain.model.RunId; import de.gecheckt.pdf.umbenenner.domain.model.RunId;
@@ -36,7 +36,7 @@ import java.util.UUID;
* <li>Map the batch outcome to a process exit code</li> * <li>Map the batch outcome to a process exit code</li>
* </ol> * </ol>
* <p> * <p>
* Exit code semantics (M3): * Exit code semantics:
* <ul> * <ul>
* <li>{@code 0}: Batch run executed successfully; individual document failures do not * <li>{@code 0}: Batch run executed successfully; individual document failures do not
* change the exit code as long as the run itself completed without a hard infrastructure error.</li> * change the exit code as long as the run itself completed without a hard infrastructure error.</li>
@@ -101,7 +101,7 @@ public class BootstrapRunner {
/** /**
* Creates the BootstrapRunner with default factories for production use. * Creates the BootstrapRunner with default factories for production use.
* <p> * <p>
* Wires the full M3 processing pipeline: * Wires the full processing pipeline:
* <ul> * <ul>
* <li>{@link PropertiesConfigurationPortAdapter} for configuration loading</li> * <li>{@link PropertiesConfigurationPortAdapter} for configuration loading</li>
* <li>{@link FilesystemRunLockPortAdapter} for exclusive run locking</li> * <li>{@link FilesystemRunLockPortAdapter} for exclusive run locking</li>
@@ -113,7 +113,7 @@ public class BootstrapRunner {
this.configPortFactory = PropertiesConfigurationPortAdapter::new; this.configPortFactory = PropertiesConfigurationPortAdapter::new;
this.runLockPortFactory = FilesystemRunLockPortAdapter::new; this.runLockPortFactory = FilesystemRunLockPortAdapter::new;
this.validatorFactory = StartConfigurationValidator::new; this.validatorFactory = StartConfigurationValidator::new;
this.useCaseFactory = (config, lock) -> new M2BatchRunProcessingUseCase( this.useCaseFactory = (config, lock) -> new BatchRunProcessingUseCase(
config, config,
lock, lock,
new SourceDocumentCandidatesPortAdapter(config.sourceFolder()), new SourceDocumentCandidatesPortAdapter(config.sourceFolder()),
@@ -172,7 +172,7 @@ public class BootstrapRunner {
} }
RunLockPort runLockPort = runLockPortFactory.create(lockFilePath); RunLockPort runLockPort = runLockPortFactory.create(lockFilePath);
// Step 5: Create the batch run context (M2-AP-003) // Step 5: Create the batch run context
// Generate a unique run ID and initialize the run context // Generate a unique run ID and initialize the run context
RunId runId = new RunId(UUID.randomUUID().toString()); RunId runId = new RunId(UUID.randomUUID().toString());
BatchRunContext runContext = new BatchRunContext(runId, Instant.now()); BatchRunContext runContext = new BatchRunContext(runId, Instant.now());
@@ -180,7 +180,7 @@ public class BootstrapRunner {
// Step 6: Create the use case with the validated config and run lock (application layer). // Step 6: Create the use case with the validated config and run lock (application layer).
// Config is passed directly; the use case does not re-read the properties file. // Config is passed directly; the use case does not re-read the properties file.
// M3 adapters (source document port, PDF extraction port) are wired by the factory. // Adapters (source document port, PDF extraction port) are wired by the factory.
RunBatchProcessingUseCase useCase = useCaseFactory.create(config, runLockPort); RunBatchProcessingUseCase useCase = useCaseFactory.create(config, runLockPort);
// Step 7: Create the CLI command adapter with the use case // Step 7: Create the CLI command adapter with the use case

View File

@@ -0,0 +1,31 @@
package de.gecheckt.pdf.umbenenner.domain.model;
/**
* Sealed interface representing the complete outcome of document processing.
* <p>
* This interface models all possible document outcomes:
* <ul>
* <li>{@link PreCheckPassed}: Document passed all pre-checks</li>
* <li>{@link PreCheckFailed}: Document failed a pre-check (deterministic content error: no usable text or page limit exceeded)</li>
* <li>{@link TechnicalDocumentError}: Technical failure during candidate access or PDF extraction (I/O, access, parsing, etc.)</li>
* </ul>
* <p>
* Design principles:
* <ul>
* <li>Exhaustive: All document processing outcomes are covered</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 (PreCheckFailed) vs. technical failures (TechnicalDocumentError)</li>
* </ul>
* <p>
* Error classification:
* <ul>
* <li>PreCheckPassed: Extraction succeeded and all pre-checks passed (ready for further processing)</li>
* <li>PreCheckFailed: Extraction succeeded but deterministic content check failed (no usable text, page limit exceeded)</li>
* <li>TechnicalDocumentError: Extraction failed due to technical issue (I/O, file access, PDF parsing, etc.)</li>
* </ul>
*/
public sealed interface DocumentProcessingOutcome
permits PreCheckPassed, PreCheckFailed, TechnicalDocumentError {
// Marker interface; concrete implementations define structure
}

View File

@@ -1,33 +0,0 @@
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

@@ -1,30 +0,0 @@
package de.gecheckt.pdf.umbenenner.domain.model;
/**
* Sealed interface representing the outcome of M3 document pre-checks.
* <p>
* This interface introduced in AP-001 establishes the architectural
* pattern for M3 pre-check results. The actual pre-check logic (fachlich validation
* such as "brauchbarer Text" and "Seitenlimit") is implemented in AP-004 via
* {@link de.gecheckt.pdf.umbenenner.application.service.M3PreCheckEvaluator}.
* <p>
* There are two allowed implementations:
* <ul>
* <li>{@link M3PreCheckPassed}: Document passed all M3 pre-checks and is ready for KI integration</li>
* <li>{@link M3PreCheckFailed}: Document failed an M3 pre-check and will not proceed further in this run</li>
* </ul>
* <p>
* Design principles:
* <ul>
* <li>Sealed: enforces exhaustive handling of all cases</li>
* <li>Carries both success path ({@link M3PreCheckPassed}) and failure reason ({@link M3PreCheckFailed})</li>
* <li>Defined early (AP-001) to ensure architecture is established before logic arrives</li>
* <li>Future-extensible for additional pre-check variants in later milestones</li>
* </ul>
*
* @since M3-AP-001
*/
public sealed interface M3ProcessingDecision
permits M3PreCheckPassed, M3PreCheckFailed {
// Marker interface; concrete implementations define structure
}

View File

@@ -3,7 +3,7 @@ package de.gecheckt.pdf.umbenenner.domain.model;
import java.util.Objects; import java.util.Objects;
/** /**
* Represents a document that failed an M3 pre-check. * Represents a document that failed a pre-check.
* <p> * <p>
* This result encapsulates: * This result encapsulates:
* <ul> * <ul>
@@ -22,12 +22,11 @@ import java.util.Objects;
* *
* @param candidate the source document metadata * @param candidate the source document metadata
* @param failureReason a human-readable explanation of the pre-check failure * @param failureReason a human-readable explanation of the pre-check failure
* @since M3-AP-001
*/ */
public record M3PreCheckFailed( public record PreCheckFailed(
SourceDocumentCandidate candidate, SourceDocumentCandidate candidate,
String failureReason String failureReason
) implements M3ProcessingDecision, M3DocumentProcessingOutcome { ) implements ProcessingDecision, DocumentProcessingOutcome {
/** /**
* Constructor with validation. * Constructor with validation.
* *
@@ -36,7 +35,7 @@ public record M3PreCheckFailed(
* @throws NullPointerException if either parameter is null * @throws NullPointerException if either parameter is null
* @throws IllegalArgumentException if failureReason is empty * @throws IllegalArgumentException if failureReason is empty
*/ */
public M3PreCheckFailed { public PreCheckFailed {
Objects.requireNonNull(candidate, "candidate must not be null"); Objects.requireNonNull(candidate, "candidate must not be null");
Objects.requireNonNull(failureReason, "failureReason must not be null"); Objects.requireNonNull(failureReason, "failureReason must not be null");
if (failureReason.isEmpty()) { if (failureReason.isEmpty()) {

View File

@@ -1,9 +1,9 @@
package de.gecheckt.pdf.umbenenner.domain.model; package de.gecheckt.pdf.umbenenner.domain.model;
/** /**
* Enumeration of M3 pre-check failure reasons. * Enumeration of pre-check failure reasons.
* <p> * <p>
* These are the deterministic content errors that can occur during M3 pre-check evaluation. * These are the deterministic content errors that can occur during pre-check evaluation.
* They distinguish between failures in the document content versus technical extraction failures. * They distinguish between failures in the document content versus technical extraction failures.
* <p> * <p>
* Deterministic content errors: * Deterministic content errors:
@@ -12,19 +12,17 @@ package de.gecheckt.pdf.umbenenner.domain.model;
* <li>{@link #PAGE_LIMIT_EXCEEDED}: The document exceeds the configured page limit.</li> * <li>{@link #PAGE_LIMIT_EXCEEDED}: The document exceeds the configured page limit.</li>
* </ul> * </ul>
* <p> * <p>
* Note: Technical extraction failures (I/O errors, PDFBox failures) are not M3 pre-check reasons; * Note: Technical extraction failures (I/O errors, PDFBox failures) are not pre-check reasons;
* they are represented as {@link PdfExtractionTechnicalError} in the extraction result. * they are represented as {@link PdfExtractionTechnicalError} in the extraction result.
*
* @since M3-AP-004
*/ */
public enum M3PreCheckFailureReason { public enum PreCheckFailureReason {
/** /**
* The extracted PDF text, after normalization, contains no letters or digits. * The extracted PDF text, after normalization, contains no letters or digits.
* <p> * <p>
* This is a deterministic content error: reprocessing the same file in a later run * This is a deterministic content error: reprocessing the same file in a later run
* will have the same outcome unless the source file is changed. * will have the same outcome unless the source file is changed.
* <p> * <p>
* In M3, retry logic: exactly 1 retry in a later batch run. * Retry logic: exactly 1 retry in a later batch run.
*/ */
NO_USABLE_TEXT("No usable text in extracted PDF content"), NO_USABLE_TEXT("No usable text in extracted PDF content"),
@@ -33,13 +31,13 @@ public enum M3PreCheckFailureReason {
* <p> * <p>
* This is a deterministic content error: the page count will not change unless the source file is modified. * This is a deterministic content error: the page count will not change unless the source file is modified.
* <p> * <p>
* In M3, retry logic: exactly 1 retry in a later batch run. * Retry logic: exactly 1 retry in a later batch run.
*/ */
PAGE_LIMIT_EXCEEDED("Document page count exceeds configured limit"); PAGE_LIMIT_EXCEEDED("Document page count exceeds configured limit");
private final String description; private final String description;
M3PreCheckFailureReason(String description) { PreCheckFailureReason(String description) {
this.description = description; this.description = description;
} }

View File

@@ -3,7 +3,7 @@ package de.gecheckt.pdf.umbenenner.domain.model;
import java.util.Objects; import java.util.Objects;
/** /**
* Represents a document that passed all M3 pre-checks. * Represents a document that passed all pre-checks.
* <p> * <p>
* This result encapsulates: * This result encapsulates:
* <ul> * <ul>
@@ -11,17 +11,16 @@ import java.util.Objects;
* <li>The successful PDF text extraction result</li> * <li>The successful PDF text extraction result</li>
* </ul> * </ul>
* <p> * <p>
* A document with this decision is ready to proceed to M4 and later milestones * A document with this decision is ready to proceed to further processing steps
* (fingerprinting, persistence, KI integration, filename generation, target copy). * (fingerprinting, persistence, KI integration, filename generation, target copy).
* *
* @param candidate the source document metadata * @param candidate the source document metadata
* @param extraction the successful text extraction result * @param extraction the successful text extraction result
* @since M3-AP-001
*/ */
public record M3PreCheckPassed( public record PreCheckPassed(
SourceDocumentCandidate candidate, SourceDocumentCandidate candidate,
PdfExtractionSuccess extraction PdfExtractionSuccess extraction
) implements M3ProcessingDecision, M3DocumentProcessingOutcome { ) implements ProcessingDecision, DocumentProcessingOutcome {
/** /**
* Constructor with validation. * Constructor with validation.
* *
@@ -29,7 +28,7 @@ public record M3PreCheckPassed(
* @param extraction must be non-null * @param extraction must be non-null
* @throws NullPointerException if either parameter is null * @throws NullPointerException if either parameter is null
*/ */
public M3PreCheckPassed { public PreCheckPassed {
Objects.requireNonNull(candidate, "candidate must not be null"); Objects.requireNonNull(candidate, "candidate must not be null");
Objects.requireNonNull(extraction, "extraction must not be null"); Objects.requireNonNull(extraction, "extraction must not be null");
} }

View File

@@ -0,0 +1,27 @@
package de.gecheckt.pdf.umbenenner.domain.model;
/**
* Sealed interface representing the outcome of document pre-checks.
* <p>
* This interface establishes the architectural pattern for pre-check results.
* The actual pre-check logic (fachlich validation such as "brauchbarer Text" and "Seitenlimit")
* is implemented via {@link de.gecheckt.pdf.umbenenner.application.service.PreCheckEvaluator}.
* <p>
* There are two allowed implementations:
* <ul>
* <li>{@link PreCheckPassed}: Document passed all pre-checks and is ready for further processing</li>
* <li>{@link PreCheckFailed}: Document failed a pre-check and will not proceed further in this run</li>
* </ul>
* <p>
* Design principles:
* <ul>
* <li>Sealed: enforces exhaustive handling of all cases</li>
* <li>Carries both success path ({@link PreCheckPassed}) and failure reason ({@link PreCheckFailed})</li>
* <li>Defined early to ensure architecture is established before logic arrives</li>
* <li>Future-extensible for additional pre-check variants in later milestones</li>
* </ul>
*/
public sealed interface ProcessingDecision
permits PreCheckPassed, PreCheckFailed {
// Marker interface; concrete implementations define structure
}

View File

@@ -20,19 +20,18 @@ import java.util.Objects;
* <li>Out of memory during extraction</li> * <li>Out of memory during extraction</li>
* </ul> * </ul>
* <p> * <p>
* This is distinct from {@link M3ExtractedContentError}, which represents problems with the document * This is distinct from content errors, which represent problems with the document
* content itself rather than infrastructure failures. * content itself rather than infrastructure failures.
* *
* @param candidate the source document metadata * @param candidate the source document metadata
* @param errorMessage a description of the technical failure * @param errorMessage a description of the technical failure
* @param cause the underlying exception, if any (may be null) * @param cause the underlying exception, if any (may be null)
* @since M3-AP-006
*/ */
public record M3TechnicalDocumentError( public record TechnicalDocumentError(
SourceDocumentCandidate candidate, SourceDocumentCandidate candidate,
String errorMessage, String errorMessage,
Throwable cause Throwable cause
) implements M3DocumentProcessingOutcome { ) implements DocumentProcessingOutcome {
/** /**
* Constructor with validation. * Constructor with validation.
* *
@@ -42,7 +41,7 @@ public record M3TechnicalDocumentError(
* @throws NullPointerException if candidate or errorMessage is null * @throws NullPointerException if candidate or errorMessage is null
* @throws IllegalArgumentException if errorMessage is empty * @throws IllegalArgumentException if errorMessage is empty
*/ */
public M3TechnicalDocumentError { public TechnicalDocumentError {
Objects.requireNonNull(candidate, "candidate must not be null"); Objects.requireNonNull(candidate, "candidate must not be null");
Objects.requireNonNull(errorMessage, "errorMessage must not be null"); Objects.requireNonNull(errorMessage, "errorMessage must not be null");
if (errorMessage.isEmpty()) { if (errorMessage.isEmpty()) {

View File

@@ -3,34 +3,34 @@
* <p> * <p>
* This package contains the fundamental domain entities and status models required for document processing: * This package contains the fundamental domain entities and status models required for document processing:
* <ul> * <ul>
* <li>{@link de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus} — enumeration of all valid document processing states (M2-AP-001)</li> * <li>{@link de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus} — enumeration of all valid document processing states</li>
* <li>{@link de.gecheckt.pdf.umbenenner.domain.model.RunId} — unique identifier for a batch run (M2-AP-003)</li> * <li>{@link de.gecheckt.pdf.umbenenner.domain.model.RunId} — unique identifier for a batch run</li>
* <li>{@link de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext} — technical context for a batch run (M2-AP-003)</li> * <li>{@link de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext} — technical context for a batch run</li>
* <li>{@link de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate} — discovered PDF from source folder (M3-AP-001)</li> * <li>{@link de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate} — discovered PDF from source folder</li>
* <li>{@link de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentLocator} — opaque locator passed from scan adapter to extraction adapter (M3-AP-001)</li> * <li>{@link de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentLocator} — opaque locator passed from scan adapter to extraction adapter</li>
* <li>{@link de.gecheckt.pdf.umbenenner.domain.model.PdfPageCount} — typed page count validation (M3-AP-001)</li> * <li>{@link de.gecheckt.pdf.umbenenner.domain.model.PdfPageCount} — typed page count validation</li>
* <li>{@link de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionResult} — sealed result of PDF text extraction (M3-AP-001)</li> * <li>{@link de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionResult} — sealed result of PDF text extraction</li>
* <li>{@link de.gecheckt.pdf.umbenenner.domain.model.M3ProcessingDecision} — sealed result of M3 pre-checks (M3-AP-001)</li> * <li>{@link de.gecheckt.pdf.umbenenner.domain.model.ProcessingDecision} — sealed result of pre-checks</li>
* </ul> * </ul>
* <p> * <p>
* Additional classes introduced in M3: * Additional classes:
* <ul> * <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.PreCheckFailureReason} — enumeration of pre-check failure reasons</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.DocumentProcessingOutcome} — sealed interface for all document processing outcomes</li>
* <li>{@link de.gecheckt.pdf.umbenenner.domain.model.M3TechnicalDocumentError} — technical failure during extraction (M3-AP-006)</li> * <li>{@link de.gecheckt.pdf.umbenenner.domain.model.TechnicalDocumentError} — technical failure during extraction</li>
* </ul> * </ul>
* *
* Implementation classes: * Implementation classes:
* <ul> * <ul>
* <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.PreCheckPassed} — document passed pre-checks</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> * <li>{@link de.gecheckt.pdf.umbenenner.domain.model.PreCheckFailed} — document failed pre-check</li>
* </ul> * </ul>
* *
* M3 Document Processing Outcome Model (M3-AP-006): * Document Processing Outcome Model:
* The complete M3 document processing pipeline results in one of three outcomes, all implementing * The complete document processing pipeline results in one of three outcomes, all implementing
* {@link de.gecheckt.pdf.umbenenner.domain.model.M3DocumentProcessingOutcome}: * {@link de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome}:
* <ul> * <ul>
* <li>Pre-check passed: Document text extracted and validated successfully (ready for M4+)</li> * <li>Pre-check passed: Document text extracted and validated successfully (ready for further processing)</li>
* <li>Pre-check failed: Deterministic content error (no usable text, page limit exceeded)</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> * <li>Technical document error: Infrastructure or parsing failure (I/O, access, PDF parsing)</li>
* </ul> * </ul>
@@ -41,7 +41,5 @@
* <li>Immutable value objects or enumerations</li> * <li>Immutable value objects or enumerations</li>
* <li>Reusable across all layers via the Application and Adapter contracts</li> * <li>Reusable across all layers via the Application and Adapter contracts</li>
* </ul> * </ul>
*
* @since M2-AP-001
*/ */
package de.gecheckt.pdf.umbenenner.domain.model; package de.gecheckt.pdf.umbenenner.domain.model;

View File

@@ -0,0 +1,111 @@
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 document processing outcome types.
* <p>
* Verifies that all outcome types are properly created and validated.
*/
class DocumentProcessingOutcomeTest {
@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 testTechnicalDocumentError_ValidConstruction() {
// Act
var error = new TechnicalDocumentError(candidate, "I/O error", null);
// Assert
assertEquals(candidate, error.candidate());
assertEquals("I/O error", error.errorMessage());
assertNull(error.cause());
}
@Test
void testTechnicalDocumentError_WithCause() {
// Arrange
var cause = new RuntimeException("File not found");
// Act
var error = new TechnicalDocumentError(candidate, "I/O error", cause);
// Assert
assertEquals(cause, error.cause());
}
@Test
void testTechnicalDocumentError_WithNullCandidate_ThrowsException() {
assertThrows(NullPointerException.class,
() -> new TechnicalDocumentError(null, "Error", null));
}
@Test
void testTechnicalDocumentError_WithNullErrorMessage_ThrowsException() {
assertThrows(NullPointerException.class,
() -> new TechnicalDocumentError(candidate, null, null));
}
@Test
void testTechnicalDocumentError_WithEmptyErrorMessage_ThrowsException() {
assertThrows(IllegalArgumentException.class,
() -> new TechnicalDocumentError(candidate, "", null));
}
@Test
void testTechnicalDocumentError_IsDocumentProcessingOutcome() {
// Verify type relationship
var error = new TechnicalDocumentError(candidate, "Error", null);
assertInstanceOf(DocumentProcessingOutcome.class, error);
}
@Test
void testPreCheckPassed_IsDocumentProcessingOutcome() {
// Verify type relationship
var extraction = new PdfExtractionSuccess("text", new PdfPageCount(1));
var passed = new PreCheckPassed(candidate, extraction);
assertInstanceOf(DocumentProcessingOutcome.class, passed);
}
@Test
void testPreCheckFailed_IsDocumentProcessingOutcome() {
// Verify type relationship
var failed = new PreCheckFailed(candidate, "Test failure reason");
assertInstanceOf(DocumentProcessingOutcome.class, failed);
}
@Test
void testAllOutcomesAreExhaustive() {
// This test verifies that the outcome types are the only implementations
var extraction = new PdfExtractionSuccess("text", new PdfPageCount(1));
DocumentProcessingOutcome[] outcomes = {
new PreCheckPassed(candidate, extraction),
new PreCheckFailed(candidate, "Deterministic content failure"),
new TechnicalDocumentError(candidate, "Technical extraction error", null)
};
for (DocumentProcessingOutcome outcome : outcomes) {
assertInstanceOf(DocumentProcessingOutcome.class, outcome);
}
}
}

View File

@@ -1,113 +0,0 @@
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);
}
}
}