diff -ruN reconcilix_orig/reconcilix_20260829/database/migrations/0005_context_item_context_role.sql reconcilix_work/reconcilix_20260829/database/migrations/0005_context_item_context_role.sql
--- reconcilix_orig/reconcilix_20260829/database/migrations/0005_context_item_context_role.sql	1970-01-01 00:00:00.000000000 +0000
+++ reconcilix_work/reconcilix_20260829/database/migrations/0005_context_item_context_role.sql	2026-08-29 15:26:09.550653871 +0000
@@ -0,0 +1,8 @@
+-- Source Delivery Import v0.1
+-- Add processing role for ContextItem without changing existing ContextItem semantics.
+-- Existing OR / legacy ContextItems remain valid with context_role = NULL.
+-- Target: MySQL 8.x and MariaDB 10.4.x
+
+ALTER TABLE `context_item`
+    ADD COLUMN `context_role` VARCHAR(64) NULL AFTER `context_type_uri`,
+    ADD KEY `idx_context_item_role` (`context_role`);
diff -ruN reconcilix_orig/reconcilix_20260829/docs/transport/SOURCE_DELIVERY_IMPORT_v0.1.md reconcilix_work/reconcilix_20260829/docs/transport/SOURCE_DELIVERY_IMPORT_v0.1.md
--- reconcilix_orig/reconcilix_20260829/docs/transport/SOURCE_DELIVERY_IMPORT_v0.1.md	1970-01-01 00:00:00.000000000 +0000
+++ reconcilix_work/reconcilix_20260829/docs/transport/SOURCE_DELIVERY_IMPORT_v0.1.md	2026-08-29 15:26:58.147264730 +0000
@@ -0,0 +1,158 @@
+# Reconcilix – Source Delivery Import v0.1
+
+**Status:** Umsetzung v0.1  
+**Eingang:** `tools/importSourceDelivery.php` (CLI)  
+**Ziel:** generisches Lieferformat für SourceDelivery, SourceValue und ContextItems; zunächst als Grundlage für Vector Candidate Discovery und vergleichende klassische Discovery-Pfade.
+
+## 1. Leitgedanke
+
+Das Lieferformat ist quellsystemneutral. Quellsystem-spezifische Identifikatoren und Feldangaben werden von Reconcilix nicht fachlich interpretiert, sondern soweit erforderlich als Rückadressierung bzw. Provenienz erhalten.
+
+Ein `records[]`-Eintrag enthält in v0.1 genau **einen** `source_value`.
+
+## 2. Contract v0.1
+
+```json
+{
+  "source_delivery": {
+    "tenant_id": "source-delivery-test_id",
+    "source_system_id": "http://digicult.vocnet.org/terminology/ter03115",
+    "delivery_channel": "BATCH_IMPORT",
+    "external_delivery_id": "dehioSubjectRheinland_001"
+  },
+  "records": [
+    {
+      "recordID": "83414",
+      "workID": "d-RF0f90rT",
+      "source_value": {
+        "recordID": "27562517",
+        "source_field": "entities_x_classification_addedNotes.lexical_value",
+        "value": "Ehem. Pfarrhaus",
+        "preferred_entity_type_uri": "CONCEPT"
+      },
+      "context_sources": [
+        {
+          "recordID": "83413",
+          "context_items": [
+            {
+              "context_type_uri": "https://reconcilix.vocnet.org/context/0001",
+              "context_role": "item_discovery",
+              "value": "<wrap>...</wrap>"
+            },
+            {
+              "context_type_uri": "https://reconcilix.vocnet.org/context/0001",
+              "context_role": "structural_context_discovery",
+              "value": "<div>...</div>"
+            }
+          ]
+        }
+      ]
+    }
+  ]
+}
+```
+
+## 3. Mapping in das Rx-Modell
+
+| Lieferformat | Reconcilix |
+|---|---|
+| `source_delivery.tenant_id` | `SourceDelivery.tenantId` |
+| `source_delivery.source_system_id` | `SourceDelivery.sourceSystemId` |
+| `source_delivery.delivery_channel` | `SourceDelivery.deliveryChannel` |
+| `source_delivery.external_delivery_id` | `SourceDelivery.externalDeliveryId` |
+| `source_value.recordID` | `SourceValue.sourceRecordId` |
+| `source_value.source_field` | `SourceValue.sourceField` |
+| `source_value.value` | `SourceValue.value` |
+| `source_value.preferred_entity_type_uri` | `SourceValue.preferredEntityTypeUri` |
+| `context_items[].context_type_uri` | `ContextItem.contextTypeUri` |
+| `context_items[].context_role` | `ContextItem.contextRole` |
+| `context_items[].value` | `ContextItem.value` |
+
+`source_value.recordID` und `source_value.source_field` sind in v0.1 optional. Fehlen sie, kann Reconcilix den Wert zwar verarbeiten, das Liefersystem erhält jedoch keine eindeutige Rückadressierung über diese beiden Merkmale.
+
+## 4. Context Type und Context Role
+
+`context_type_uri` beschreibt die **fachliche Bedeutung** des Kontextes. `context_role` beschreibt seine **Rolle im Verarbeitungsprozess**.
+
+Für v0.1 sind fachlich vereinbart:
+
+- `item_discovery` – Kontext unmittelbar zum Source-Item.
+- `structural_context_discovery` – zusätzlicher Kontext aus dem strukturellen Zusammenhang des Source-Items; keine bestimmte hierarchische Richtung wird vorausgesetzt.
+
+Die Domain-Eigenschaft `ContextItem.contextRole` ist nullable, damit bestehende OpenRefine-/Legacy-Pfade unverändert funktionieren. Der Source-Delivery-Contract v0.1 verlangt `context_role` für jedes gelieferte `context_item`.
+
+## 5. Provenienz
+
+`records[].recordID`, `records[].workID`, `context_sources[].recordID` und die übrige ursprüngliche Record-Struktur werden in v0.1 nicht zu zusätzlichen Domain-Eigenschaften ausgebaut.
+
+Stattdessen wird der **vollständige empfangene `records[]`-Eintrag** unter
+
+```text
+SourceValue.provenance_json.source_delivery_record
+```
+
+gespeichert. Dadurch bleibt der Lieferkontext verlustfrei verfügbar, ohne quellsystemspezifische Modellannahmen in den Rx-Core zu übernehmen.
+
+## 6. Persistenzänderung
+
+`ContextItem` wird um die optionale Eigenschaft `contextRole` erweitert. Dazu ergänzt Migration `0005_context_item_context_role.sql`:
+
+```sql
+context_role VARCHAR(64) NULL
+```
+
+Bestehende ContextItems bleiben mit `NULL` gültig.
+
+## 7. Importverhalten
+
+Der CLI-Import:
+
+1. liest und validiert das JSON,
+2. prüft bei vorhandener `external_delivery_id`, ob dieselbe Kombination aus Tenant und externer Delivery-ID bereits existiert,
+3. legt eine `SourceDelivery` an,
+4. setzt sie auf `PROCESSING`,
+5. erzeugt je `records[]` genau einen `SourceValue`,
+6. flacht alle `context_sources[].context_items[]` auf die `ContextItem[]` des SourceValue ab,
+7. speichert den vollständigen Record in `provenance_json`,
+8. setzt die Lieferung auf `COMPLETED`,
+9. führt den gesamten Import atomar in einer DB-Transaktion aus.
+
+Bei einem Fehler wird die Transaktion zurückgerollt; es werden keine Teilimporte persistiert.
+
+## 8. CLI
+
+Vor dem ersten Import Migrationen ausführen:
+
+```bash
+php tools/migrate.php
+```
+
+Danach:
+
+```bash
+php tools/importSourceDelivery.php path/to/source-delivery.json
+```
+
+Beispielausgabe:
+
+```text
+Source Delivery Import v0.1: OK
+source_delivery_id: <UUID>
+records: 1
+source_values: 1
+context_items: 2
+```
+
+## 9. Nicht im Scope
+
+Nicht Bestandteil dieses Imports sind:
+
+- Candidate Discovery selbst,
+- Qdrant-/Vector-API-Aufruf,
+- OpenRefine-Ausgabe,
+- LLM-Interpretation,
+- Ableitung von ContextItems aus Roh-/Umgebungstext,
+- fachliche Interpretation quellsystemspezifischer Record-/Work-IDs,
+- automatische MatchDecision.
+
+Der Import schafft ausschließlich eine gemeinsame persistierte Ausgangsbasis für diese nachfolgenden Verarbeitungspfade.
diff -ruN reconcilix_orig/reconcilix_20260829/docs/transport/source_delivery_v0.1_example.json reconcilix_work/reconcilix_20260829/docs/transport/source_delivery_v0.1_example.json
--- reconcilix_orig/reconcilix_20260829/docs/transport/source_delivery_v0.1_example.json	1970-01-01 00:00:00.000000000 +0000
+++ reconcilix_work/reconcilix_20260829/docs/transport/source_delivery_v0.1_example.json	2026-08-29 15:26:58.148103862 +0000
@@ -0,0 +1,37 @@
+{
+  "source_delivery": {
+    "tenant_id": "source-delivery-test_id",
+    "source_system_id": "http://digicult.vocnet.org/terminology/ter03115",
+    "delivery_channel": "BATCH_IMPORT",
+    "external_delivery_id": "dehioSubjectRheinland_001"
+  },
+  "records": [
+    {
+      "recordID": "83414",
+      "workID": "d-RF0f90rT",
+      "source_value": {
+        "recordID": "27562517",
+        "source_field": "entities_x_classification_addedNotes.lexical_value",
+        "value": "Ehem. Pfarrhaus",
+        "preferred_entity_type_uri": "CONCEPT"
+      },
+      "context_sources": [
+        {
+          "recordID": "83413",
+          "context_items": [
+            {
+              "context_type_uri": "https://reconcilix.vocnet.org/context/0001",
+              "context_role": "item_discovery",
+              "value": "<wrap xmlns:dh=\"http://www.digicult-verbund.de/ns/dh\"><objectName type=\"preferred\" xml:id=\"d-RF0f90rT\">Ehem. Pfarrhaus</objectName> (<address> <street>Hochstr. 9</street> </address>): Urspr. an die Vorgängerkirche von 1760/61 angebauter Bruchsteinbau in zwei Geschossen und drei Achsen mit Fenstergewänden aus Blaustein und Krüppelwalmdach, <date>1777</date>.</wrap>"
+            },
+            {
+              "context_type_uri": "https://reconcilix.vocnet.org/context/0001",
+              "context_role": "structural_context_discovery",
+              "value": "<div type=\"objekt\" xml:id=\"d-4SF4wab3\"> <p><objectName type=\"preferred\" xml:id=\"d-RF0f90rT\">Ehem. Pfarrhaus</objectName> (<address> <street>Hochstr. 9</street> </address>): Urspr. an die Vorgängerkirche von 1760/61 angebauter Bruchsteinbau in zwei Geschossen und drei Achsen mit Fenstergewänden aus Blaustein und Krüppelwalmdach, <date>1777</date>.</p> </div> HIER KANN NOCH MEHR FOLGEN...."
+            }
+          ]
+        }
+      ]
+    }
+  ]
+}
diff -ruN reconcilix_orig/reconcilix_20260829/src/Application/Delivery/SourceDeliveryJsonImporter.php reconcilix_work/reconcilix_20260829/src/Application/Delivery/SourceDeliveryJsonImporter.php
--- reconcilix_orig/reconcilix_20260829/src/Application/Delivery/SourceDeliveryJsonImporter.php	1970-01-01 00:00:00.000000000 +0000
+++ reconcilix_work/reconcilix_20260829/src/Application/Delivery/SourceDeliveryJsonImporter.php	2026-08-29 15:26:09.553202567 +0000
@@ -0,0 +1,238 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Application\Delivery;
+
+use App\Application\Persistence\SourceValueRepository;
+use App\Application\Persistence\TransactionManager;
+use App\Domain\Delivery\DeliveryChannel;
+use App\Domain\Delivery\SourceDelivery;
+use App\Domain\Delivery\SourceSystemId;
+use App\Domain\Delivery\TenantId;
+use App\Domain\Reconciliation\ContextItem;
+use App\Domain\Reconciliation\SourceValue;
+use App\Domain\Repository\SourceDeliveryRepository;
+use InvalidArgumentException;
+use Throwable;
+
+final class SourceDeliveryJsonImporter
+{
+    public function __construct(
+        private readonly SourceDeliveryRepository $sourceDeliveries,
+        private readonly SourceValueRepository $sourceValues,
+        private readonly TransactionManager $transactionManager,
+    ) {
+    }
+
+    /**
+     * @param array<string,mixed> $payload
+     * @return array{source_delivery_id:string,records:int,source_values:int,context_items:int}
+     */
+    public function import(array $payload): array
+    {
+        $normalized = $this->validateAndNormalize($payload);
+        $deliveryData = $normalized['source_delivery'];
+        $records = $normalized['records'];
+
+        $tenantId = new TenantId($deliveryData['tenant_id']);
+        $externalDeliveryId = $deliveryData['external_delivery_id'];
+
+        if ($externalDeliveryId !== null
+            && $this->sourceDeliveries->findByExternalDeliveryId($tenantId, $externalDeliveryId) !== null) {
+            throw new InvalidArgumentException(sprintf(
+                'SourceDelivery with tenant_id "%s" and external_delivery_id "%s" already exists.',
+                $tenantId->value(),
+                $externalDeliveryId,
+            ));
+        }
+
+        $delivery = SourceDelivery::create(
+            tenantId: $tenantId,
+            sourceSystemId: new SourceSystemId($deliveryData['source_system_id']),
+            deliveryChannel: DeliveryChannel::fromString($deliveryData['delivery_channel']),
+            externalDeliveryId: $externalDeliveryId,
+        );
+
+        $contextItemCount = 0;
+        $sourceValueCount = 0;
+
+        $this->transactionManager->begin();
+
+        try {
+            $this->sourceDeliveries->save($delivery);
+            $delivery->markProcessing();
+            $this->sourceDeliveries->save($delivery);
+
+            foreach ($records as $record) {
+                $sourceData = $record['source_value'];
+                $sourceValue = SourceValue::create(
+                    sourceDeliveryId: $delivery->id(),
+                    value: $sourceData['value'],
+                    preferredEntityTypeUri: $sourceData['preferred_entity_type_uri'],
+                    sourceField: $sourceData['source_field'],
+                    sourceRecordId: $sourceData['recordID'],
+                    provenance: [
+                        // Deliberately retain the complete source record as received.
+                        // Rx does not interpret source-specific record/work identifiers here.
+                        'source_delivery_record' => $record,
+                    ],
+                );
+
+                foreach ($record['context_sources'] as $contextSource) {
+                    foreach ($contextSource['context_items'] as $contextData) {
+                        $sourceValue->addContextItem(new ContextItem(
+                            contextTypeUri: $contextData['context_type_uri'],
+                            value: $contextData['value'],
+                            contextRole: $contextData['context_role'],
+                        ));
+                        $contextItemCount++;
+                    }
+                }
+
+                $this->sourceValues->save($sourceValue);
+                $sourceValueCount++;
+            }
+
+            $delivery->markCompleted();
+            $this->sourceDeliveries->save($delivery);
+            $this->transactionManager->commit();
+        } catch (Throwable $exception) {
+            if ($this->transactionManager->isActive()) {
+                $this->transactionManager->rollback();
+            }
+            throw $exception;
+        }
+
+        return [
+            'source_delivery_id' => $delivery->id()->value(),
+            'records' => count($records),
+            'source_values' => $sourceValueCount,
+            'context_items' => $contextItemCount,
+        ];
+    }
+
+    /**
+     * @param array<string,mixed> $payload
+     * @return array{
+     *   source_delivery:array{tenant_id:string,source_system_id:string,delivery_channel:string,external_delivery_id:?string},
+     *   records:list<array{
+     *     recordID:?string,
+     *     workID:?string,
+     *     source_value:array{recordID:?string,source_field:?string,value:string,preferred_entity_type_uri:string},
+     *     context_sources:list<array{recordID:?string,context_items:list<array{context_type_uri:string,context_role:string,value:string}>}>
+     *   }>
+     * }
+     */
+    private function validateAndNormalize(array $payload): array
+    {
+        $delivery = $this->requireArray($payload, 'source_delivery', 'root');
+        $recordsRaw = $this->requireList($payload, 'records', 'root');
+        if ($recordsRaw === []) {
+            throw new InvalidArgumentException('records must contain at least one record.');
+        }
+
+        $normalizedRecords = [];
+        foreach ($recordsRaw as $recordIndex => $recordRaw) {
+            if (!is_array($recordRaw)) {
+                throw new InvalidArgumentException(sprintf('records[%d] must be an object.', $recordIndex));
+            }
+
+            $path = sprintf('records[%d]', $recordIndex);
+            $sourceRaw = $this->requireArray($recordRaw, 'source_value', $path);
+            $contextSourcesRaw = $this->requireList($recordRaw, 'context_sources', $path);
+
+            $normalizedContextSources = [];
+            foreach ($contextSourcesRaw as $sourceIndex => $contextSourceRaw) {
+                if (!is_array($contextSourceRaw)) {
+                    throw new InvalidArgumentException(sprintf('%s.context_sources[%d] must be an object.', $path, $sourceIndex));
+                }
+
+                $contextSourcePath = sprintf('%s.context_sources[%d]', $path, $sourceIndex);
+                $contextItemsRaw = $this->requireList($contextSourceRaw, 'context_items', $contextSourcePath);
+                $normalizedContextItems = [];
+
+                foreach ($contextItemsRaw as $contextIndex => $contextRaw) {
+                    if (!is_array($contextRaw)) {
+                        throw new InvalidArgumentException(sprintf('%s.context_items[%d] must be an object.', $contextSourcePath, $contextIndex));
+                    }
+                    $contextPath = sprintf('%s.context_items[%d]', $contextSourcePath, $contextIndex);
+                    $normalizedContextItems[] = [
+                        'context_type_uri' => $this->requiredString($contextRaw, 'context_type_uri', $contextPath),
+                        'context_role' => $this->requiredString($contextRaw, 'context_role', $contextPath),
+                        'value' => $this->requiredString($contextRaw, 'value', $contextPath, trim: false),
+                    ];
+                }
+
+                $normalizedContextSources[] = [
+                    'recordID' => $this->optionalString($contextSourceRaw, 'recordID', $contextSourcePath),
+                    'context_items' => $normalizedContextItems,
+                ];
+            }
+
+            $normalizedRecords[] = [
+                'recordID' => $this->optionalString($recordRaw, 'recordID', $path),
+                'workID' => $this->optionalString($recordRaw, 'workID', $path),
+                'source_value' => [
+                    'recordID' => $this->optionalString($sourceRaw, 'recordID', $path . '.source_value'),
+                    'source_field' => $this->optionalString($sourceRaw, 'source_field', $path . '.source_value'),
+                    'value' => $this->requiredString($sourceRaw, 'value', $path . '.source_value', trim: false),
+                    'preferred_entity_type_uri' => $this->requiredString($sourceRaw, 'preferred_entity_type_uri', $path . '.source_value'),
+                ],
+                'context_sources' => $normalizedContextSources,
+            ];
+        }
+
+        return [
+            'source_delivery' => [
+                'tenant_id' => $this->requiredString($delivery, 'tenant_id', 'source_delivery'),
+                'source_system_id' => $this->requiredString($delivery, 'source_system_id', 'source_delivery'),
+                'delivery_channel' => $this->requiredString($delivery, 'delivery_channel', 'source_delivery'),
+                'external_delivery_id' => $this->optionalString($delivery, 'external_delivery_id', 'source_delivery'),
+            ],
+            'records' => $normalizedRecords,
+        ];
+    }
+
+    /** @param array<string,mixed> $data @return array<string,mixed> */
+    private function requireArray(array $data, string $key, string $path): array
+    {
+        if (!isset($data[$key]) || !is_array($data[$key]) || array_is_list($data[$key])) {
+            throw new InvalidArgumentException(sprintf('%s.%s must be an object.', $path, $key));
+        }
+        return $data[$key];
+    }
+
+    /** @param array<string,mixed> $data @return list<mixed> */
+    private function requireList(array $data, string $key, string $path): array
+    {
+        if (!isset($data[$key]) || !is_array($data[$key]) || !array_is_list($data[$key])) {
+            throw new InvalidArgumentException(sprintf('%s.%s must be an array.', $path, $key));
+        }
+        return $data[$key];
+    }
+
+    /** @param array<string,mixed> $data */
+    private function requiredString(array $data, string $key, string $path, bool $trim = true): string
+    {
+        if (!array_key_exists($key, $data) || !is_string($data[$key])) {
+            throw new InvalidArgumentException(sprintf('%s.%s must be a string.', $path, $key));
+        }
+        $value = $trim ? trim($data[$key]) : $data[$key];
+        if (trim($value) === '') {
+            throw new InvalidArgumentException(sprintf('%s.%s must not be empty.', $path, $key));
+        }
+        return $value;
+    }
+
+    /** @param array<string,mixed> $data */
+    private function optionalString(array $data, string $key, string $path): ?string
+    {
+        if (!array_key_exists($key, $data) || $data[$key] === null) {
+            return null;
+        }
+        if (!is_string($data[$key]) || trim($data[$key]) === '') {
+            throw new InvalidArgumentException(sprintf('%s.%s must be null or a non-empty string.', $path, $key));
+        }
+        return $data[$key];
+    }
+}
diff -ruN reconcilix_orig/reconcilix_20260829/src/Domain/Reconciliation/ContextItem.php reconcilix_work/reconcilix_20260829/src/Domain/Reconciliation/ContextItem.php
--- reconcilix_orig/reconcilix_20260829/src/Domain/Reconciliation/ContextItem.php	2026-07-25 20:51:01.000000000 +0000
+++ reconcilix_work/reconcilix_20260829/src/Domain/Reconciliation/ContextItem.php	2026-08-29 15:26:09.551445055 +0000
@@ -8,6 +8,7 @@
     public function __construct(
         private readonly string $contextTypeUri,
         private readonly string $value,
+        private readonly ?string $contextRole = null,
     ) {
         if ($contextTypeUri === '') {
             throw new DomainInvariantViolation('ContextItem.contextTypeUri must not be empty.');
@@ -16,6 +17,10 @@
         if ($value === '') {
             throw new DomainInvariantViolation('ContextItem.value must not be empty.');
         }
+
+        if ($contextRole !== null && trim($contextRole) === '') {
+            throw new DomainInvariantViolation('ContextItem.contextRole must not be empty when provided.');
+        }
     }
 
     public function contextTypeUri(): string
@@ -27,4 +32,9 @@
     {
         return $this->value;
     }
+
+    public function contextRole(): ?string
+    {
+        return $this->contextRole;
+    }
 }
diff -ruN reconcilix_orig/reconcilix_20260829/src/Infrastructure/Persistence/Mapper/ContextItemMapper.php reconcilix_work/reconcilix_20260829/src/Infrastructure/Persistence/Mapper/ContextItemMapper.php
--- reconcilix_orig/reconcilix_20260829/src/Infrastructure/Persistence/Mapper/ContextItemMapper.php	2026-07-26 05:34:01.000000000 +0000
+++ reconcilix_work/reconcilix_20260829/src/Infrastructure/Persistence/Mapper/ContextItemMapper.php	2026-08-29 15:27:26.810390925 +0000
@@ -16,12 +16,13 @@
     public function insert(ContextItem $contextItem, int $sourceValueId): int
     {
         $statement = $this->connection->prepare(
-            'INSERT INTO context_item (source_value_id, context_type_uri, value) '
-            . 'VALUES (:source_value_id, :context_type_uri, :value)'
+            'INSERT INTO context_item (source_value_id, context_type_uri, context_role, value) '
+            . 'VALUES (:source_value_id, :context_type_uri, :context_role, :value)'
         );
         $statement->execute([
             'source_value_id' => $sourceValueId,
             'context_type_uri' => $contextItem->contextTypeUri(),
+            'context_role' => $contextItem->contextRole(),
             'value' => $contextItem->value(),
         ]);
 
@@ -32,7 +33,7 @@
     public function findBySourceValueId(int $sourceValueId): array
     {
         $statement = $this->connection->prepare(
-            'SELECT context_type_uri, value '
+            'SELECT context_type_uri, context_role, value '
             . 'FROM context_item '
             . 'WHERE source_value_id = :source_value_id '
             . 'ORDER BY id ASC'
@@ -47,6 +48,7 @@
             $contextItems[] = new ContextItem(
                 contextTypeUri: (string) $row['context_type_uri'],
                 value: (string) $row['value'],
+                contextRole: ($row['context_role'] ?? null) !== null ? (string) $row['context_role'] : null,
             );
         }
 
diff -ruN reconcilix_orig/reconcilix_20260829/tests/Application/Delivery/SourceDeliveryJsonImporterTest.php reconcilix_work/reconcilix_20260829/tests/Application/Delivery/SourceDeliveryJsonImporterTest.php
--- reconcilix_orig/reconcilix_20260829/tests/Application/Delivery/SourceDeliveryJsonImporterTest.php	1970-01-01 00:00:00.000000000 +0000
+++ reconcilix_work/reconcilix_20260829/tests/Application/Delivery/SourceDeliveryJsonImporterTest.php	2026-08-29 15:26:57.513484420 +0000
@@ -0,0 +1,156 @@
+<?php
+declare(strict_types=1);
+
+require_once dirname(__DIR__, 3) . '/src/bootstrap.php';
+
+use App\Application\Delivery\SourceDeliveryJsonImporter;
+use App\Application\Persistence\SourceValueRepository;
+use App\Application\Persistence\TransactionManager;
+use App\Domain\Delivery\SourceDelivery;
+use App\Domain\Delivery\SourceDeliveryId;
+use App\Domain\Delivery\TenantId;
+use App\Domain\Reconciliation\SourceValue;
+use App\Domain\Repository\SourceDeliveryRepository;
+
+function assertImportSame(mixed $expected, mixed $actual, string $message): void
+{
+    if ($expected !== $actual) {
+        throw new RuntimeException($message . sprintf(
+            ' Expected %s, got %s.',
+            var_export($expected, true),
+            var_export($actual, true),
+        ));
+    }
+}
+
+final class ImportSourceDeliveryRepository implements SourceDeliveryRepository
+{
+    /** @var list<SourceDelivery> */
+    public array $saved = [];
+
+    public function save(SourceDelivery $delivery): void
+    {
+        $this->saved[] = clone $delivery;
+    }
+
+    public function findById(SourceDeliveryId $id): ?SourceDelivery
+    {
+        return null;
+    }
+
+    public function findByExternalDeliveryId(TenantId $tenantId, string $externalDeliveryId): ?SourceDelivery
+    {
+        return null;
+    }
+}
+
+final class ImportSourceValueRepository implements SourceValueRepository
+{
+    /** @var list<SourceValue> */
+    public array $saved = [];
+
+    public function save(SourceValue $sourceValue): void
+    {
+        $sourceValue->assignId(count($this->saved) + 1);
+        $this->saved[] = $sourceValue;
+    }
+
+    public function findById(int $id): ?SourceValue
+    {
+        return null;
+    }
+}
+
+final class ImportTransactionManager implements TransactionManager
+{
+    public int $begins = 0;
+    public int $commits = 0;
+    public int $rollbacks = 0;
+    private bool $active = false;
+
+    public function begin(): void { $this->begins++; $this->active = true; }
+    public function commit(): void { $this->commits++; $this->active = false; }
+    public function rollback(): void { $this->rollbacks++; $this->active = false; }
+    public function isActive(): bool { return $this->active; }
+}
+
+$payload = [
+    'source_delivery' => [
+        'tenant_id' => 'source-delivery-test_id',
+        'source_system_id' => 'http://digicult.vocnet.org/terminology/ter03115',
+        'delivery_channel' => 'BATCH_IMPORT',
+        'external_delivery_id' => 'dehioSubjectRheinland_001',
+    ],
+    'records' => [[
+        'recordID' => '83414',
+        'workID' => 'd-RF0f90rT',
+        'source_value' => [
+            'recordID' => '27562517',
+            'source_field' => 'entities_x_classification_addedNotes.lexical_value',
+            'value' => 'Ehem. Pfarrhaus',
+            'preferred_entity_type_uri' => 'CONCEPT',
+        ],
+        'context_sources' => [[
+            'recordID' => '83413',
+            'context_items' => [
+                [
+                    'context_type_uri' => 'https://reconcilix.vocnet.org/context/0001',
+                    'context_role' => 'item_discovery',
+                    'value' => '<wrap>Ehem. Pfarrhaus</wrap>',
+                ],
+                [
+                    'context_type_uri' => 'https://reconcilix.vocnet.org/context/0001',
+                    'context_role' => 'structural_context_discovery',
+                    'value' => '<div>Ehem. Pfarrhaus und struktureller Kontext</div>',
+                ],
+            ],
+        ]],
+    ]],
+];
+
+$deliveryRepository = new ImportSourceDeliveryRepository();
+$sourceRepository = new ImportSourceValueRepository();
+$transaction = new ImportTransactionManager();
+$importer = new SourceDeliveryJsonImporter($deliveryRepository, $sourceRepository, $transaction);
+$result = $importer->import($payload);
+
+assertImportSame(1, $result['records'], 'One contract record must be imported.');
+assertImportSame(1, $result['source_values'], 'One SourceValue per record must be imported.');
+assertImportSame(2, $result['context_items'], 'All nested ContextItems must be flattened to the SourceValue.');
+assertImportSame(1, $transaction->begins, 'Import must start one transaction.');
+assertImportSame(1, $transaction->commits, 'Import must commit one transaction.');
+assertImportSame(0, $transaction->rollbacks, 'Successful import must not roll back.');
+
+$source = $sourceRepository->saved[0];
+assertImportSame('Ehem. Pfarrhaus', $source->value(), 'SourceValue.value must be mapped.');
+assertImportSame('CONCEPT', $source->preferredEntityTypeUri(), 'preferred_entity_type_uri must be mapped.');
+assertImportSame('27562517', $source->sourceRecordId(), 'source_value.recordID must map to sourceRecordId.');
+assertImportSame('entities_x_classification_addedNotes.lexical_value', $source->sourceField(), 'source_field must be mapped.');
+assertImportSame(2, count($source->contextItems()), 'SourceValue must contain both context roles.');
+assertImportSame('item_discovery', $source->contextItems()[0]->contextRole(), 'item_discovery role must survive import mapping.');
+assertImportSame('structural_context_discovery', $source->contextItems()[1]->contextRole(), 'structural role must survive import mapping.');
+assertImportSame('83414', $source->provenance()['source_delivery_record']['recordID'] ?? null, 'Original recordID must be retained in provenance.');
+assertImportSame('83413', $source->provenance()['source_delivery_record']['context_sources'][0]['recordID'] ?? null, 'Context source provenance must be retained.');
+
+// source_value.recordID and source_field are deliberately optional in v0.1.
+$payloadWithoutReturnAddress = $payload;
+unset($payloadWithoutReturnAddress['source_delivery']['external_delivery_id']);
+unset($payloadWithoutReturnAddress['records'][0]['source_value']['recordID']);
+unset($payloadWithoutReturnAddress['records'][0]['source_value']['source_field']);
+$sourceRepository2 = new ImportSourceValueRepository();
+$importer2 = new SourceDeliveryJsonImporter(new ImportSourceDeliveryRepository(), $sourceRepository2, new ImportTransactionManager());
+$importer2->import($payloadWithoutReturnAddress);
+assertImportSame(null, $sourceRepository2->saved[0]->sourceRecordId(), 'source_value.recordID must be optional.');
+assertImportSame(null, $sourceRepository2->saved[0]->sourceField(), 'source_field must be optional.');
+
+// context_role is mandatory in the v0.1 source-delivery contract even though the Domain property is nullable for legacy paths.
+$invalid = $payload;
+unset($invalid['records'][0]['context_sources'][0]['context_items'][0]['context_role']);
+try {
+    (new SourceDeliveryJsonImporter(new ImportSourceDeliveryRepository(), new ImportSourceValueRepository(), new ImportTransactionManager()))->import($invalid);
+    throw new RuntimeException('Missing context_role must fail contract validation.');
+} catch (InvalidArgumentException $exception) {
+    assertImportSame(true, str_contains($exception->getMessage(), 'context_role'), 'Validation error must identify context_role.');
+}
+
+echo "Source Delivery JSON Import v0.1: OK\n";
diff -ruN reconcilix_orig/reconcilix_20260829/tests/Domain/Reconciliation/ContextItemTest.php reconcilix_work/reconcilix_20260829/tests/Domain/Reconciliation/ContextItemTest.php
--- reconcilix_orig/reconcilix_20260829/tests/Domain/Reconciliation/ContextItemTest.php	2026-08-15 16:25:27.000000000 +0000
+++ reconcilix_work/reconcilix_20260829/tests/Domain/Reconciliation/ContextItemTest.php	2026-08-29 15:26:57.994116889 +0000
@@ -38,6 +38,7 @@
 $textContext = new ContextItem(
     contextTypeUri: TEXT_CONTEXT_URI,
     value: '<tei><title>Skulptur</title></tei>',
+    contextRole: 'item_discovery',
 );
 
 assertSameContextValue(
@@ -46,6 +47,11 @@
     'ContextItem must preserve the context type URI.'
 );
 assertSameContextValue(
+    'item_discovery',
+    $textContext->contextRole(),
+    'ContextItem must preserve the optional context role.'
+);
+assertSameContextValue(
     '<tei><title>Skulptur</title></tei>',
     $textContext->value(),
     'ContextItem must preserve the context value.'
@@ -72,4 +78,9 @@
     'An empty ContextItem value must be rejected.'
 );
 
+assertContextInvariantViolation(
+    static fn () => new ContextItem(TEXT_CONTEXT_URI, 'Context value', '   '),
+    'An empty contextRole must be rejected when provided.'
+);
+
 echo "ED-01-001 ContextItem Domain Model: OK\n";
diff -ruN reconcilix_orig/reconcilix_20260829/tests/Persistence/ContextPersistenceTest.php reconcilix_work/reconcilix_20260829/tests/Persistence/ContextPersistenceTest.php
--- reconcilix_orig/reconcilix_20260829/tests/Persistence/ContextPersistenceTest.php	2026-08-01 09:59:52.000000000 +0000
+++ reconcilix_work/reconcilix_20260829/tests/Persistence/ContextPersistenceTest.php	2026-08-29 15:26:57.994390180 +0000
@@ -112,6 +112,7 @@
             return array_map(
                 static fn(array $row): array => [
                     'context_type_uri' => $row['context_type_uri'],
+                    'context_role' => $row['context_role'] ?? null,
                     'value' => $row['value'],
                 ],
                 $rows,
@@ -165,6 +166,7 @@
 $source->addContextItem(new ContextItem(
     contextTypeUri: 'https://reconcilix.vocnet.org/context/0001',
     value: 'Kirche St. Nikolai',
+    contextRole: 'item_discovery',
 ));
 $repository->save($source);
 $loaded = $repository->findById((int) $source->id());
@@ -176,6 +178,7 @@
     'T002: contextTypeUri must survive persistence and rehydration.',
 );
 assertContextSame('Kirche St. Nikolai', $loaded?->contextItems()[0]->value(), 'T002: value must survive persistence and rehydration.');
+assertContextSame('item_discovery', $loaded?->contextItems()[0]->contextRole(), 'T002: contextRole must survive persistence and rehydration.');
 
 // T003 and T006 – Multiple ContextItems and complete deterministic rehydration.
 $pdo = new ContextPersistencePdo();
@@ -245,6 +248,7 @@
     'id' => 2,
     'source_value_id' => 1,
     'context_type_uri' => '',
+    'context_role' => null,
     'value' => 'Ungültiger Context',
 ];
 $repository = createRepository($pdo);
diff -ruN reconcilix_orig/reconcilix_20260829/tests/tests.md reconcilix_work/reconcilix_20260829/tests/tests.md
--- reconcilix_orig/reconcilix_20260829/tests/tests.md	2026-08-01 08:52:02.000000000 +0000
+++ reconcilix_work/reconcilix_20260829/tests/tests.md	2026-08-29 15:26:58.148103862 +0000
@@ -66,3 +66,5 @@
 
 
 
+
+- php tests/Application/Delivery/SourceDeliveryJsonImporterTest.php
diff -ruN reconcilix_orig/reconcilix_20260829/tools/importSourceDelivery.php reconcilix_work/reconcilix_20260829/tools/importSourceDelivery.php
--- reconcilix_orig/reconcilix_20260829/tools/importSourceDelivery.php	1970-01-01 00:00:00.000000000 +0000
+++ reconcilix_work/reconcilix_20260829/tools/importSourceDelivery.php	2026-08-29 15:26:09.553998995 +0000
@@ -0,0 +1,63 @@
+<?php
+declare(strict_types=1);
+
+require_once dirname(__DIR__) . '/src/bootstrap.php';
+
+use App\Application\Delivery\SourceDeliveryJsonImporter;
+use App\Infrastructure\Composition\ReconciliationCompositionRoot;
+use App\Infrastructure\Configuration\Configuration;
+
+if (PHP_SAPI !== 'cli') {
+    fwrite(STDERR, "Source Delivery Import läuft nur über CLI.\n");
+    exit(2);
+}
+
+if ($argc !== 2 || in_array($argv[1], ['-h', '--help'], true)) {
+    fwrite(STDOUT, "Usage: php tools/importSourceDelivery.php <source-delivery.json>\n");
+    exit($argc === 2 ? 0 : 2);
+}
+
+try {
+    $root = dirname(__DIR__);
+    $path = $argv[1];
+    if (!is_file($path) || !is_readable($path)) {
+        throw new RuntimeException('Input file is missing or unreadable: ' . $path);
+    }
+
+    $json = file_get_contents($path);
+    if ($json === false) {
+        throw new RuntimeException('Unable to read input file: ' . $path);
+    }
+
+    $payload = json_decode($json, true, flags: JSON_THROW_ON_ERROR);
+    if (!is_array($payload) || array_is_list($payload)) {
+        throw new RuntimeException('Source Delivery JSON root must be an object.');
+    }
+
+    $configuration = Configuration::load($root . '/config');
+    if (!$configuration->databaseEnabled()) {
+        throw new RuntimeException('Database access is disabled by configuration.');
+    }
+
+    $compositionRoot = new ReconciliationCompositionRoot($configuration);
+    $importer = new SourceDeliveryJsonImporter(
+        sourceDeliveries: $compositionRoot->createSourceDeliveryRepository(),
+        sourceValues: $compositionRoot->createSourceValueRepository(),
+        transactionManager: $compositionRoot->createTransactionManager(),
+    );
+
+    $result = $importer->import($payload);
+
+    fwrite(STDOUT, "Source Delivery Import v0.1: OK\n");
+    fwrite(STDOUT, 'source_delivery_id: ' . $result['source_delivery_id'] . "\n");
+    fwrite(STDOUT, 'records: ' . $result['records'] . "\n");
+    fwrite(STDOUT, 'source_values: ' . $result['source_values'] . "\n");
+    fwrite(STDOUT, 'context_items: ' . $result['context_items'] . "\n");
+    exit(0);
+} catch (JsonException $exception) {
+    fwrite(STDERR, 'Invalid JSON: ' . $exception->getMessage() . "\n");
+    exit(1);
+} catch (Throwable $exception) {
+    fwrite(STDERR, $exception->getMessage() . "\n");
+    exit(1);
+}
