diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/database/migrations/0006_reconciliation_run_foundation.sql qdr001a_work/base/reconcilix_20260829/database/migrations/0006_reconciliation_run_foundation.sql
--- qdr001a_pre/reconcilix_20260829/database/migrations/0006_reconciliation_run_foundation.sql	1970-01-01 00:00:00.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/database/migrations/0006_reconciliation_run_foundation.sql	2026-08-29 20:16:06.847276803 +0000
@@ -0,0 +1,82 @@
+-- QDR-001A Reconciliation Run Foundation
+-- Target: MySQL 8.x and MariaDB 10.4.x
+-- Forward-only migration.
+
+CREATE TABLE `reconciliation_run` (
+    `id` VARCHAR(36) NOT NULL,
+    `source_delivery_id` VARCHAR(36) NOT NULL,
+    `target_vocabulary_uri` VARCHAR(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
+    `scope_vocabulary_uri` VARCHAR(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL,
+    `started_at` DATETIME(6) NULL,
+    `finished_at` DATETIME(6) NULL,
+    `status` VARCHAR(32) NOT NULL,
+    `configuration_json` JSON NOT NULL,
+
+    PRIMARY KEY (`id`),
+    KEY `idx_reconciliation_run_source_delivery` (`source_delivery_id`),
+    KEY `idx_reconciliation_run_target_vocabulary` (`target_vocabulary_uri`(191)),
+    KEY `idx_reconciliation_run_status` (`status`),
+
+    CONSTRAINT `fk_reconciliation_run_source_delivery`
+        FOREIGN KEY (`source_delivery_id`)
+        REFERENCES `source_delivery` (`id`)
+        ON UPDATE RESTRICT
+        ON DELETE CASCADE,
+
+    CONSTRAINT `chk_reconciliation_run_target_vocabulary`
+        CHECK (CHAR_LENGTH(TRIM(`target_vocabulary_uri`)) > 0),
+    CONSTRAINT `chk_reconciliation_run_scope_vocabulary`
+        CHECK (`scope_vocabulary_uri` IS NULL OR CHAR_LENGTH(TRIM(`scope_vocabulary_uri`)) > 0),
+    CONSTRAINT `chk_reconciliation_run_status`
+        CHECK (`status` IN ('CREATED', 'RUNNING', 'COMPLETED', 'FAILED')),
+    CONSTRAINT `chk_reconciliation_run_timestamps`
+        CHECK (
+            (`status` = 'CREATED' AND `started_at` IS NULL AND `finished_at` IS NULL)
+            OR
+            (`status` = 'RUNNING' AND `started_at` IS NOT NULL AND `finished_at` IS NULL)
+            OR
+            (`status` IN ('COMPLETED', 'FAILED') AND `started_at` IS NOT NULL AND `finished_at` IS NOT NULL)
+        )
+) ENGINE=InnoDB;
+
+CREATE TABLE `reconciliation_run_item` (
+    `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+    `reconciliation_run_id` VARCHAR(36) NOT NULL,
+    `source_value_id` BIGINT UNSIGNED NOT NULL,
+    `status` VARCHAR(32) NOT NULL,
+
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uq_reconciliation_run_item_population` (`reconciliation_run_id`, `source_value_id`),
+    KEY `idx_reconciliation_run_item_source_value` (`source_value_id`),
+    KEY `idx_reconciliation_run_item_status` (`reconciliation_run_id`, `status`),
+
+    CONSTRAINT `fk_reconciliation_run_item_run`
+        FOREIGN KEY (`reconciliation_run_id`)
+        REFERENCES `reconciliation_run` (`id`)
+        ON UPDATE RESTRICT
+        ON DELETE CASCADE,
+    CONSTRAINT `fk_reconciliation_run_item_source_value`
+        FOREIGN KEY (`source_value_id`)
+        REFERENCES `source_value` (`id`)
+        ON UPDATE RESTRICT
+        ON DELETE CASCADE,
+    CONSTRAINT `chk_reconciliation_run_item_status`
+        CHECK (`status` IN ('PENDING', 'COMPLETED', 'FAILED'))
+) ENGINE=InnoDB;
+
+ALTER TABLE `interpretation_graph`
+    ADD COLUMN `reconciliation_run_id` VARCHAR(36) NULL AFTER `source_value_id`,
+    ADD KEY `idx_interpretation_graph_reconciliation_run` (`reconciliation_run_id`),
+    ADD CONSTRAINT `fk_interpretation_graph_reconciliation_run`
+        FOREIGN KEY (`reconciliation_run_id`)
+        REFERENCES `reconciliation_run` (`id`)
+        ON UPDATE RESTRICT
+        ON DELETE RESTRICT;
+
+-- The old key prevented the same SourceValue/span/version from being processed
+-- in more than one run. The run-aware key preserves span/version uniqueness per run.
+-- Because reconciliation_run_id is nullable, legacy graphs remain migration-compatible.
+ALTER TABLE `interpretation_graph`
+    DROP INDEX `uq_interpretation_graph_span_version`,
+    ADD UNIQUE KEY `uq_interpretation_graph_run_span_version`
+        (`reconciliation_run_id`, `source_value_id`, `span_start`, `span_end`, `version`);
diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/docs/qdrant/QDR-001A_Reconciliation_Run_Foundation.md qdr001a_work/base/reconcilix_20260829/docs/qdrant/QDR-001A_Reconciliation_Run_Foundation.md
--- qdr001a_pre/reconcilix_20260829/docs/qdrant/QDR-001A_Reconciliation_Run_Foundation.md	1970-01-01 00:00:00.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/docs/qdrant/QDR-001A_Reconciliation_Run_Foundation.md	2026-08-29 20:17:13.870834346 +0000
@@ -0,0 +1,323 @@
+# QDR-001A – Reconciliation Run Foundation
+
+**Status:** IMPLEMENTED – SERVER TEST PENDING  
+**Datum:** 2026-08-29  
+**Basis:** LA Architecture Review – Freigabe mit Anpassungen  
+**Scope:** Persistente Run-Grundlage für vergleichbare Reconciliation-Verarbeitungen
+
+## 1. Ziel
+
+QDR-001A führt zwischen `SourceDelivery` und der tatsächlich entstandenen Interpretation eine explizite Run-Ebene ein.
+
+```text
+SourceDelivery
+  └── ReconciliationRun
+        ├── ReconciliationRunItem ── SourceValue
+        └── InterpretationGraph ───── SourceValue
+              └── InterpretationNode
+                    └── ReconciliationResult
+                          └── CandidateItem
+```
+
+Die drei Ebenen bleiben fachlich getrennt:
+
+```text
+ReconciliationRun
+→ Bedingungen und Konfigurations-Snapshot einer konkreten Verarbeitung
+
+ReconciliationRunItem
+→ persistierte Soll-Population des Runs
+
+InterpretationGraph + Resultate
+→ tatsächlich entstandene Verarbeitung
+```
+
+Damit kann ein Run über 100 SourceValues auch dann korrekt als 100er-Population ausgewertet werden, wenn einzelne Items fehlschlagen und für diese kein `InterpretationGraph` entsteht.
+
+## 2. Domain Objects
+
+Neu:
+
+```text
+ReconciliationRun
+ReconciliationRunId
+ReconciliationRunStatus
+ReconciliationRunItem
+ReconciliationRunItemStatus
+```
+
+Run-Status:
+
+```text
+CREATED
+RUNNING
+COMPLETED
+FAILED
+```
+
+RunItem-Status:
+
+```text
+PENDING
+COMPLETED
+FAILED
+```
+
+Einzelne `FAILED` RunItems verhindern einen `COMPLETED` Run nicht. `COMPLETED` bedeutet, dass alle RunItems abgearbeitet wurden; jedes Item muss dann terminal (`COMPLETED` oder `FAILED`) sein.
+
+## 3. Persistenz
+
+Migration:
+
+```text
+database/migrations/0006_reconciliation_run_foundation.sql
+```
+
+Neu:
+
+```text
+reconciliation_run
+reconciliation_run_item
+```
+
+Erweiterung:
+
+```text
+interpretation_graph.reconciliation_run_id NULL
+```
+
+Die Nullable-Eigenschaft ist ausschließlich migrationsbedingt für Altbestände. Neue Run-basierte Verarbeitung persistiert Graphen mit `reconciliation_run_id`.
+
+### 3.1 reconciliation_run
+
+Stabile Vergleichsdimensionen als eigene Felder:
+
+```text
+source_delivery_id
+target_vocabulary_uri
+scope_vocabulary_uri
+```
+
+Variable Parameter liegen als unveränderlicher Snapshot in:
+
+```text
+configuration_json
+```
+
+Beispiel:
+
+```json
+{
+  "discovery_method": "VECTOR_SIMILARITY",
+  "adapter_id": "vector-qdrant",
+  "client_id": "qdrant-marburg",
+  "context_type_uris": [
+    "https://reconcilix.vocnet.org/context/0001"
+  ],
+  "context_roles": [
+    "item_discovery",
+    "structural_context_discovery"
+  ],
+  "language": "*",
+  "offset": 0,
+  "limit": 100,
+  "rerank": false,
+  "output_language": "de"
+}
+```
+
+`ReconciliationRunMapper::update()` verändert bewusst nur `status`, `started_at` und `finished_at`. SourceDelivery, Target/Scope und `configuration_json` werden nach dem Insert nicht überschrieben.
+
+### 3.2 reconciliation_run_item
+
+Ein RunItem enthält:
+
+```text
+id
+reconciliation_run_id
+source_value_id
+status
+```
+
+DB-Invariante:
+
+```text
+UNIQUE(reconciliation_run_id, source_value_id)
+```
+
+Damit ist die Run-Population dauerhaft eindeutig.
+
+## 4. InterpretationGraph und Mehrfach-Runs
+
+Vor QDR-001A bestand in der Anwendung noch die WP1-Einschränkung "exactly one InterpretationGraph per SourceValue". Diese Einschränkung musste für Run-Vergleiche aufgehoben werden.
+
+Ein SourceValue darf nun Graphen aus unterschiedlichen Runs besitzen:
+
+```text
+SourceValue X
+  ├── Graph / Run A
+  ├── Graph / Run B
+  └── Graph / Run C
+```
+
+### 4.1 Entscheidung zur von LA offenen Graph-Eindeutigkeit
+
+Es wird **nicht**
+
+```text
+UNIQUE(reconciliation_run_id, source_value_id)
+```
+
+auf `interpretation_graph` eingeführt.
+
+Grund: Das bestehende Domain Model beschreibt einen `InterpretationGraph` als interpretierbaren Span und erlaubt grundsätzlich mehrere Graphen/Spans pro SourceValue. Eine harte 1:1-Regel pro Run würde diese bestehende fachliche Möglichkeit unnötig abschneiden.
+
+Stattdessen ersetzt QDR-001A den bisherigen Schlüssel
+
+```text
+(source_value_id, span_start, span_end, version)
+```
+
+durch den run-sensitiven Schlüssel:
+
+```text
+(reconciliation_run_id, source_value_id, span_start, span_end, version)
+```
+
+Damit sind identische Whole-Value-Graphen für verschiedene Runs möglich, ohne die Span-Semantik des Interpretation-Modells zu verlieren.
+
+Da `reconciliation_run_id` für Legacy-Daten nullable ist, bleiben bestehende Graphen migrationskompatibel.
+
+## 5. Repositories und Application Service
+
+Neu:
+
+```text
+ReconciliationRunRepository
+RelationalReconciliationRunRepository
+ReconciliationRunMapper
+ReconciliationRunItemMapper
+```
+
+Zusätzlich wurde ein append-only Pfad für Graphen bereits bestehender SourceValues geschaffen:
+
+```text
+InterpretationGraphRepository
+RelationalInterpretationGraphRepository
+```
+
+Dies wird in QDR-001B benötigt, weil Qdrant auf bereits importierten SourceValues arbeitet und dafür keine neuen SourceValue-Datensätze erzeugt werden dürfen.
+
+`ReconciliationRunService` steuert die minimale v0.1-Lifecycle-Logik:
+
+```text
+createRun()
+definePopulation()
+start()
+markItemCompleted()
+markItemFailed()
+complete()
+fail()
+```
+
+Bei `definePopulation()` wird geprüft:
+
+- SourceValue existiert,
+- SourceValue gehört zur SourceDelivery des Runs,
+- SourceValue kommt im Run nur einmal vor.
+
+`start()` verlangt mindestens ein RunItem. `complete()` verlangt, dass kein RunItem mehr `PENDING` ist.
+
+## 6. Composition Root
+
+`ReconciliationCompositionRoot` stellt zusätzlich bereit:
+
+```text
+createReconciliationRunRepository()
+createInterpretationGraphRepository()
+createReconciliationRunService()
+```
+
+Damit kann QDR-001B auf der bestehenden Composition-/Persistence-Infrastruktur aufsetzen.
+
+## 7. Nicht Bestandteil von QDR-001A
+
+Weiterhin ausdrücklich nicht umgesetzt:
+
+- Qdrant HTTP Client
+- Qdrant Credentials
+- Vector Request-/Response-Mapping
+- `VECTOR_SIMILARITY` Adapter
+- Discovery Testbench GUI
+- Candidate Rendering
+- LLM Candidate Assessment
+- Prompt-/Modellverwaltung
+- Evaluation-Metriken
+
+Diese Punkte gehören zu QDR-001B bzw. QDR-001C.
+
+## 8. Tests
+
+Neu:
+
+```text
+tests/Domain/Reconciliation/ReconciliationRunTest.php
+tests/Domain/Reconciliation/ReconciliationRunItemTest.php
+tests/Domain/Reconciliation/InterpretationGraphRunTest.php
+tests/Application/Reconciliation/ReconciliationRunServiceTest.php
+tests/Migration/ReconciliationRunMigrationTest.php
+```
+
+Zusätzlich wurde `tests/Migration/SchemaAndMigrationsTest.php` auf die Migrationen 0005 und 0006 aktualisiert.
+
+Lokaler ED-Teststand:
+
+```text
+QDR-001A ReconciliationRun Domain: OK
+QDR-001A ReconciliationRunItem Domain: OK
+QDR-001A InterpretationGraph Run Association: OK
+QDR-001A ReconciliationRun Service: OK
+QDR-001A Migration Contract: OK
+WP2-004 SchemaAndMigrations: OK
+PHP syntax check src/ + tests/: OK
+```
+
+Der vollständige Suite-Lauf enthält in der ED-Umgebung weiterhin bekannte umgebungsabhängige Fehler (fehlende lokale `config/database.php`, fehlende `mbstring`-Extension sowie Tests mit erforderlichem externem Input). Diese Fehler sind nicht durch QDR-001A verursacht.
+
+## 9. Server-Test
+
+Nach Einspielen der Änderungen:
+
+```bash
+php tools/migrate.php
+```
+
+Danach mindestens:
+
+```bash
+php tests/Domain/Reconciliation/ReconciliationRunTest.php
+php tests/Domain/Reconciliation/ReconciliationRunItemTest.php
+php tests/Domain/Reconciliation/InterpretationGraphRunTest.php
+php tests/Application/Reconciliation/ReconciliationRunServiceTest.php
+php tests/Migration/ReconciliationRunMigrationTest.php
+php tests/Migration/SchemaAndMigrationsTest.php
+```
+
+Optional DB-Kontrolle:
+
+```sql
+SHOW CREATE TABLE reconciliation_run;
+SHOW CREATE TABLE reconciliation_run_item;
+SHOW CREATE TABLE interpretation_graph;
+```
+
+## 10. Exit Criteria
+
+QDR-001A ist abgeschlossen, wenn:
+
+1. Migration 0006 auf der Rx-DB erfolgreich ausgeführt wurde.
+2. Domain-/Migrationstests auf dem Zielsystem erfolgreich sind.
+3. `reconciliation_run` und `reconciliation_run_item` vorhanden sind.
+4. `interpretation_graph.reconciliation_run_id` vorhanden und nullable ist.
+5. Existing OpenRefine-/Legacy-Verarbeitung weiterhin Graphen ohne Run-Zuordnung persistieren kann.
+6. QDR-001B einen neuen Run samt Population anlegen und anschließend einen run-gebundenen Graph für einen bereits persistierten SourceValue speichern kann.
diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/src/Application/Persistence/InterpretationGraphRepository.php qdr001a_work/base/reconcilix_20260829/src/Application/Persistence/InterpretationGraphRepository.php
--- qdr001a_pre/reconcilix_20260829/src/Application/Persistence/InterpretationGraphRepository.php	1970-01-01 00:00:00.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/src/Application/Persistence/InterpretationGraphRepository.php	2026-08-29 20:14:43.050891718 +0000
@@ -0,0 +1,11 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Application\Persistence;
+
+use App\Domain\Reconciliation\InterpretationGraph;
+
+interface InterpretationGraphRepository
+{
+    public function saveForSourceValue(InterpretationGraph $graph, int $sourceValueId): void;
+}
diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/src/Application/Reconciliation/ReconciliationRunService.php qdr001a_work/base/reconcilix_20260829/src/Application/Reconciliation/ReconciliationRunService.php
--- qdr001a_pre/reconcilix_20260829/src/Application/Reconciliation/ReconciliationRunService.php	1970-01-01 00:00:00.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/src/Application/Reconciliation/ReconciliationRunService.php	2026-08-29 20:15:10.150986157 +0000
@@ -0,0 +1,200 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Application\Reconciliation;
+
+use App\Application\Persistence\PersistenceException;
+use App\Application\Persistence\SourceValueRepository;
+use App\Application\Persistence\TransactionManager;
+use App\Domain\Delivery\SourceDeliveryId;
+use App\Domain\Reconciliation\DomainInvariantViolation;
+use App\Domain\Reconciliation\ReconciliationRun;
+use App\Domain\Reconciliation\ReconciliationRunItem;
+use App\Domain\Reconciliation\ReconciliationRunItemStatus;
+use App\Domain\Reconciliation\ReconciliationRunStatus;
+use App\Domain\Repository\ReconciliationRunRepository;
+use App\Domain\Repository\SourceDeliveryRepository;
+use Throwable;
+
+final class ReconciliationRunService
+{
+    public function __construct(
+        private readonly ReconciliationRunRepository $runs,
+        private readonly SourceDeliveryRepository $deliveries,
+        private readonly SourceValueRepository $sourceValues,
+        private readonly ?TransactionManager $transactionManager = null,
+    ) {
+    }
+
+    /**
+     * @param array<string, mixed> $configuration
+     */
+    public function createRun(
+        SourceDeliveryId $sourceDeliveryId,
+        string $targetVocabularyUri,
+        ?string $scopeVocabularyUri = null,
+        array $configuration = [],
+    ): ReconciliationRun {
+        if ($this->deliveries->findById($sourceDeliveryId) === null) {
+            throw new DomainInvariantViolation('ReconciliationRun requires an existing SourceDelivery.');
+        }
+
+        $run = ReconciliationRun::create(
+            sourceDeliveryId: $sourceDeliveryId,
+            targetVocabularyUri: $targetVocabularyUri,
+            scopeVocabularyUri: $scopeVocabularyUri,
+            configuration: $configuration,
+        );
+        $this->runs->save($run);
+
+        return $run;
+    }
+
+    /**
+     * Defines the persistent run population. SourceValues must belong to the
+     * same SourceDelivery as the run and may occur only once per run.
+     *
+     * @param list<int> $sourceValueIds
+     * @return list<ReconciliationRunItem>
+     */
+    public function definePopulation(ReconciliationRun $run, array $sourceValueIds): array
+    {
+        if ($run->status() !== ReconciliationRunStatus::CREATED) {
+            throw new DomainInvariantViolation('Run population may only be defined while the run is CREATED.');
+        }
+        if ($sourceValueIds === []) {
+            throw new DomainInvariantViolation('ReconciliationRun population must not be empty.');
+        }
+
+        $normalizedIds = [];
+        foreach ($sourceValueIds as $sourceValueId) {
+            if ($sourceValueId <= 0) {
+                throw new DomainInvariantViolation('Run population contains an invalid SourceValue id.');
+            }
+            if (isset($normalizedIds[$sourceValueId])) {
+                throw new DomainInvariantViolation('Run population contains the same SourceValue more than once.');
+            }
+            $normalizedIds[$sourceValueId] = true;
+        }
+
+        return $this->transactional(function () use ($run, $sourceValueIds): array {
+            $items = [];
+            foreach ($sourceValueIds as $sourceValueId) {
+                $sourceValue = $this->sourceValues->findById($sourceValueId);
+                if ($sourceValue === null) {
+                    throw new DomainInvariantViolation(sprintf(
+                        'SourceValue %d does not exist.',
+                        $sourceValueId,
+                    ));
+                }
+                if ($sourceValue->sourceDeliveryId()->value() !== $run->sourceDeliveryId()->value()) {
+                    throw new DomainInvariantViolation(sprintf(
+                        'SourceValue %d does not belong to ReconciliationRun SourceDelivery.',
+                        $sourceValueId,
+                    ));
+                }
+                if ($this->runs->findItem($run->id(), $sourceValueId) !== null) {
+                    throw new DomainInvariantViolation(sprintf(
+                        'SourceValue %d already belongs to this ReconciliationRun.',
+                        $sourceValueId,
+                    ));
+                }
+
+                $item = ReconciliationRunItem::pending($run->id(), $sourceValueId);
+                $this->runs->saveItem($item);
+                $items[] = $item;
+            }
+
+            return $items;
+        });
+    }
+
+    public function start(ReconciliationRun $run, ?\DateTimeImmutable $startedAt = null): void
+    {
+        if ($this->runs->findItems($run->id()) === []) {
+            throw new DomainInvariantViolation('ReconciliationRun cannot start without RunItems.');
+        }
+
+        $run->start($startedAt);
+        $this->runs->save($run);
+    }
+
+    public function markItemCompleted(ReconciliationRun $run, int $sourceValueId): ReconciliationRunItem
+    {
+        return $this->changeItemStatus($run, $sourceValueId, ReconciliationRunItemStatus::COMPLETED);
+    }
+
+    public function markItemFailed(ReconciliationRun $run, int $sourceValueId): ReconciliationRunItem
+    {
+        return $this->changeItemStatus($run, $sourceValueId, ReconciliationRunItemStatus::FAILED);
+    }
+
+    public function complete(ReconciliationRun $run, ?\DateTimeImmutable $finishedAt = null): void
+    {
+        $items = $this->runs->findItems($run->id());
+        if ($items === []) {
+            throw new DomainInvariantViolation('ReconciliationRun cannot complete without RunItems.');
+        }
+        foreach ($items as $item) {
+            if ($item->status() === ReconciliationRunItemStatus::PENDING) {
+                throw new DomainInvariantViolation('ReconciliationRun cannot complete while RunItems are PENDING.');
+            }
+        }
+
+        $run->markCompleted($finishedAt);
+        $this->runs->save($run);
+    }
+
+    public function fail(ReconciliationRun $run, ?\DateTimeImmutable $finishedAt = null): void
+    {
+        $run->markFailed($finishedAt);
+        $this->runs->save($run);
+    }
+
+    private function changeItemStatus(
+        ReconciliationRun $run,
+        int $sourceValueId,
+        ReconciliationRunItemStatus $target,
+    ): ReconciliationRunItem {
+        if ($run->status() !== ReconciliationRunStatus::RUNNING) {
+            throw new DomainInvariantViolation('RunItems may only be processed while ReconciliationRun is RUNNING.');
+        }
+
+        $item = $this->runs->findItem($run->id(), $sourceValueId);
+        if ($item === null) {
+            throw new DomainInvariantViolation('SourceValue is not part of this ReconciliationRun population.');
+        }
+
+        if ($target === ReconciliationRunItemStatus::COMPLETED) {
+            $item->markCompleted();
+        } else {
+            $item->markFailed();
+        }
+        $this->runs->saveItem($item);
+
+        return $item;
+    }
+
+    /** @template T @param callable():T $operation @return T */
+    private function transactional(callable $operation): mixed
+    {
+        if ($this->transactionManager === null) {
+            return $operation();
+        }
+
+        $this->transactionManager->begin();
+        try {
+            $result = $operation();
+            $this->transactionManager->commit();
+            return $result;
+        } catch (Throwable $exception) {
+            if ($this->transactionManager->isActive()) {
+                $this->transactionManager->rollback();
+            }
+            if ($exception instanceof DomainInvariantViolation || $exception instanceof PersistenceException) {
+                throw $exception;
+            }
+            throw new PersistenceException('Unable to define ReconciliationRun population.', previous: $exception);
+        }
+    }
+}
diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/src/Domain/Reconciliation/InterpretationGraph.php qdr001a_work/base/reconcilix_20260829/src/Domain/Reconciliation/InterpretationGraph.php
--- qdr001a_pre/reconcilix_20260829/src/Domain/Reconciliation/InterpretationGraph.php	2026-07-18 04:44:00.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/src/Domain/Reconciliation/InterpretationGraph.php	2026-08-29 20:14:25.857421585 +0000
@@ -16,6 +16,7 @@
         private readonly string $statusUri,
         private readonly int $version = 1,
         private readonly \DateTimeImmutable $createdAt = new \DateTimeImmutable(),
+        private readonly ?ReconciliationRunId $reconciliationRunId = null,
     ) {
         if ($id !== null && $id <= 0) {
             throw new DomainInvariantViolation('InterpretationGraph.id must be positive when assigned.');
@@ -45,6 +46,7 @@
         ?string $createdBy = null,
         int $version = 1,
         ?\DateTimeImmutable $createdAt = null,
+        ?ReconciliationRunId $reconciliationRunId = null,
     ): self {
         $timestamp = $createdAt ?? new \DateTimeImmutable();
         $graph = new self(
@@ -55,6 +57,7 @@
             statusUri: $statusUri,
             version: $version,
             createdAt: $timestamp,
+            reconciliationRunId: $reconciliationRunId,
         );
 
         $graph->addRootNode(InterpretationNode::root(
@@ -139,6 +142,12 @@
         return $this->createdAt;
     }
 
+
+    public function reconciliationRunId(): ?ReconciliationRunId
+    {
+        return $this->reconciliationRunId;
+    }
+
     /** @return list<InterpretationNode> */
     public function nodes(): array
     {
diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/src/Domain/Reconciliation/ReconciliationRun.php qdr001a_work/base/reconcilix_20260829/src/Domain/Reconciliation/ReconciliationRun.php
--- qdr001a_pre/reconcilix_20260829/src/Domain/Reconciliation/ReconciliationRun.php	1970-01-01 00:00:00.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/src/Domain/Reconciliation/ReconciliationRun.php	2026-08-29 20:13:47.669401801 +0000
@@ -0,0 +1,194 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Domain\Reconciliation;
+
+use App\Domain\Delivery\SourceDeliveryId;
+
+final class ReconciliationRun
+{
+    /**
+     * @param array<string, mixed> $configuration
+     */
+    private function __construct(
+        private readonly ReconciliationRunId $id,
+        private readonly SourceDeliveryId $sourceDeliveryId,
+        private readonly string $targetVocabularyUri,
+        private readonly ?string $scopeVocabularyUri,
+        private ReconciliationRunStatus $status,
+        private readonly array $configuration,
+        private ?\DateTimeImmutable $startedAt,
+        private ?\DateTimeImmutable $finishedAt,
+    ) {
+        if (trim($targetVocabularyUri) === '') {
+            throw new DomainInvariantViolation('ReconciliationRun.targetVocabularyUri must not be empty.');
+        }
+
+        if ($scopeVocabularyUri !== null && trim($scopeVocabularyUri) === '') {
+            throw new DomainInvariantViolation(
+                'ReconciliationRun.scopeVocabularyUri must not be empty when provided.'
+            );
+        }
+
+        $this->assertLifecycleState();
+    }
+
+    /**
+     * @param array<string, mixed> $configuration
+     */
+    public static function create(
+        SourceDeliveryId $sourceDeliveryId,
+        string $targetVocabularyUri,
+        ?string $scopeVocabularyUri = null,
+        array $configuration = [],
+    ): self {
+        return new self(
+            id: ReconciliationRunId::generate(),
+            sourceDeliveryId: $sourceDeliveryId,
+            targetVocabularyUri: $targetVocabularyUri,
+            scopeVocabularyUri: $scopeVocabularyUri,
+            status: ReconciliationRunStatus::CREATED,
+            configuration: $configuration,
+            startedAt: null,
+            finishedAt: null,
+        );
+    }
+
+    /**
+     * @param array<string, mixed> $configuration
+     */
+    public static function rehydrate(
+        ReconciliationRunId $id,
+        SourceDeliveryId $sourceDeliveryId,
+        string $targetVocabularyUri,
+        ?string $scopeVocabularyUri,
+        ReconciliationRunStatus $status,
+        array $configuration,
+        ?\DateTimeImmutable $startedAt,
+        ?\DateTimeImmutable $finishedAt,
+    ): self {
+        return new self(
+            id: $id,
+            sourceDeliveryId: $sourceDeliveryId,
+            targetVocabularyUri: $targetVocabularyUri,
+            scopeVocabularyUri: $scopeVocabularyUri,
+            status: $status,
+            configuration: $configuration,
+            startedAt: $startedAt,
+            finishedAt: $finishedAt,
+        );
+    }
+
+    public function start(?\DateTimeImmutable $startedAt = null): void
+    {
+        if ($this->status !== ReconciliationRunStatus::CREATED) {
+            throw new DomainInvariantViolation(sprintf(
+                'Cannot start ReconciliationRun in status %s.',
+                $this->status->value,
+            ));
+        }
+
+        $this->status = ReconciliationRunStatus::RUNNING;
+        $this->startedAt = $startedAt ?? new \DateTimeImmutable();
+    }
+
+    public function markCompleted(?\DateTimeImmutable $finishedAt = null): void
+    {
+        if ($this->status !== ReconciliationRunStatus::RUNNING || $this->startedAt === null) {
+            throw new DomainInvariantViolation(sprintf(
+                'Cannot complete ReconciliationRun in status %s.',
+                $this->status->value,
+            ));
+        }
+
+        $timestamp = $finishedAt ?? new \DateTimeImmutable();
+        if ($timestamp < $this->startedAt) {
+            throw new DomainInvariantViolation('ReconciliationRun.finishedAt must not precede startedAt.');
+        }
+
+        $this->status = ReconciliationRunStatus::COMPLETED;
+        $this->finishedAt = $timestamp;
+    }
+
+    public function markFailed(?\DateTimeImmutable $finishedAt = null): void
+    {
+        if (!in_array($this->status, [ReconciliationRunStatus::CREATED, ReconciliationRunStatus::RUNNING], true)) {
+            throw new DomainInvariantViolation(sprintf(
+                'Cannot fail ReconciliationRun in status %s.',
+                $this->status->value,
+            ));
+        }
+
+        $timestamp = $finishedAt ?? new \DateTimeImmutable();
+        if ($this->startedAt === null) {
+            $this->startedAt = $timestamp;
+        }
+        if ($timestamp < $this->startedAt) {
+            throw new DomainInvariantViolation('ReconciliationRun.finishedAt must not precede startedAt.');
+        }
+
+        $this->status = ReconciliationRunStatus::FAILED;
+        $this->finishedAt = $timestamp;
+    }
+
+    public function id(): ReconciliationRunId
+    {
+        return $this->id;
+    }
+
+    public function sourceDeliveryId(): SourceDeliveryId
+    {
+        return $this->sourceDeliveryId;
+    }
+
+    public function targetVocabularyUri(): string
+    {
+        return $this->targetVocabularyUri;
+    }
+
+    public function scopeVocabularyUri(): ?string
+    {
+        return $this->scopeVocabularyUri;
+    }
+
+    public function status(): ReconciliationRunStatus
+    {
+        return $this->status;
+    }
+
+    /** @return array<string, mixed> */
+    public function configuration(): array
+    {
+        return $this->configuration;
+    }
+
+    public function startedAt(): ?\DateTimeImmutable
+    {
+        return $this->startedAt;
+    }
+
+    public function finishedAt(): ?\DateTimeImmutable
+    {
+        return $this->finishedAt;
+    }
+
+    private function assertLifecycleState(): void
+    {
+        if ($this->finishedAt !== null && $this->startedAt !== null && $this->finishedAt < $this->startedAt) {
+            throw new DomainInvariantViolation('ReconciliationRun.finishedAt must not precede startedAt.');
+        }
+
+        $valid = match ($this->status) {
+            ReconciliationRunStatus::CREATED => $this->startedAt === null && $this->finishedAt === null,
+            ReconciliationRunStatus::RUNNING => $this->startedAt !== null && $this->finishedAt === null,
+            ReconciliationRunStatus::COMPLETED,
+            ReconciliationRunStatus::FAILED => $this->startedAt !== null && $this->finishedAt !== null,
+        };
+
+        if (!$valid) {
+            throw new DomainInvariantViolation(
+                'ReconciliationRun timestamps are inconsistent with status ' . $this->status->value . '.'
+            );
+        }
+    }
+}
diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/src/Domain/Reconciliation/ReconciliationRunId.php qdr001a_work/base/reconcilix_20260829/src/Domain/Reconciliation/ReconciliationRunId.php
--- qdr001a_pre/reconcilix_20260829/src/Domain/Reconciliation/ReconciliationRunId.php	1970-01-01 00:00:00.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/src/Domain/Reconciliation/ReconciliationRunId.php	2026-08-29 20:13:47.668739501 +0000
@@ -0,0 +1,44 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Domain\Reconciliation;
+
+final readonly class ReconciliationRunId
+{
+    private function __construct(private string $value)
+    {
+        if (!preg_match(
+            '/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/D',
+            $value
+        )) {
+            throw new DomainInvariantViolation('ReconciliationRunId must be a valid UUID v4.');
+        }
+    }
+
+    public static function generate(): self
+    {
+        $bytes = random_bytes(16);
+        $bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
+        $bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);
+        $hex = bin2hex($bytes);
+
+        return new self(sprintf(
+            '%s-%s-%s-%s-%s',
+            substr($hex, 0, 8),
+            substr($hex, 8, 4),
+            substr($hex, 12, 4),
+            substr($hex, 16, 4),
+            substr($hex, 20, 12),
+        ));
+    }
+
+    public static function fromString(string $value): self
+    {
+        return new self($value);
+    }
+
+    public function value(): string
+    {
+        return $this->value;
+    }
+}
diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/src/Domain/Reconciliation/ReconciliationRunItem.php qdr001a_work/base/reconcilix_20260829/src/Domain/Reconciliation/ReconciliationRunItem.php
--- qdr001a_pre/reconcilix_20260829/src/Domain/Reconciliation/ReconciliationRunItem.php	1970-01-01 00:00:00.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/src/Domain/Reconciliation/ReconciliationRunItem.php	2026-08-29 20:13:47.669974836 +0000
@@ -0,0 +1,93 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Domain\Reconciliation;
+
+final class ReconciliationRunItem
+{
+    private function __construct(
+        private ?int $id,
+        private readonly ReconciliationRunId $reconciliationRunId,
+        private readonly int $sourceValueId,
+        private ReconciliationRunItemStatus $status,
+    ) {
+        if ($id !== null && $id <= 0) {
+            throw new DomainInvariantViolation('ReconciliationRunItem.id must be positive when assigned.');
+        }
+        if ($sourceValueId <= 0) {
+            throw new DomainInvariantViolation('ReconciliationRunItem.sourceValueId must be positive.');
+        }
+    }
+
+    public static function pending(ReconciliationRunId $reconciliationRunId, int $sourceValueId): self
+    {
+        return new self(
+            id: null,
+            reconciliationRunId: $reconciliationRunId,
+            sourceValueId: $sourceValueId,
+            status: ReconciliationRunItemStatus::PENDING,
+        );
+    }
+
+    public static function rehydrate(
+        int $id,
+        ReconciliationRunId $reconciliationRunId,
+        int $sourceValueId,
+        ReconciliationRunItemStatus $status,
+    ): self {
+        return new self($id, $reconciliationRunId, $sourceValueId, $status);
+    }
+
+    public function assignId(int $id): void
+    {
+        if ($id <= 0) {
+            throw new DomainInvariantViolation('ReconciliationRunItem.id must be positive.');
+        }
+        if ($this->id !== null && $this->id !== $id) {
+            throw new DomainInvariantViolation('ReconciliationRunItem.id cannot be reassigned.');
+        }
+        $this->id = $id;
+    }
+
+    public function markCompleted(): void
+    {
+        $this->transitionFromPending(ReconciliationRunItemStatus::COMPLETED);
+    }
+
+    public function markFailed(): void
+    {
+        $this->transitionFromPending(ReconciliationRunItemStatus::FAILED);
+    }
+
+    public function id(): ?int
+    {
+        return $this->id;
+    }
+
+    public function reconciliationRunId(): ReconciliationRunId
+    {
+        return $this->reconciliationRunId;
+    }
+
+    public function sourceValueId(): int
+    {
+        return $this->sourceValueId;
+    }
+
+    public function status(): ReconciliationRunItemStatus
+    {
+        return $this->status;
+    }
+
+    private function transitionFromPending(ReconciliationRunItemStatus $target): void
+    {
+        if ($this->status !== ReconciliationRunItemStatus::PENDING) {
+            throw new DomainInvariantViolation(sprintf(
+                'Cannot change ReconciliationRunItem from status %s to %s.',
+                $this->status->value,
+                $target->value,
+            ));
+        }
+        $this->status = $target;
+    }
+}
diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/src/Domain/Reconciliation/ReconciliationRunItemStatus.php qdr001a_work/base/reconcilix_20260829/src/Domain/Reconciliation/ReconciliationRunItemStatus.php
--- qdr001a_pre/reconcilix_20260829/src/Domain/Reconciliation/ReconciliationRunItemStatus.php	1970-01-01 00:00:00.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/src/Domain/Reconciliation/ReconciliationRunItemStatus.php	2026-08-29 20:13:47.668145996 +0000
@@ -0,0 +1,11 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Domain\Reconciliation;
+
+enum ReconciliationRunItemStatus: string
+{
+    case PENDING = 'PENDING';
+    case COMPLETED = 'COMPLETED';
+    case FAILED = 'FAILED';
+}
diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/src/Domain/Reconciliation/ReconciliationRunStatus.php qdr001a_work/base/reconcilix_20260829/src/Domain/Reconciliation/ReconciliationRunStatus.php
--- qdr001a_pre/reconcilix_20260829/src/Domain/Reconciliation/ReconciliationRunStatus.php	1970-01-01 00:00:00.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/src/Domain/Reconciliation/ReconciliationRunStatus.php	2026-08-29 20:13:47.667529871 +0000
@@ -0,0 +1,12 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Domain\Reconciliation;
+
+enum ReconciliationRunStatus: string
+{
+    case CREATED = 'CREATED';
+    case RUNNING = 'RUNNING';
+    case COMPLETED = 'COMPLETED';
+    case FAILED = 'FAILED';
+}
diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/src/Domain/Reconciliation/SourceValue.php qdr001a_work/base/reconcilix_20260829/src/Domain/Reconciliation/SourceValue.php
--- qdr001a_pre/reconcilix_20260829/src/Domain/Reconciliation/SourceValue.php	2026-08-01 09:59:52.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/src/Domain/Reconciliation/SourceValue.php	2026-08-29 20:14:25.857780867 +0000
@@ -86,18 +86,27 @@
 
     public function addInterpretationGraph(InterpretationGraph $graph): void
     {
-        if ($this->interpretationGraphs !== []) {
-            throw new DomainInvariantViolation(
-                'WP1 supports exactly one InterpretationGraph per SourceValue.'
-            );
-        }
-
         if ($graph->sourceFragment() !== $this->substring($graph->spanStart(), $graph->spanEnd())) {
             throw new DomainInvariantViolation(
                 'InterpretationGraph.sourceFragment must match SourceValue.value and graph span.'
             );
         }
 
+        foreach ($this->interpretationGraphs as $existingGraph) {
+            $sameRun = $existingGraph->reconciliationRunId()?->value()
+                === $graph->reconciliationRunId()?->value();
+            if (
+                $sameRun
+                && $existingGraph->spanStart() === $graph->spanStart()
+                && $existingGraph->spanEnd() === $graph->spanEnd()
+                && $existingGraph->version() === $graph->version()
+            ) {
+                throw new DomainInvariantViolation(
+                    'An equivalent InterpretationGraph already exists for this SourceValue and ReconciliationRun.'
+                );
+            }
+        }
+
         $this->interpretationGraphs[] = $graph;
     }
 
@@ -107,6 +116,7 @@
         ?string $createdBy = null,
         int $version = 1,
         ?\DateTimeImmutable $createdAt = null,
+        ?ReconciliationRunId $reconciliationRunId = null,
     ): InterpretationGraph {
         $graph = InterpretationGraph::forWholeSourceValue(
             sourceValue: $this,
@@ -115,6 +125,7 @@
             createdBy: $createdBy,
             version: $version,
             createdAt: $createdAt,
+            reconciliationRunId: $reconciliationRunId,
         );
 
         $this->addInterpretationGraph($graph);
diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/src/Domain/Repository/ReconciliationRunRepository.php qdr001a_work/base/reconcilix_20260829/src/Domain/Repository/ReconciliationRunRepository.php
--- qdr001a_pre/reconcilix_20260829/src/Domain/Repository/ReconciliationRunRepository.php	1970-01-01 00:00:00.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/src/Domain/Repository/ReconciliationRunRepository.php	2026-08-29 20:14:10.587350094 +0000
@@ -0,0 +1,22 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Domain\Repository;
+
+use App\Domain\Reconciliation\ReconciliationRun;
+use App\Domain\Reconciliation\ReconciliationRunId;
+use App\Domain\Reconciliation\ReconciliationRunItem;
+
+interface ReconciliationRunRepository
+{
+    public function save(ReconciliationRun $run): void;
+
+    public function findById(ReconciliationRunId $id): ?ReconciliationRun;
+
+    public function saveItem(ReconciliationRunItem $item): void;
+
+    /** @return list<ReconciliationRunItem> */
+    public function findItems(ReconciliationRunId $runId): array;
+
+    public function findItem(ReconciliationRunId $runId, int $sourceValueId): ?ReconciliationRunItem;
+}
diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/src/Infrastructure/Composition/ReconciliationCompositionRoot.php qdr001a_work/base/reconcilix_20260829/src/Infrastructure/Composition/ReconciliationCompositionRoot.php
--- qdr001a_pre/reconcilix_20260829/src/Infrastructure/Composition/ReconciliationCompositionRoot.php	2026-08-08 06:04:28.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/src/Infrastructure/Composition/ReconciliationCompositionRoot.php	2026-08-29 20:15:10.619219957 +0000
@@ -5,6 +5,8 @@
 
 use App\Application\Delivery\OpenRefineSourceDeliveryRegistrar;
 use App\Application\Delivery\SourceDeliveryService;
+use App\Application\Reconciliation\ReconciliationRunService;
+use App\Application\Persistence\InterpretationGraphRepository;
 use App\Application\Persistence\ReconciliationResultRepository;
 use App\Application\Persistence\SourceValueRepository;
 use App\Application\Persistence\TransactionManager;
@@ -23,11 +25,16 @@
 use App\Infrastructure\Persistence\Mapper\InterpretationNodeMapper;
 use App\Infrastructure\Persistence\Mapper\MatchDecisionMapper;
 use App\Infrastructure\Persistence\Mapper\ReconciliationResultMapper;
+use App\Infrastructure\Persistence\Mapper\ReconciliationRunItemMapper;
+use App\Infrastructure\Persistence\Mapper\ReconciliationRunMapper;
 use App\Infrastructure\Persistence\Mapper\SourceDeliveryMapper;
 use App\Infrastructure\Persistence\Mapper\SourceValueMapper;
+use App\Infrastructure\Persistence\Repository\RelationalInterpretationGraphRepository;
 use App\Infrastructure\Persistence\Repository\RelationalReconciliationResultRepository;
+use App\Infrastructure\Persistence\Repository\RelationalReconciliationRunRepository;
 use App\Infrastructure\Persistence\Repository\RelationalSourceDeliveryRepository;
 use App\Infrastructure\Persistence\Repository\RelationalSourceValueRepository;
+use App\Domain\Repository\ReconciliationRunRepository;
 use App\Domain\Repository\SourceDeliveryRepository;
 use App\Manifest\ManifestBuilder;
 use App\OpenRefine\OpenRefineRequestMapper;
@@ -76,6 +83,26 @@
         );
     }
 
+    public function createInterpretationGraphRepository(): InterpretationGraphRepository
+    {
+        $connection = $this->databaseConnectionFactory->connection();
+
+        return new RelationalInterpretationGraphRepository(
+            new InterpretationGraphMapper($connection),
+            new InterpretationNodeMapper($connection),
+        );
+    }
+
+    public function createReconciliationRunRepository(): ReconciliationRunRepository
+    {
+        $connection = $this->databaseConnectionFactory->connection();
+
+        return new RelationalReconciliationRunRepository(
+            new ReconciliationRunMapper($connection),
+            new ReconciliationRunItemMapper($connection),
+        );
+    }
+
     public function createReconciliationResultRepository(): ReconciliationResultRepository
     {
         $connection = $this->databaseConnectionFactory->connection();
@@ -104,6 +131,16 @@
         );
     }
 
+    public function createReconciliationRunService(): ReconciliationRunService
+    {
+        return new ReconciliationRunService(
+            $this->createReconciliationRunRepository(),
+            $this->createSourceDeliveryRepository(),
+            $this->createSourceValueRepository(),
+            $this->createTransactionManager(),
+        );
+    }
+
     public function createController(): ReconciliationController
     {
         $vocabularies = $this->configuration->vocabularies();
diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/src/Infrastructure/Persistence/Mapper/InterpretationGraphMapper.php qdr001a_work/base/reconcilix_20260829/src/Infrastructure/Persistence/Mapper/InterpretationGraphMapper.php
--- qdr001a_pre/reconcilix_20260829/src/Infrastructure/Persistence/Mapper/InterpretationGraphMapper.php	2026-07-20 20:42:57.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/src/Infrastructure/Persistence/Mapper/InterpretationGraphMapper.php	2026-08-29 20:14:26.015507025 +0000
@@ -1,12 +1,70 @@
 <?php
 declare(strict_types=1);
+
 namespace App\Infrastructure\Persistence\Mapper;
+
 use App\Domain\Reconciliation\InterpretationGraph;
+use App\Domain\Reconciliation\ReconciliationRunId;
 use PDO;
+
 final class InterpretationGraphMapper
 {
-    public function __construct(private readonly PDO $connection) {}
-    public function insert(InterpretationGraph $graph,int $sourceValueId):int { $s=$this->connection->prepare('INSERT INTO interpretation_graph (source_value_id, span_start, span_end, source_fragment, status_uri, version, created_at) VALUES (:source_value_id, :span_start, :span_end, :source_fragment, :status_uri, :version, :created_at)'); $s->execute(['source_value_id'=>$sourceValueId,'span_start'=>$graph->spanStart(),'span_end'=>$graph->spanEnd(),'source_fragment'=>$graph->sourceFragment(),'status_uri'=>$graph->statusUri(),'version'=>$graph->version(),'created_at'=>$graph->createdAt()->format('Y-m-d H:i:s.u')]); return (int)$this->connection->lastInsertId(); }
+    public function __construct(private readonly PDO $connection)
+    {
+    }
+
+    public function insert(InterpretationGraph $graph, int $sourceValueId): int
+    {
+        $statement = $this->connection->prepare(
+            'INSERT INTO interpretation_graph '
+            . '(source_value_id, reconciliation_run_id, span_start, span_end, source_fragment, status_uri, version, created_at) '
+            . 'VALUES (:source_value_id, :reconciliation_run_id, :span_start, :span_end, :source_fragment, :status_uri, :version, :created_at)'
+        );
+        $statement->execute([
+            'source_value_id' => $sourceValueId,
+            'reconciliation_run_id' => $graph->reconciliationRunId()?->value(),
+            'span_start' => $graph->spanStart(),
+            'span_end' => $graph->spanEnd(),
+            'source_fragment' => $graph->sourceFragment(),
+            'status_uri' => $graph->statusUri(),
+            'version' => $graph->version(),
+            'created_at' => $graph->createdAt()->format('Y-m-d H:i:s.u'),
+        ]);
+
+        return (int) $this->connection->lastInsertId();
+    }
+
     /** @return list<InterpretationGraph> */
-    public function findBySourceValueId(int $sourceValueId):array { $s=$this->connection->prepare('SELECT id, span_start, span_end, source_fragment, status_uri, version, created_at FROM interpretation_graph WHERE source_value_id = :source_value_id ORDER BY id'); $s->execute(['source_value_id'=>$sourceValueId]); $out=[]; while(($r=$s->fetch(PDO::FETCH_ASSOC))!==false){$out[]=new InterpretationGraph((int)$r['id'],(int)$r['span_start'],(int)$r['span_end'],(string)$r['source_fragment'],(string)$r['status_uri'],(int)$r['version'],new \DateTimeImmutable((string)$r['created_at']));} return $out; }
+    public function findBySourceValueId(int $sourceValueId): array
+    {
+        $statement = $this->connection->prepare(
+            'SELECT id, reconciliation_run_id, span_start, span_end, source_fragment, status_uri, version, created_at '
+            . 'FROM interpretation_graph WHERE source_value_id = :source_value_id ORDER BY id'
+        );
+        $statement->execute(['source_value_id' => $sourceValueId]);
+
+        $graphs = [];
+        while (($row = $statement->fetch(PDO::FETCH_ASSOC)) !== false) {
+            $graphs[] = $this->mapRowToDomain($row);
+        }
+
+        return $graphs;
+    }
+
+    /** @param array<string, mixed> $row */
+    private function mapRowToDomain(array $row): InterpretationGraph
+    {
+        return new InterpretationGraph(
+            id: (int) $row['id'],
+            spanStart: (int) $row['span_start'],
+            spanEnd: (int) $row['span_end'],
+            sourceFragment: (string) $row['source_fragment'],
+            statusUri: (string) $row['status_uri'],
+            version: (int) $row['version'],
+            createdAt: new \DateTimeImmutable((string) $row['created_at']),
+            reconciliationRunId: $row['reconciliation_run_id'] !== null
+                ? ReconciliationRunId::fromString((string) $row['reconciliation_run_id'])
+                : null,
+        );
+    }
 }
diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/src/Infrastructure/Persistence/Mapper/ReconciliationRunItemMapper.php qdr001a_work/base/reconcilix_20260829/src/Infrastructure/Persistence/Mapper/ReconciliationRunItemMapper.php
--- qdr001a_pre/reconcilix_20260829/src/Infrastructure/Persistence/Mapper/ReconciliationRunItemMapper.php	1970-01-01 00:00:00.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/src/Infrastructure/Persistence/Mapper/ReconciliationRunItemMapper.php	2026-08-29 20:14:10.588738950 +0000
@@ -0,0 +1,96 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Infrastructure\Persistence\Mapper;
+
+use App\Domain\Reconciliation\ReconciliationRunId;
+use App\Domain\Reconciliation\ReconciliationRunItem;
+use App\Domain\Reconciliation\ReconciliationRunItemStatus;
+use PDO;
+
+final class ReconciliationRunItemMapper
+{
+    public function __construct(private readonly PDO $connection)
+    {
+    }
+
+    public function insert(ReconciliationRunItem $item): int
+    {
+        $statement = $this->connection->prepare(
+            'INSERT INTO reconciliation_run_item (reconciliation_run_id, source_value_id, status) '
+            . 'VALUES (:reconciliation_run_id, :source_value_id, :status)'
+        );
+        $statement->execute($this->parameters($item));
+
+        return (int) $this->connection->lastInsertId();
+    }
+
+    public function update(ReconciliationRunItem $item): void
+    {
+        if ($item->id() === null) {
+            throw new \InvalidArgumentException('Cannot update a non-persistent ReconciliationRunItem.');
+        }
+
+        $statement = $this->connection->prepare(
+            'UPDATE reconciliation_run_item SET status = :status '
+            . 'WHERE id = :id AND reconciliation_run_id = :reconciliation_run_id AND source_value_id = :source_value_id'
+        );
+        $parameters = $this->parameters($item);
+        $parameters['id'] = $item->id();
+        $statement->execute($parameters);
+    }
+
+    /** @return list<ReconciliationRunItem> */
+    public function findByRunId(ReconciliationRunId $runId): array
+    {
+        $statement = $this->connection->prepare(
+            'SELECT id, reconciliation_run_id, source_value_id, status '
+            . 'FROM reconciliation_run_item WHERE reconciliation_run_id = :reconciliation_run_id ORDER BY id'
+        );
+        $statement->execute(['reconciliation_run_id' => $runId->value()]);
+
+        $items = [];
+        while (($row = $statement->fetch(PDO::FETCH_ASSOC)) !== false) {
+            $items[] = $this->mapRowToDomain($row);
+        }
+
+        return $items;
+    }
+
+    public function findByRunAndSourceValue(ReconciliationRunId $runId, int $sourceValueId): ?ReconciliationRunItem
+    {
+        $statement = $this->connection->prepare(
+            'SELECT id, reconciliation_run_id, source_value_id, status '
+            . 'FROM reconciliation_run_item '
+            . 'WHERE reconciliation_run_id = :reconciliation_run_id AND source_value_id = :source_value_id'
+        );
+        $statement->execute([
+            'reconciliation_run_id' => $runId->value(),
+            'source_value_id' => $sourceValueId,
+        ]);
+        $row = $statement->fetch(PDO::FETCH_ASSOC);
+
+        return $row === false ? null : $this->mapRowToDomain($row);
+    }
+
+    /** @param array<string, mixed> $row */
+    private function mapRowToDomain(array $row): ReconciliationRunItem
+    {
+        return ReconciliationRunItem::rehydrate(
+            id: (int) $row['id'],
+            reconciliationRunId: ReconciliationRunId::fromString((string) $row['reconciliation_run_id']),
+            sourceValueId: (int) $row['source_value_id'],
+            status: ReconciliationRunItemStatus::from((string) $row['status']),
+        );
+    }
+
+    /** @return array<string, int|string> */
+    private function parameters(ReconciliationRunItem $item): array
+    {
+        return [
+            'reconciliation_run_id' => $item->reconciliationRunId()->value(),
+            'source_value_id' => $item->sourceValueId(),
+            'status' => $item->status()->value,
+        ];
+    }
+}
diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/src/Infrastructure/Persistence/Mapper/ReconciliationRunMapper.php qdr001a_work/base/reconcilix_20260829/src/Infrastructure/Persistence/Mapper/ReconciliationRunMapper.php
--- qdr001a_pre/reconcilix_20260829/src/Infrastructure/Persistence/Mapper/ReconciliationRunMapper.php	1970-01-01 00:00:00.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/src/Infrastructure/Persistence/Mapper/ReconciliationRunMapper.php	2026-08-29 20:16:36.900804210 +0000
@@ -0,0 +1,93 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Infrastructure\Persistence\Mapper;
+
+use App\Domain\Delivery\SourceDeliveryId;
+use App\Domain\Reconciliation\ReconciliationRun;
+use App\Domain\Reconciliation\ReconciliationRunId;
+use App\Domain\Reconciliation\ReconciliationRunStatus;
+use PDO;
+
+final class ReconciliationRunMapper
+{
+    private readonly JsonValueEncoder $jsonEncoder;
+    private readonly JsonValueDecoder $jsonDecoder;
+
+    public function __construct(private readonly PDO $connection)
+    {
+        $this->jsonEncoder = new JsonValueEncoder();
+        $this->jsonDecoder = new JsonValueDecoder();
+    }
+
+    public function insert(ReconciliationRun $run): void
+    {
+        $statement = $this->connection->prepare(
+            'INSERT INTO reconciliation_run '
+            . '(id, source_delivery_id, target_vocabulary_uri, scope_vocabulary_uri, started_at, finished_at, status, configuration_json) '
+            . 'VALUES (:id, :source_delivery_id, :target_vocabulary_uri, :scope_vocabulary_uri, :started_at, :finished_at, :status, :configuration_json)'
+        );
+        $statement->execute($this->parameters($run));
+    }
+
+    public function update(ReconciliationRun $run): void
+    {
+        // Run identity, source delivery, vocabulary dimensions and configuration
+        // are immutable snapshots. Lifecycle updates therefore touch timestamps
+        // and status only.
+        $statement = $this->connection->prepare(
+            'UPDATE reconciliation_run SET '
+            . 'started_at = :started_at, finished_at = :finished_at, status = :status '
+            . 'WHERE id = :id'
+        );
+        $statement->execute([
+            'id' => $run->id()->value(),
+            'started_at' => $run->startedAt()?->format('Y-m-d H:i:s.u'),
+            'finished_at' => $run->finishedAt()?->format('Y-m-d H:i:s.u'),
+            'status' => $run->status()->value,
+        ]);
+    }
+
+    public function findById(ReconciliationRunId $id): ?ReconciliationRun
+    {
+        $statement = $this->connection->prepare(
+            'SELECT id, source_delivery_id, target_vocabulary_uri, scope_vocabulary_uri, '
+            . 'started_at, finished_at, status, configuration_json '
+            . 'FROM reconciliation_run WHERE id = :id'
+        );
+        $statement->execute(['id' => $id->value()]);
+        $row = $statement->fetch(PDO::FETCH_ASSOC);
+
+        return $row === false ? null : $this->mapRowToDomain($row);
+    }
+
+    /** @param array<string, mixed> $row */
+    public function mapRowToDomain(array $row): ReconciliationRun
+    {
+        return ReconciliationRun::rehydrate(
+            id: ReconciliationRunId::fromString((string) $row['id']),
+            sourceDeliveryId: SourceDeliveryId::fromString((string) $row['source_delivery_id']),
+            targetVocabularyUri: (string) $row['target_vocabulary_uri'],
+            scopeVocabularyUri: $row['scope_vocabulary_uri'] !== null ? (string) $row['scope_vocabulary_uri'] : null,
+            status: ReconciliationRunStatus::from((string) $row['status']),
+            configuration: $this->jsonDecoder->decode((string) $row['configuration_json']) ?? [],
+            startedAt: $row['started_at'] !== null ? new \DateTimeImmutable((string) $row['started_at']) : null,
+            finishedAt: $row['finished_at'] !== null ? new \DateTimeImmutable((string) $row['finished_at']) : null,
+        );
+    }
+
+    /** @return array<string, string|null> */
+    private function parameters(ReconciliationRun $run): array
+    {
+        return [
+            'id' => $run->id()->value(),
+            'source_delivery_id' => $run->sourceDeliveryId()->value(),
+            'target_vocabulary_uri' => $run->targetVocabularyUri(),
+            'scope_vocabulary_uri' => $run->scopeVocabularyUri(),
+            'started_at' => $run->startedAt()?->format('Y-m-d H:i:s.u'),
+            'finished_at' => $run->finishedAt()?->format('Y-m-d H:i:s.u'),
+            'status' => $run->status()->value,
+            'configuration_json' => $this->jsonEncoder->encode($run->configuration()) ?? '{}',
+        ];
+    }
+}
diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/src/Infrastructure/Persistence/Repository/RelationalInterpretationGraphRepository.php qdr001a_work/base/reconcilix_20260829/src/Infrastructure/Persistence/Repository/RelationalInterpretationGraphRepository.php
--- qdr001a_pre/reconcilix_20260829/src/Infrastructure/Persistence/Repository/RelationalInterpretationGraphRepository.php	1970-01-01 00:00:00.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/src/Infrastructure/Persistence/Repository/RelationalInterpretationGraphRepository.php	2026-08-29 20:14:43.051591699 +0000
@@ -0,0 +1,49 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Infrastructure\Persistence\Repository;
+
+use App\Application\Persistence\InterpretationGraphRepository;
+use App\Application\Persistence\PersistenceException;
+use App\Domain\Reconciliation\InterpretationGraph;
+use App\Infrastructure\Persistence\Mapper\InterpretationGraphMapper;
+use App\Infrastructure\Persistence\Mapper\InterpretationNodeMapper;
+use Throwable;
+
+final class RelationalInterpretationGraphRepository implements InterpretationGraphRepository
+{
+    public function __construct(
+        private readonly InterpretationGraphMapper $graphs,
+        private readonly InterpretationNodeMapper $nodes,
+    ) {
+    }
+
+    public function saveForSourceValue(InterpretationGraph $graph, int $sourceValueId): void
+    {
+        if ($sourceValueId <= 0) {
+            throw new PersistenceException('SourceValue id must be positive.');
+        }
+        if ($graph->id() !== null) {
+            throw new PersistenceException('InterpretationGraph is already persistent.');
+        }
+        if ($graph->reconciliationRunId() === null) {
+            throw new PersistenceException(
+                'Run-based InterpretationGraph persistence requires reconciliationRunId.'
+            );
+        }
+
+        try {
+            $graphId = $this->graphs->insert($graph, $sourceValueId);
+            $graph->assignId($graphId);
+
+            foreach ($graph->nodes() as $node) {
+                $nodeId = $this->nodes->insert($node, $graphId, $node->parentId());
+                $node->assignId($nodeId);
+            }
+        } catch (PersistenceException $exception) {
+            throw $exception;
+        } catch (Throwable $exception) {
+            throw new PersistenceException('Unable to persist InterpretationGraph.', previous: $exception);
+        }
+    }
+}
diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/src/Infrastructure/Persistence/Repository/RelationalReconciliationRunRepository.php qdr001a_work/base/reconcilix_20260829/src/Infrastructure/Persistence/Repository/RelationalReconciliationRunRepository.php
--- qdr001a_pre/reconcilix_20260829/src/Infrastructure/Persistence/Repository/RelationalReconciliationRunRepository.php	1970-01-01 00:00:00.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/src/Infrastructure/Persistence/Repository/RelationalReconciliationRunRepository.php	2026-08-29 20:14:10.589386602 +0000
@@ -0,0 +1,92 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Infrastructure\Persistence\Repository;
+
+use App\Application\Persistence\PersistenceException;
+use App\Domain\Reconciliation\ReconciliationRun;
+use App\Domain\Reconciliation\ReconciliationRunId;
+use App\Domain\Reconciliation\ReconciliationRunItem;
+use App\Domain\Repository\ReconciliationRunRepository;
+use App\Infrastructure\Persistence\Mapper\ReconciliationRunItemMapper;
+use App\Infrastructure\Persistence\Mapper\ReconciliationRunMapper;
+use Throwable;
+
+final class RelationalReconciliationRunRepository implements ReconciliationRunRepository
+{
+    public function __construct(
+        private readonly ReconciliationRunMapper $runs,
+        private readonly ReconciliationRunItemMapper $items,
+    ) {
+    }
+
+    public function save(ReconciliationRun $run): void
+    {
+        try {
+            if ($this->runs->findById($run->id()) === null) {
+                $this->runs->insert($run);
+                return;
+            }
+
+            $this->runs->update($run);
+        } catch (PersistenceException $exception) {
+            throw $exception;
+        } catch (Throwable $exception) {
+            throw new PersistenceException('Unable to persist ReconciliationRun.', previous: $exception);
+        }
+    }
+
+    public function findById(ReconciliationRunId $id): ?ReconciliationRun
+    {
+        try {
+            return $this->runs->findById($id);
+        } catch (PersistenceException $exception) {
+            throw $exception;
+        } catch (Throwable $exception) {
+            throw new PersistenceException('Unable to rehydrate ReconciliationRun.', previous: $exception);
+        }
+    }
+
+    public function saveItem(ReconciliationRunItem $item): void
+    {
+        try {
+            if ($item->id() === null) {
+                $id = $this->items->insert($item);
+                $item->assignId($id);
+                return;
+            }
+
+            $this->items->update($item);
+        } catch (PersistenceException $exception) {
+            throw $exception;
+        } catch (Throwable $exception) {
+            throw new PersistenceException('Unable to persist ReconciliationRunItem.', previous: $exception);
+        }
+    }
+
+    public function findItems(ReconciliationRunId $runId): array
+    {
+        try {
+            return $this->items->findByRunId($runId);
+        } catch (PersistenceException $exception) {
+            throw $exception;
+        } catch (Throwable $exception) {
+            throw new PersistenceException('Unable to rehydrate ReconciliationRunItems.', previous: $exception);
+        }
+    }
+
+    public function findItem(ReconciliationRunId $runId, int $sourceValueId): ?ReconciliationRunItem
+    {
+        if ($sourceValueId <= 0) {
+            throw new PersistenceException('SourceValue id must be positive.');
+        }
+
+        try {
+            return $this->items->findByRunAndSourceValue($runId, $sourceValueId);
+        } catch (PersistenceException $exception) {
+            throw $exception;
+        } catch (Throwable $exception) {
+            throw new PersistenceException('Unable to rehydrate ReconciliationRunItem.', previous: $exception);
+        }
+    }
+}
diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/tests/Application/Reconciliation/ReconciliationRunServiceTest.php qdr001a_work/base/reconcilix_20260829/tests/Application/Reconciliation/ReconciliationRunServiceTest.php
--- qdr001a_pre/reconcilix_20260829/tests/Application/Reconciliation/ReconciliationRunServiceTest.php	1970-01-01 00:00:00.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/tests/Application/Reconciliation/ReconciliationRunServiceTest.php	2026-08-29 20:16:23.707958862 +0000
@@ -0,0 +1,138 @@
+<?php
+declare(strict_types=1);
+
+require_once dirname(__DIR__, 3) . '/src/bootstrap.php';
+
+use App\Application\Persistence\SourceValueRepository;
+use App\Application\Reconciliation\ReconciliationRunService;
+use App\Domain\Delivery\DeliveryChannel;
+use App\Domain\Delivery\SourceDelivery;
+use App\Domain\Delivery\SourceDeliveryId;
+use App\Domain\Delivery\SourceSystemId;
+use App\Domain\Delivery\TenantId;
+use App\Domain\Reconciliation\DomainInvariantViolation;
+use App\Domain\Reconciliation\ReconciliationRun;
+use App\Domain\Reconciliation\ReconciliationRunId;
+use App\Domain\Reconciliation\ReconciliationRunItem;
+use App\Domain\Reconciliation\ReconciliationRunItemStatus;
+use App\Domain\Reconciliation\ReconciliationRunStatus;
+use App\Domain\Reconciliation\SourceValue;
+use App\Domain\Repository\ReconciliationRunRepository;
+use App\Domain\Repository\SourceDeliveryRepository;
+
+final class InMemoryRunRepository implements ReconciliationRunRepository
+{
+    /** @var array<string,ReconciliationRun> */
+    public array $runs = [];
+    /** @var array<string,ReconciliationRunItem> */
+    public array $items = [];
+    private int $nextItemId = 1;
+
+    public function save(ReconciliationRun $run): void { $this->runs[$run->id()->value()] = $run; }
+    public function findById(ReconciliationRunId $id): ?ReconciliationRun { return $this->runs[$id->value()] ?? null; }
+    public function saveItem(ReconciliationRunItem $item): void
+    {
+        if ($item->id() === null) { $item->assignId($this->nextItemId++); }
+        $this->items[$item->reconciliationRunId()->value() . ':' . $item->sourceValueId()] = $item;
+    }
+    public function findItems(ReconciliationRunId $runId): array
+    {
+        return array_values(array_filter(
+            $this->items,
+            static fn (ReconciliationRunItem $item): bool => $item->reconciliationRunId()->value() === $runId->value(),
+        ));
+    }
+    public function findItem(ReconciliationRunId $runId, int $sourceValueId): ?ReconciliationRunItem
+    {
+        return $this->items[$runId->value() . ':' . $sourceValueId] ?? null;
+    }
+}
+
+final class InMemoryDeliveryRepository implements SourceDeliveryRepository
+{
+    /** @param array<string,SourceDelivery> $deliveries */
+    public function __construct(private array $deliveries) {}
+    public function save(SourceDelivery $delivery): void { $this->deliveries[$delivery->id()->value()] = $delivery; }
+    public function findById(SourceDeliveryId $id): ?SourceDelivery { return $this->deliveries[$id->value()] ?? null; }
+    public function findByExternalDeliveryId(TenantId $tenantId, string $externalDeliveryId): ?SourceDelivery { return null; }
+}
+
+final class InMemorySourceValueRepository implements SourceValueRepository
+{
+    /** @param array<int,SourceValue> $values */
+    public function __construct(private array $values) {}
+    public function save(SourceValue $sourceValue): void { throw new RuntimeException('Not required.'); }
+    public function findById(int $id): ?SourceValue { return $this->values[$id] ?? null; }
+}
+
+function assertRunServiceSame(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)));
+    }
+}
+function assertRunServiceViolation(callable $operation, string $message): void
+{
+    try { $operation(); } catch (DomainInvariantViolation) { return; }
+    throw new RuntimeException($message);
+}
+
+$delivery = SourceDelivery::create(
+    tenantId: new TenantId('source-delivery-test_id'),
+    sourceSystemId: new SourceSystemId('http://digicult.vocnet.org/terminology/ter03115'),
+    deliveryChannel: DeliveryChannel::fromString(DeliveryChannel::BATCH_IMPORT),
+    externalDeliveryId: 'dehioSubjectRheinland_001',
+);
+$otherDelivery = SourceDelivery::create(
+    tenantId: new TenantId('source-delivery-test_id'),
+    sourceSystemId: new SourceSystemId('other'),
+    deliveryChannel: DeliveryChannel::fromString(DeliveryChannel::BATCH_IMPORT),
+);
+
+$source1 = SourceValue::create($delivery->id(), 'Ehem. Pfarrhaus', 'CONCEPT');
+$source1->assignId(1);
+$source2 = SourceValue::create($delivery->id(), 'Flügelaltar', 'CONCEPT');
+$source2->assignId(2);
+$foreign = SourceValue::create($otherDelivery->id(), 'Foreign', 'CONCEPT');
+$foreign->assignId(3);
+
+$runs = new InMemoryRunRepository();
+$service = new ReconciliationRunService(
+    runs: $runs,
+    deliveries: new InMemoryDeliveryRepository([
+        $delivery->id()->value() => $delivery,
+        $otherDelivery->id()->value() => $otherDelivery,
+    ]),
+    sourceValues: new InMemorySourceValueRepository([1 => $source1, 2 => $source2, 3 => $foreign]),
+);
+
+$run = $service->createRun(
+    sourceDeliveryId: $delivery->id(),
+    targetVocabularyUri: 'http://matcult-the.vocnet.org',
+    scopeVocabularyUri: 'http://matcult-the.vocnet.org/00000019',
+    configuration: ['discovery_method' => 'VECTOR_SIMILARITY', 'rerank' => false],
+);
+$items = $service->definePopulation($run, [1, 2]);
+assertRunServiceSame(2, count($items), 'Population must contain exactly two RunItems.');
+assertRunServiceSame(ReconciliationRunItemStatus::PENDING, $items[0]->status(), 'Population starts PENDING.');
+
+assertRunServiceViolation(
+    static fn () => $service->definePopulation($run, [3]),
+    'SourceValue from another SourceDelivery must be rejected.'
+);
+
+$started = new DateTimeImmutable('2026-08-29 21:00:00.000000');
+$service->start($run, $started);
+assertRunServiceSame(ReconciliationRunStatus::RUNNING, $run->status(), 'Run must start after population is defined.');
+$service->markItemCompleted($run, 1);
+$service->markItemFailed($run, 2);
+assertRunServiceSame(ReconciliationRunItemStatus::COMPLETED, $runs->findItem($run->id(), 1)?->status(), 'Item 1 must complete.');
+assertRunServiceSame(ReconciliationRunItemStatus::FAILED, $runs->findItem($run->id(), 2)?->status(), 'Item 2 must fail independently.');
+
+$service->complete($run, new DateTimeImmutable('2026-08-29 21:05:00.000000'));
+assertRunServiceSame(ReconciliationRunStatus::COMPLETED, $run->status(), 'Run may complete with failed items once all items are terminal.');
+
+$emptyRun = $service->createRun($delivery->id(), 'http://matcult-the.vocnet.org');
+assertRunServiceViolation(static fn () => $service->start($emptyRun), 'Run without population must not start.');
+
+echo "QDR-001A ReconciliationRun Service: OK\n";
diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/tests/Domain/Reconciliation/InterpretationGraphRunTest.php qdr001a_work/base/reconcilix_20260829/tests/Domain/Reconciliation/InterpretationGraphRunTest.php
--- qdr001a_pre/reconcilix_20260829/tests/Domain/Reconciliation/InterpretationGraphRunTest.php	1970-01-01 00:00:00.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/tests/Domain/Reconciliation/InterpretationGraphRunTest.php	2026-08-29 20:15:47.612469287 +0000
@@ -0,0 +1,38 @@
+<?php
+declare(strict_types=1);
+
+require_once dirname(__DIR__, 3) . '/src/bootstrap.php';
+
+use App\Domain\Delivery\SourceDeliveryId;
+use App\Domain\Reconciliation\DomainInvariantViolation;
+use App\Domain\Reconciliation\ReconciliationRunId;
+use App\Domain\Reconciliation\SourceValue;
+
+function assertGraphRunSame(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)));
+    }
+}
+
+$source = SourceValue::create(
+    sourceDeliveryId: SourceDeliveryId::fromString('92f3c81d-9cf7-44cc-b16a-a2b4c00ef751'),
+    value: 'Ehem. Pfarrhaus',
+    preferredEntityTypeUri: 'CONCEPT',
+);
+$runA = ReconciliationRunId::fromString('8b08fdd2-9a43-4e2e-a317-60358f96a0ac');
+$runB = ReconciliationRunId::fromString('9aabdc49-089e-4de1-b266-10b787327bdc');
+
+$graphA = $source->createWholeValueGraph('status:created', 'status:initial', reconciliationRunId: $runA);
+$graphB = $source->createWholeValueGraph('status:created', 'status:initial', reconciliationRunId: $runB);
+assertGraphRunSame(2, count($source->interpretationGraphs()), 'Same SourceValue must support graphs from different runs.');
+assertGraphRunSame($runA->value(), $graphA->reconciliationRunId()?->value(), 'Graph A must preserve run id.');
+assertGraphRunSame($runB->value(), $graphB->reconciliationRunId()?->value(), 'Graph B must preserve run id.');
+
+try {
+    $source->createWholeValueGraph('status:created', 'status:initial', reconciliationRunId: $runA);
+    throw new RuntimeException('Equivalent graph within same run must be rejected.');
+} catch (DomainInvariantViolation) {
+}
+
+echo "QDR-001A InterpretationGraph Run Association: OK\n";
diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/tests/Domain/Reconciliation/ReconciliationRunItemTest.php qdr001a_work/base/reconcilix_20260829/tests/Domain/Reconciliation/ReconciliationRunItemTest.php
--- qdr001a_pre/reconcilix_20260829/tests/Domain/Reconciliation/ReconciliationRunItemTest.php	1970-01-01 00:00:00.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/tests/Domain/Reconciliation/ReconciliationRunItemTest.php	2026-08-29 20:15:47.611754407 +0000
@@ -0,0 +1,44 @@
+<?php
+declare(strict_types=1);
+
+require_once dirname(__DIR__, 3) . '/src/bootstrap.php';
+
+use App\Domain\Reconciliation\DomainInvariantViolation;
+use App\Domain\Reconciliation\ReconciliationRunId;
+use App\Domain\Reconciliation\ReconciliationRunItem;
+use App\Domain\Reconciliation\ReconciliationRunItemStatus;
+
+function assertRunItemSame(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)));
+    }
+}
+
+function assertRunItemViolation(callable $operation, string $message): void
+{
+    try {
+        $operation();
+    } catch (DomainInvariantViolation) {
+        return;
+    }
+    throw new RuntimeException($message);
+}
+
+$runId = ReconciliationRunId::fromString('8b08fdd2-9a43-4e2e-a317-60358f96a0ac');
+$item = ReconciliationRunItem::pending($runId, 42);
+assertRunItemSame(null, $item->id(), 'New RunItem must not have a persistence id.');
+assertRunItemSame(42, $item->sourceValueId(), 'SourceValue id must be preserved.');
+assertRunItemSame(ReconciliationRunItemStatus::PENDING, $item->status(), 'New RunItem must be PENDING.');
+$item->assignId(7);
+$item->markCompleted();
+assertRunItemSame(7, $item->id(), 'Assigned id must be preserved.');
+assertRunItemSame(ReconciliationRunItemStatus::COMPLETED, $item->status(), 'RunItem must complete.');
+assertRunItemViolation(static fn () => $item->markFailed(), 'Terminal RunItem must not transition again.');
+
+$failed = ReconciliationRunItem::pending($runId, 43);
+$failed->markFailed();
+assertRunItemSame(ReconciliationRunItemStatus::FAILED, $failed->status(), 'RunItem must fail independently.');
+assertRunItemViolation(static fn () => ReconciliationRunItem::pending($runId, 0), 'Invalid SourceValue id must be rejected.');
+
+echo "QDR-001A ReconciliationRunItem Domain: OK\n";
diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/tests/Domain/Reconciliation/ReconciliationRunTest.php qdr001a_work/base/reconcilix_20260829/tests/Domain/Reconciliation/ReconciliationRunTest.php
--- qdr001a_pre/reconcilix_20260829/tests/Domain/Reconciliation/ReconciliationRunTest.php	1970-01-01 00:00:00.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/tests/Domain/Reconciliation/ReconciliationRunTest.php	2026-08-29 20:15:47.611066579 +0000
@@ -0,0 +1,103 @@
+<?php
+declare(strict_types=1);
+
+require_once dirname(__DIR__, 3) . '/src/bootstrap.php';
+
+use App\Domain\Delivery\SourceDeliveryId;
+use App\Domain\Reconciliation\DomainInvariantViolation;
+use App\Domain\Reconciliation\ReconciliationRun;
+use App\Domain\Reconciliation\ReconciliationRunId;
+use App\Domain\Reconciliation\ReconciliationRunStatus;
+
+function assertRunSame(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)));
+    }
+}
+
+function assertRunViolation(callable $operation, string $message): void
+{
+    try {
+        $operation();
+    } catch (DomainInvariantViolation) {
+        return;
+    }
+    throw new RuntimeException($message);
+}
+
+$deliveryId = SourceDeliveryId::fromString('92f3c81d-9cf7-44cc-b16a-a2b4c00ef751');
+$config = [
+    'discovery_method' => 'VECTOR_SIMILARITY',
+    'adapter_id' => 'vector-qdrant',
+    'rerank' => false,
+];
+$run = ReconciliationRun::create(
+    sourceDeliveryId: $deliveryId,
+    targetVocabularyUri: 'http://matcult-the.vocnet.org',
+    scopeVocabularyUri: 'http://matcult-the.vocnet.org/00000019',
+    configuration: $config,
+);
+
+assertRunSame(ReconciliationRunStatus::CREATED, $run->status(), 'New run must be CREATED.');
+assertRunSame(null, $run->startedAt(), 'CREATED run must not have startedAt.');
+assertRunSame(null, $run->finishedAt(), 'CREATED run must not have finishedAt.');
+assertRunSame($config, $run->configuration(), 'Configuration snapshot must be preserved.');
+assertRunSame('http://matcult-the.vocnet.org', $run->targetVocabularyUri(), 'Target vocabulary must be preserved.');
+assertRunSame('http://matcult-the.vocnet.org/00000019', $run->scopeVocabularyUri(), 'Scope vocabulary must be preserved.');
+ReconciliationRunId::fromString($run->id()->value());
+
+$startedAt = new DateTimeImmutable('2026-08-29 20:00:00.123456');
+$finishedAt = new DateTimeImmutable('2026-08-29 20:05:00.654321');
+$run->start($startedAt);
+assertRunSame(ReconciliationRunStatus::RUNNING, $run->status(), 'start() must move run to RUNNING.');
+assertRunSame($startedAt, $run->startedAt(), 'start() must preserve timestamp.');
+$run->markCompleted($finishedAt);
+assertRunSame(ReconciliationRunStatus::COMPLETED, $run->status(), 'markCompleted() must move run to COMPLETED.');
+assertRunSame($finishedAt, $run->finishedAt(), 'markCompleted() must preserve timestamp.');
+
+assertRunViolation(static fn () => $run->start(), 'COMPLETED run must not restart.');
+assertRunViolation(static fn () => $run->markFailed(), 'COMPLETED run must not fail afterwards.');
+assertRunViolation(
+    static fn () => ReconciliationRun::create($deliveryId, '   '),
+    'Empty target vocabulary must be rejected.'
+);
+assertRunViolation(
+    static fn () => ReconciliationRun::create($deliveryId, 'http://example.org/voc', '   '),
+    'Empty scope vocabulary must be rejected when provided.'
+);
+
+$failed = ReconciliationRun::create($deliveryId, 'http://matcult-the.vocnet.org');
+$failedAt = new DateTimeImmutable('2026-08-29 20:10:00.000000');
+$failed->markFailed($failedAt);
+assertRunSame(ReconciliationRunStatus::FAILED, $failed->status(), 'CREATED run may fail as a whole.');
+assertRunSame($failedAt, $failed->startedAt(), 'Failure before start must establish a deterministic startedAt.');
+assertRunSame($failedAt, $failed->finishedAt(), 'Failure before start must establish finishedAt.');
+
+$rehydrated = ReconciliationRun::rehydrate(
+    id: ReconciliationRunId::fromString('8b08fdd2-9a43-4e2e-a317-60358f96a0ac'),
+    sourceDeliveryId: $deliveryId,
+    targetVocabularyUri: 'http://matcult-the.vocnet.org',
+    scopeVocabularyUri: null,
+    status: ReconciliationRunStatus::RUNNING,
+    configuration: ['discovery_method' => 'LEXICAL'],
+    startedAt: $startedAt,
+    finishedAt: null,
+);
+assertRunSame(ReconciliationRunStatus::RUNNING, $rehydrated->status(), 'RUNNING run must rehydrate.');
+
+assertRunViolation(
+    static fn () => ReconciliationRun::rehydrate(
+        id: ReconciliationRunId::fromString('8b08fdd2-9a43-4e2e-a317-60358f96a0ac'),
+        sourceDeliveryId: $deliveryId,
+        targetVocabularyUri: 'http://matcult-the.vocnet.org',
+        scopeVocabularyUri: null,
+        status: ReconciliationRunStatus::COMPLETED,
+        configuration: [],
+        startedAt: null,
+        finishedAt: $finishedAt,
+    ),
+    'Inconsistent persisted lifecycle state must be rejected.'
+);
+
+echo "QDR-001A ReconciliationRun Domain: OK\n";
diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/tests/Migration/ReconciliationRunMigrationTest.php qdr001a_work/base/reconcilix_20260829/tests/Migration/ReconciliationRunMigrationTest.php
--- qdr001a_pre/reconcilix_20260829/tests/Migration/ReconciliationRunMigrationTest.php	1970-01-01 00:00:00.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/tests/Migration/ReconciliationRunMigrationTest.php	2026-08-29 20:15:47.613097139 +0000
@@ -0,0 +1,35 @@
+<?php
+declare(strict_types=1);
+
+require_once dirname(__DIR__, 2) . '/src/bootstrap.php';
+
+$migrationPath = dirname(__DIR__, 2) . '/database/migrations/0006_reconciliation_run_foundation.sql';
+$sql = file_get_contents($migrationPath);
+if ($sql === false) {
+    throw new RuntimeException('QDR-001A migration could not be read.');
+}
+
+$requiredFragments = [
+    'CREATE TABLE `reconciliation_run`',
+    'CREATE TABLE `reconciliation_run_item`',
+    '`target_vocabulary_uri`',
+    '`scope_vocabulary_uri`',
+    '`configuration_json` JSON NOT NULL',
+    "'CREATED', 'RUNNING', 'COMPLETED', 'FAILED'",
+    "'PENDING', 'COMPLETED', 'FAILED'",
+    'UNIQUE KEY `uq_reconciliation_run_item_population` (`reconciliation_run_id`, `source_value_id`)',
+    'ADD COLUMN `reconciliation_run_id` VARCHAR(36) NULL',
+    'DROP INDEX `uq_interpretation_graph_span_version`',
+    'UNIQUE KEY `uq_interpretation_graph_run_span_version`',
+];
+foreach ($requiredFragments as $fragment) {
+    if (!str_contains($sql, $fragment)) {
+        throw new RuntimeException('QDR-001A migration is missing: ' . $fragment);
+    }
+}
+
+if (str_contains($sql, '`discovery_method`')) {
+    throw new RuntimeException('discovery_method must not be a mandatory reconciliation_run column in QDR-001A.');
+}
+
+echo "QDR-001A Migration Contract: OK\n";
diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/tests/Migration/SchemaAndMigrationsTest.php qdr001a_work/base/reconcilix_20260829/tests/Migration/SchemaAndMigrationsTest.php
--- qdr001a_pre/reconcilix_20260829/tests/Migration/SchemaAndMigrationsTest.php	2026-08-01 09:59:52.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/tests/Migration/SchemaAndMigrationsTest.php	2026-08-29 20:16:06.847410655 +0000
@@ -28,7 +28,7 @@
 $repository = new MigrationFileRepository($root . '/database/migrations');
 $migrations = $repository->all();
 
-assertMigrationSame(4, count($migrations), 'Baseline, candidate-semantics, source-delivery, and source-value delivery-reference migrations are expected.');
+assertMigrationSame(6, count($migrations), 'Six migrations through QDR-001A are expected.');
 $baseline = $migrations[0];
 assertMigrationSame('0001', $baseline->version(), 'Baseline version must be 0001.');
 assertMigrationSame(
@@ -74,6 +74,33 @@
     'SourceValue delivery-reference migration must introduce source_delivery_id.',
 );
 
+$contextRole = $migrations[4];
+assertMigrationSame('0005', $contextRole->version(), 'ContextItem role migration version must be 0005.');
+assertMigrationSame(
+    '0005_context_item_context_role.sql',
+    $contextRole->filename(),
+    'ContextItem role migration filename must follow the migration convention.',
+);
+assertMigrationTrue(
+    str_contains($contextRole->sql(), '`context_role` VARCHAR(64) NULL'),
+    'ContextItem role migration must introduce context_role.',
+);
+
+$reconciliationRun = $migrations[5];
+assertMigrationSame('0006', $reconciliationRun->version(), 'ReconciliationRun migration version must be 0006.');
+assertMigrationSame(
+    '0006_reconciliation_run_foundation.sql',
+    $reconciliationRun->filename(),
+    'ReconciliationRun migration filename must follow the migration convention.',
+);
+assertMigrationTrue(
+    str_contains($reconciliationRun->sql(), 'CREATE TABLE `reconciliation_run`'),
+    'QDR-001A migration must create reconciliation_run.',
+);
+assertMigrationTrue(
+    str_contains($reconciliationRun->sql(), 'CREATE TABLE `reconciliation_run_item`'),
+    'QDR-001A migration must create reconciliation_run_item.',
+);
 
 $sql = $baseline->sql();
 assertMigrationTrue(!preg_match('/^\s*CREATE\s+DATABASE\b/mi', $sql), 'Migration must not create a database.');
diff -ruN '--exclude=local' qdr001a_pre/reconcilix_20260829/tests/tests.md qdr001a_work/base/reconcilix_20260829/tests/tests.md
--- qdr001a_pre/reconcilix_20260829/tests/tests.md	2026-08-29 15:27:36.000000000 +0000
+++ qdr001a_work/base/reconcilix_20260829/tests/tests.md	2026-08-29 20:17:13.870834346 +0000
@@ -68,3 +68,14 @@
 
 
 - php tests/Application/Delivery/SourceDeliveryJsonImporterTest.php
+
+## QDR-001A – Reconciliation Run Foundation
+
+```bash
+php tests/Domain/Reconciliation/ReconciliationRunTest.php
+php tests/Domain/Reconciliation/ReconciliationRunItemTest.php
+php tests/Domain/Reconciliation/InterpretationGraphRunTest.php
+php tests/Application/Reconciliation/ReconciliationRunServiceTest.php
+php tests/Migration/ReconciliationRunMigrationTest.php
+php tests/Migration/SchemaAndMigrationsTest.php
+```
