# WP3-003b – IMPLEMENTATION PLAN
## Discovery Access Audit

**Status:** Proposed for PL approval  
**Version:** 0.1  
**Date:** 2026-08-07  
**Basis:** `reconcile_ADR002.WP2_20260807_0.zip`  
**Depends on:** WP3-001, WP3-002, WP3-003, WP3-003a, ED-04-010  
**Reference:** `WP3-003b_DISCOVERY_ACCESS_AUDIT_v0.1.md`

PL, 2026-08-08:
- Umsetzung auf localhost + dev.reconcilix.net/v2 erfolgreich getestet.
Abschlussbericht ED:
```text
  SUCCESS, EMPTY und ERROR praktisch verifiziert.
  xTree und Lobid/GND als unterschiedliche externe Candidate Provider verifiziert.
  discoveryAccessId ermöglicht die Korrelation zum technischen Error Log.
  TIMEOUT und HTTP_ERROR werden unterscheidbar diagnostiziert.
  Query, Adapter, Service, Operation, HTTP-Status und Laufzeiten liefern die benötigte technische Nachvollziehbarkeit.
  sensible Response-Inhalte werden redigiert.
  die notwendige xTree-302-Korrektur im XtreeJsonApiClient ist Bestandteil des finalen Implementierungsstands.
  der xTree-HTTP-500 bei tokenführendem Minus ist ein bekanntes Problem des externen Legacy-API und kein Reconcilix-Fix; der Patch bleibt folgerichtig bei digiCULT.
```


---

## 1. Ziel

WP3-003b ergänzt Reconcilix um ein strukturiertes, dateibasiertes **Discovery Access Audit** für externe Candidate-Discovery-Zugriffe.

Initial erfasst werden:

```text
xTree API
lobid/GND Reconciliation API
```

Nicht erfasst wird:

```text
LocalReconciliationStoreAdapter
```

Das Paket verändert weder Candidate-Discovery-Semantik noch Ranking, Persistenz oder Fehlerverhalten gegenüber OpenRefine.

Die Implementierung bleibt KISS:

```text
Audit Entry
+
Logger Interface
+
JSONL Logger
+
strukturierte externe Fehlerdiagnostik
+
Integration xTree / lobid
```

---

## 2. Befund im aktuellen Code

### 2.1 SourceDelivery

`ReconciliationController` erzeugt bei jedem OpenRefine-POST eine `SourceDelivery` und übergibt deren `SourceDeliveryId` an `RuntimeFactory::create()`.

Aktuell:

```text
ReconciliationController
    ↓
OpenRefineSourceDeliveryRegistrar
    ↓
SourceDelivery
    ↓
SourceDeliveryId
    ↓
RuntimeFactory
```

Nach ED-04-010 bleibt dieses Verhalten unverändert.

### 2.2 RuntimeFactory

`RuntimeFactory` erzeugt aktuell die produktiven Discovery Adapter:

```text
LocalReconciliationStoreAdapter
XtreeJsonApiAdapter
LobidGndSubjectHeadingAdapter
LegacyDiscoveryCompatibilityAdapter
```

Damit ist `RuntimeFactory` der geeignete Composition Point für die Audit-Infrastruktur.

### 2.3 xTree

`XtreeJsonApiClient` kennt bereits:

```text
HTTP status
raw response
cURL error
operation
```

verwirft diese technischen Informationen jedoch vor Rückgabe beziehungsweise vor dem Werfen einer generischen `RuntimeException`.

`XtreeJsonApiAdapter` kennt zusätzlich:

```text
ReconciliationCommand.requestId
query
targetVocabularyUri
scopeVocabularyUri
tenant
adapter route
```

### 2.4 lobid

Für `LobidGndReconcileClient` und `LobidGndSubjectHeadingAdapter` gilt dasselbe Muster:

```text
Client
→ kennt HTTP-Diagnostik

Adapter
→ kennt Discovery-Kontext
```

### 2.5 Konsequenz

Das Audit darf weder ausschließlich im HTTP-Client noch ausschließlich im Controller implementiert werden.

Der Adapter ist der richtige Ort für den vollständigen Audit Entry.

Die HTTP-Clients müssen lediglich strukturierte technische Fehlerinformationen verfügbar machen.

---

## 3. Keine Änderung an ApplicationServiceFactory

`ApplicationServiceFactory::create()` bleibt unverändert:

```php
public function create(
    TenantContext $tenant,
    SourceDeliveryId $sourceDeliveryId,
): ReconciliationApplicationService;
```

WP3-003b führt keine Audit-spezifischen Parameter in den Application Contract ein.

Damit bleibt die Application-Grenze sauber.

---

## 4. Zugriff auf SourceDelivery-Metadaten

Für das Audit werden neben `sourceDeliveryId` benötigt:

```text
externalDeliveryId
sourceSystemId
deliveryChannel
tenantId
```

Diese Werte existieren bereits im `SourceDelivery` Aggregate.

### Entscheidung

`RuntimeFactory` erhält optional ein:

```php
?SourceDeliveryRepository
```

und lädt beim Erzeugen der Runtime genau einmal:

```php
$sourceDeliveryRepository->findById($sourceDeliveryId)
```

Aus dem Aggregate wird anschließend ein kleiner Infrastructure Context erzeugt.

Vorteile:

- keine Änderung am Controller-Contract,
- keine Änderung am ApplicationServiceFactory-Contract,
- keine OpenRefine-Sondersemantik,
- zukünftige Delivery Adapter profitieren automatisch von den tatsächlich im Aggregate gespeicherten Werten.

Bei deaktivierter Datenbank oder nicht verfügbarem Aggregate bleibt das Audit technisch nutzbar; nicht verfügbare optionale Felder werden nicht erfunden.

---

## 5. Neue Klasse: DiscoveryAccessAuditContext

**Pfad**

```text
src/Infrastructure/Discovery/Audit/DiscoveryAccessAuditContext.php
```

**Typ**

```php
final readonly class DiscoveryAccessAuditContext
```

Felder:

```text
sourceDeliveryId
tenantId
externalDeliveryId?
sourceSystemId?
deliveryChannel?
```

Der Context enthält ausschließlich Korrelationsdaten des aktuellen Reconcilix Delivery-Batches.

Er enthält ausdrücklich keine `ReconciliationRunId`.

---

## 6. Neue Klasse: DiscoveryAccessAuditEntry

**Pfad**

```text
src/Infrastructure/Discovery/Audit/DiscoveryAccessAuditEntry.php
```

**Typ**

```php
final readonly class DiscoveryAccessAuditEntry
```

Felder gemäß freigegebenem WP:

```text
discoveryAccessId
timestamp

sourceDeliveryId
externalDeliveryId?
tenantId
sourceSystemId?
deliveryChannel?

queryId
query?

adapterKey
service
operation

targetVocabularyUri
scopeVocabularyUri?

durationMs
status
candidateCount?

httpStatus?
responseBytes?

errorType?
errorMessage?
responsePreview?
```

### Invarianten

- `discoveryAccessId` darf nicht leer sein.
- `sourceDeliveryId` darf nicht leer sein.
- `tenantId` darf nicht leer sein.
- `adapterKey`, `service`, `operation`, `targetVocabularyUri` dürfen nicht leer sein.
- `durationMs >= 0`.
- `SUCCESS` verlangt `candidateCount > 0`.
- `EMPTY` verlangt `candidateCount === 0`.
- `ERROR` verlangt `errorType`.
- bei `SUCCESS`/`EMPTY` bleiben Error-Felder leer.

---

## 7. Statuswerte

**Neue Klasse**

```text
src/Infrastructure/Discovery/Audit/DiscoveryAccessStatus.php
```

KISS-Konstanten oder Enum:

```text
SUCCESS
EMPTY
ERROR
```

Empfehlung:

```php
enum DiscoveryAccessStatus: string
```

---

## 8. Error Types

**Neue Klasse**

```text
src/Infrastructure/Discovery/Audit/DiscoveryAccessErrorType.php
```

Initial:

```text
TIMEOUT
HTTP_ERROR
INVALID_RESPONSE
CONNECTION_ERROR
UNKNOWN
```

Empfehlung ebenfalls als String-Enum.

---

## 9. Logger Contract

**Pfad**

```text
src/Infrastructure/Discovery/Audit/DiscoveryAccessAuditLogger.php
```

```php
interface DiscoveryAccessAuditLogger
{
    public function log(DiscoveryAccessAuditEntry $entry): void;
}
```

Der Adapter kennt ausschließlich dieses Interface.

---

## 10. Null Logger

Zusätzlich wird ein kleiner:

```text
NullDiscoveryAccessAuditLogger
```

eingeführt.

**Pfad**

```text
src/Infrastructure/Discovery/Audit/NullDiscoveryAccessAuditLogger.php
```

Damit benötigen die Adapter keine:

```php
if ($logger !== null)
```

Verzweigungen.

Bei deaktiviertem Audit wird der Null Logger verwendet.

---

## 11. JSONL Logger

**Pfad**

```text
src/Infrastructure/Discovery/Audit/JsonlDiscoveryAccessAuditLogger.php
```

### Root-Verzeichnis

Konfiguriert:

```text
<project>/logs/discovery
```

### Monatsstruktur

```text
logs/discovery/YYYY-MM/
```

### Dateiname

```text
{sourceDeliveryId}.jsonl
```

Beispiel:

```text
logs/discovery/2026-08/
001bef3e-bdf1-4db1-bd5a-e12a4646f3b0.jsonl
```

### Schreibweise

Jeder Access erzeugt genau **eine JSON-Zeile**:

```php
json_encode(
    $entryData,
    JSON_UNESCAPED_UNICODE
    | JSON_UNESCAPED_SLASHES
    | JSON_THROW_ON_ERROR
)
```

anschließend:

```text
"\n"
```

und `FILE_APPEND | LOCK_EX`.

Keine Pretty-Print-Ausgabe in JSONL.

---

## 12. Audit-Konfiguration

Die bestehende `config/providers.php` erhält unter `service`:

```php
'discovery_audit' => [
    'enabled' => true,
    'log_query_values' => true,
    'response_preview_max_length' => 1000,
],
```

Damit ist keine neue Konfigurationsdatei notwendig.

### Configuration

`Configuration` erhält:

```php
public function discoveryAudit(): array
```

mit defensiven Defaults:

```text
enabled = false
log_query_values = false
response_preview_max_length = 1000
```

Die Validierung stellt sicher:

- `enabled` boolean,
- `log_query_values` boolean,
- `response_preview_max_length >= 0`.

---

## 13. Technische Fehlerdiagnostik der Provider

Die vorhandenen Clients werfen derzeit generische `RuntimeException`.

Damit gingen beim realen xTree-Fehler genau die Informationen verloren, die WP3-003b benötigt.

### Neue technische Exception

**Pfad**

```text
src/Provider/External/ExternalServiceRequestException.php
```

**Typ**

```php
final class ExternalServiceRequestException extends RuntimeException
```

Felder:

```text
service
operation
errorType
httpStatus?
responseBytes?
responsePreview?
```

Die Exception enthält **keine Credentials**, Cookies oder Request Header.

Sie dient ausschließlich als Transport technischer Diagnoseinformationen vom Provider Client zum Adapter.

---

## 14. xTree Client – Änderungen

**Datei**

```text
src/Provider/Xtree/XtreeJsonApiClient.php
```

### Operationen

Die bestehenden Methoden bleiben erhalten:

```text
searchVocItemsByTerm()
getFetchHierarchy()
```

Ihre Rückgabe bleibt:

```text
array
```

Damit müssen Mapper und reguläre Erfolgs-Tests nicht auf ein neues Response-Wrapper-Modell umgebaut werden.

### Fehlerfälle

`getJson()` erhält zusätzlich den Operationsnamen:

```text
getSearchVocItemsByTerm
getFetchHierarchy
```

und wirft bei technischen Fehlern `ExternalServiceRequestException`.

Mapping:

```text
cURL timeout
→ TIMEOUT

anderer cURL-Fehler
→ CONNECTION_ERROR

HTTP außerhalb 2xx
→ HTTP_ERROR

ungültiges JSON
→ INVALID_RESPONSE
```

### Fehlerdiagnostik

Bei HTTP-/Response-Fehlern werden aufgenommen:

```text
httpStatus
strlen(raw response)
gekürzte responsePreview
```

Die Preview wird bereits auf Client-Ebene begrenzt beziehungsweise dem Logger nur als sanitizable Rohwert zur Verfügung gestellt.

### Login

Auch xTree Login-Fehler werden strukturiert klassifiziert.

Credentials, POST-Felder und Session Cookie werden niemals in Exception oder Audit übernommen.

---

## 15. lobid Client – Änderungen

**Datei**

```text
src/Provider/Lobid/LobidGndReconcileClient.php
```

Die öffentliche Methode:

```php
reconcile(string $query, string $type, int $limit): array
```

bleibt unverändert.

`postForm()` und JSON-Prüfung werfen bei externen technischen Problemen `ExternalServiceRequestException`.

Mapping analog xTree:

```text
cURL timeout       → TIMEOUT
cURL connection    → CONNECTION_ERROR
HTTP != 2xx        → HTTP_ERROR
invalid JSON       → INVALID_RESPONSE
missing q0/result  → INVALID_RESPONSE
```

Damit bleibt der erfolgreiche Adapter-Contract unverändert.

---

## 16. UUID-Erzeugung

Für `discoveryAccessId` wird kein Persistenz-Identifier wiederverwendet.

KISS:

```php
DiscoveryAccessId::generate()
```

### Neue kleine Value Class

**Pfad**

```text
src/Infrastructure/Discovery/Audit/DiscoveryAccessId.php
```

Sie erzeugt eine UUID in derselben technischen Art wie bestehende Reconcilix UUID-Identifier.

Die ID existiert ausschließlich im Audit-Kontext.

Keine Datenbankmigration.

---

## 17. xTree Adapter – Integration

**Datei**

```text
src/Infrastructure/Discovery/Adapter/XtreeJsonApiAdapter.php
```

Neue Constructor Dependencies:

```text
adapterKey
DiscoveryAccessAuditContext
DiscoveryAccessAuditLogger
logQueryValues
responsePreviewMaxLength
```

### Ablauf

Nach allen lokalen Scope-/Tenant-Prüfungen, unmittelbar vor dem externen Aufruf:

```text
generate discoveryAccessId
start timer
determine operation
```

Danach:

```text
external xTree call
→ mapping
→ candidate count
→ audit SUCCESS/EMPTY
```

Bei `ExternalServiceRequestException`:

```text
audit ERROR
→ technical diagnostics übernehmen
→ discoveryAccessId zusätzlich error_log()
→ ursprüngliches Fehlerverhalten beibehalten
→ Exception weiterwerfen
```

Bei anderen unerwarteten Exceptions wird kein externer Providerfehler erfunden.

Sie bleiben reguläre Reconcilix/Application-Fehler.

### Operation

Root Vocabulary:

```text
getSearchVocItemsByTerm
```

SubVocabulary:

```text
getFetchHierarchy
```

---

## 18. lobid Adapter – Integration

**Datei**

```text
src/Infrastructure/Discovery/Adapter/LobidGndSubjectHeadingAdapter.php
```

Neue Dependencies analog xTree.

Operation:

```text
reconcile
```

Ablauf:

```text
generate access id
start timer
client.reconcile()
map candidates
log SUCCESS / EMPTY
```

Bei `ExternalServiceRequestException`:

```text
log ERROR
error_log with discoveryAccessId
rethrow
```

---

## 19. Query-ID

`ReconciliationCommand` besitzt bereits:

```text
requestId
```

Diese wird ohne neue Domain-Änderung als:

```text
queryId
```

in den Audit Entry übernommen.

Beispiel:

```text
q7
```

---

## 20. Query Value

Bei:

```text
log_query_values = true
```

wird:

```text
ReconciliationCommand.sourceValue
```

als `query` gespeichert.

Bei:

```text
false
```

wird das Feld vollständig weggelassen beziehungsweise `null` serialisiert nach finaler Serializer-Regel.

Es wird in v0.1 kein Hashing eingeführt.

---

## 21. RuntimeFactory – Änderungen

**Datei**

```text
src/Infrastructure/Factory/RuntimeFactory.php
```

Neue optionale Dependencies:

```text
?SourceDeliveryRepository
DiscoveryAccessAuditLogger
Audit configuration
```

### create()

Zu Beginn:

```text
SourceDeliveryId
        ↓
SourceDeliveryRepository.findById()
        ↓
DiscoveryAccessAuditContext
```

Wenn kein Repository vorhanden ist:

```text
sourceDeliveryId
tenantId
```

werden weiterhin gesetzt; nicht bekannte optionale Felder bleiben leer.

### Adapter-Erzeugung

`appendXtreeRouteAndAdapter()` und `appendLobidGndRouteAndAdapter()` übergeben:

```text
adapterKey
auditContext
auditLogger
audit config
```

Der LocalStore Adapter bleibt unverändert.

---

## 22. Composition Root – Änderungen

**Datei**

```text
src/Infrastructure/Composition/ReconciliationCompositionRoot.php
```

### Repository Factory

Die bisher intern wiederholt erzeugte SourceDelivery-Repository-Konstruktion wird in eine Hilfsmethode gezogen:

```php
createSourceDeliveryRepository(): SourceDeliveryRepository
```

Verwendung durch:

```text
SourceDeliveryService
RuntimeFactory
```

### Audit Logger

Neue Methode:

```php
createDiscoveryAccessAuditLogger(): DiscoveryAccessAuditLogger
```

Bei:

```text
enabled = false
```

→ `NullDiscoveryAccessAuditLogger`

Bei:

```text
enabled = true
```

→ `JsonlDiscoveryAccessAuditLogger`

Root Path:

```text
dirname(__DIR__, 3) . '/logs/discovery'
```

oder äquivalente robuste Projekt-Root-Auflösung.

Keine Pfadabhängigkeit von aktuellem Working Directory.

---

## 23. Audit Write Failure – wichtiges Verhalten

Das Logger Interface darf technisch Exceptions werfen.

Der Adapter schützt jedoch den Discovery-Prozess:

```php
try {
    $this->auditLogger->log($entry);
} catch (Throwable $auditError) {
    error_log('[discovery-audit] ...');
}
```

Damit gilt:

```text
Discovery SUCCESS
+
Audit ERROR
=
Discovery bleibt SUCCESS
```

und ebenso:

```text
Discovery ERROR
+
Audit ERROR
=
ursprünglicher Discovery Error bleibt maßgeblich
```

Audit darf niemals die ursprüngliche Exception ersetzen.

---

## 24. Response Preview Sanitizing

Der JSONL Logger beziehungsweise eine kleine private Sanitizing-Methode:

1. begrenzt auf `response_preview_max_length`,
2. entfernt/ersetzt erkennbare Credential-Muster,
3. schreibt niemals Request Header oder Cookies,
4. schreibt bei xTree niemals `PHPSESSID`,
5. schreibt Preview ausschließlich bei `status=ERROR`.

KISS: keine allgemeine DLP-/PII-Engine.

---

## 25. Bestehende Logs

Folgende bestehende Debug-Logs bleiben zunächst unverändert:

```text
logs/openrefine-post.log
logs/reconcilix_response.log
logs/phpLoggingdatei.log
```

WP3-003b ersetzt oder bereinigt diese Logs nicht.

Das ist ein separates späteres Operations-Thema.

Bei externem Discovery-Fehler wird zusätzlich ins bestehende PHP Error Log geschrieben:

```text
[discovery-access]
accessId=<uuid>
service=xtree|lobid
errorType=<...>
```

Keine Query und keine Secrets im technischen Error Log.

---

## 26. Geplanter Dateiumfang

### Neue Produktionsdateien

```text
src/Infrastructure/Discovery/Audit/
    DiscoveryAccessId.php
    DiscoveryAccessStatus.php
    DiscoveryAccessErrorType.php
    DiscoveryAccessAuditContext.php
    DiscoveryAccessAuditEntry.php
    DiscoveryAccessAuditLogger.php
    NullDiscoveryAccessAuditLogger.php
    JsonlDiscoveryAccessAuditLogger.php

src/Provider/External/
    ExternalServiceRequestException.php
```

### Geänderte Produktionsdateien

```text
src/Provider/Xtree/XtreeJsonApiClient.php
src/Provider/Lobid/LobidGndReconcileClient.php

src/Infrastructure/Discovery/Adapter/XtreeJsonApiAdapter.php
src/Infrastructure/Discovery/Adapter/LobidGndSubjectHeadingAdapter.php

src/Infrastructure/Factory/RuntimeFactory.php
src/Infrastructure/Composition/ReconciliationCompositionRoot.php
src/Infrastructure/Configuration/Configuration.php

config/providers.php
```

### Unverändert geplant

```text
src/Application/Reconciliation/ApplicationServiceFactory.php
src/Application/Reconciliation/ReconciliationApplicationService.php
src/Application/Reconciliation/ReconciliationCommand.php

src/Controller/ReconciliationController.php

src/Domain/Delivery/SourceDelivery.php
src/Domain/Reconciliation/SourceValue.php

src/Infrastructure/Discovery/Adapter/LocalReconciliationStoreAdapter.php
src/Infrastructure/Discovery/Routing/CandidateDiscoveryRouter.php

public/reconcile.php
```

Keine Datenbankmigration.

---

## 27. Neue Tests

### 27.1 DiscoveryAccessAuditEntryTest

```text
tests/Infrastructure/Discovery/Audit/DiscoveryAccessAuditEntryTest.php
```

Prüft:

- Invarianten,
- Status / candidateCount,
- ERROR / errorType.

### 27.2 JsonlDiscoveryAccessAuditLoggerTest

```text
tests/Infrastructure/Discovery/Audit/JsonlDiscoveryAccessAuditLoggerTest.php
```

Prüft:

- Monatsverzeichnis,
- Dateiname = sourceDeliveryId,
- eine Zeile pro Access,
- gültiges JSON,
- Append,
- Unicode,
- Query aus/an,
- Preview-Limit.

### 27.3 XtreeDiscoveryAuditIntegrationTest

```text
tests/Integration/XtreeDiscoveryAuditIntegrationTest.php
```

Prüft:

```text
SUCCESS
EMPTY
INVALID_RESPONSE
```

mit kontrolliertem Fake-Client.

### 27.4 LobidDiscoveryAuditIntegrationTest

```text
tests/Integration/LobidDiscoveryAuditIntegrationTest.php
```

analog xTree.

### 27.5 DiscoveryAuditFailureIsolationTest

```text
tests/Integration/DiscoveryAuditFailureIsolationTest.php
```

Ein absichtlich fehlerhafter Logger darf weder erfolgreiche noch fehlerhafte Discovery semantisch verändern.

### 27.6 Runtime Audit Configuration Test

Prüft:

```text
enabled=true  → Jsonl Logger
enabled=false → Null Logger
```

### 27.7 Regression

Bestehende Tests:

```text
WP3-001
WP3-002
WP3-003
WP3-003a
OpenRefineSourceDeliveryIntegrationTest
```

müssen unverändert erfolgreich bleiben.

---

## 28. Manueller Acceptance Test

Der bekannte xTree-Fall wird absichtlich reproduziert.

Voraussetzung:

Der xTree-Patch für:

```text
Alexianerkrankenhaus und -kloster
```

wird testweise entfernt.

### Erwartung

Reconcilix liefert weiterhin sein bestehendes Fehlerverhalten.

Zusätzlich existiert:

```text
logs/discovery/YYYY-MM/{sourceDeliveryId}.jsonl
```

mit einem Entry:

```text
queryId       = entsprechende q-ID
query         = Alexianerkrankenhaus und -kloster
service       = xtree
operation     = getSearchVocItemsByTerm
status        = ERROR
errorType     = INVALID_RESPONSE
httpStatus    = soweit verfügbar
responseBytes = soweit verfügbar
responsePreview = gekürzt
```

Das PHP Error Log enthält dieselbe:

```text
discoveryAccessId
```

Damit muss der Fehler ohne Rekonstruktion über fehlende `SourceValue`-Datensätze lokalisierbar sein.

---

## 29. Zweiter manueller Test – lobid

Ein kontrollierter technischer lobid-Fehler wird nach Möglichkeit über einen temporär falschen Test-Endpoint beziehungsweise einen Test-Client simuliert.

Kein absichtlicher Lasttest gegen den produktiven öffentlichen lobid-Dienst.

Erwartung:

```text
service = lobid
status = ERROR
errorType = HTTP_ERROR / CONNECTION_ERROR
```

Der reguläre lobid-Smoke-Test muss anschließend weiterhin erfolgreich funktionieren.

---

## 30. Risiken

### R-01 – Audit verändert bestehende Adapter zu stark

**Gegenmaßnahme:** erfolgreiche Client-Rückgabetypen bleiben unverändert; nur strukturierte technische Exception wird ergänzt.

### R-02 – Audit-Fehler beeinflusst Reconciliation

**Gegenmaßnahme:** Logger-Aufrufe sind strikt fail-safe gekapselt.

### R-03 – Sensitive Daten im Preview

**Gegenmaßnahme:** keine Header/Cookies, Preview nur bei ERROR, hartes Limit, einfache Sanitization.

### R-04 – SourceDelivery-Metadaten fehlen bei deaktivierter DB

**Gegenmaßnahme:** `sourceDeliveryId` und `tenantId` bleiben vorhanden; optionale Metadaten werden nicht erfunden.

### R-05 – External Service Error vs. Reconcilix Mapping Error

**Gegenmaßnahme:** nur `ExternalServiceRequestException` wird als externer technischer ERROR klassifiziert. Andere Runtime-/Mapping-Fehler bleiben normale Reconcilix-Fehler und werden nicht fälschlich dem externen Dienst zugerechnet.

### R-06 – Audit-Dateimenge wächst

**Gegenmaßnahme:** Monatspartitionierung. Rotation/Retention bleibt bewusst Operations/Parking Lot.

---

## 31. Akzeptanzkriterien

WP3-003b ist abgeschlossen, wenn:

1. jeder produktive externe xTree-Zugriff genau einen Audit Entry erzeugt;
2. jeder produktive externe lobid-Zugriff genau einen Audit Entry erzeugt;
3. LocalStore keinen externen Audit Entry erzeugt;
4. `SUCCESS`, `EMPTY`, `ERROR` unterschieden werden;
5. externe technische Fehler klassifiziert werden;
6. `discoveryAccessId` pro Access eindeutig ist;
7. `sourceDeliveryId` jeden Entry mit Reconcilix korreliert;
8. `externalDeliveryId` soweit vorhanden aus dem SourceDelivery Aggregate übernommen wird;
9. die Datei unter `logs/discovery/YYYY-MM/{sourceDeliveryId}.jsonl` liegt;
10. Query Logging abschaltbar ist;
11. Error Preview begrenzt und credential-frei ist;
12. Audit Write Failure Candidate Discovery niemals verändert;
13. bestehendes OpenRefine-Response-Verhalten unverändert bleibt;
14. keine Domain-/DB-Migration erforderlich ist;
15. xTree Regression Tests erfolgreich sind;
16. lobid Regression Tests erfolgreich sind;
17. LocalStore Regression Tests erfolgreich sind;
18. der reproduzierte xTree-Fehler über die Audit-Datei unmittelbar diagnostizierbar ist.

---

## 32. Engineering-Votum

Der aktuelle Code bietet einen günstigen Integrationspunkt für WP3-003b:

```text
RuntimeFactory
→ besitzt SourceDeliveryId

SourceDeliveryRepository
→ liefert vorhandene Delivery-Metadaten

Adapter
→ besitzt fachlichen Discovery-Kontext

HTTP Client
→ besitzt technische Fehlerdiagnostik
```

Die Implementierung benötigt deshalb weder ein neues Run-Modell noch eine Änderung des Candidate-Discovery-Contracts.

Besonders wichtig ist die Trennung:

```text
Provider Client
→ beschreibt technischen externen Fehler

Candidate Discovery Adapter
→ baut vollständigen Audit Entry

JSONL Logger
→ persistiert Audit fail-safe
```

Damit bleibt die Verantwortlichkeit klar und die Lösung klein.

**Empfehlung: Umsetzung nach PL-Freigabe in einem Paket.**
