## Audit-Ergebnis Repository geprüft auf **SHA `a2053718d5eb077e1b44b7923e391f1ef9a1bfe3`**, Working Tree unverändert und sauber. Es wurden keine Patientendaten, Dokumentnamen oder produktiven Dateien gelesen. ### Vorhandene, wiederverwendbare Basis - **6I-A-Schema** - `document_processing`: technische Verarbeitung, Review- und Transferstatus - `document_text_versions`: unveränderliche Extraktions-/Korrekturversionen - `document_pages`: Seiten, Hashes und Wiederholungsstatus - `document_candidates`: medizinische Kandidaten im Staging - `document_review_log`: aktionsbezogene Idempotenz/Audit - `health_document_machine_fts`: getrennte, ungeprüfte Suchschicht - **6I-B** - `document_media` sowie sichere Preview-/Proxy-Routen sind für Foto-/Video-Dokumente wiederverwendbar. - **APIs** - `GET /api/v1/document-review-queue` - `GET /api/v1/documents/{id}/review` - `GET /api/v1/documents/{id}/candidates` - `GET /api/v1/documents/{id}/compare` - `GET /api/v1/documents/{id}/extracted-preview` - geprüfte Detail-, FTS- und Reportpfade bleiben korrekt getrennt. - **Worker** - HTTP-Prozess schreibt Aktionen nur in die private Queue; DB-Mutationen erfolgen im Action-Worker. - `candidate_decision` verändert derzeit nur `document_candidates`, nicht `laborwerte`, Medikamente oder andere kanonische medizinische Tabellen. ## Wesentliche Lücken vor 6I-C 1. **Transfer ist nur als Status vorgesehen** - `partially_transferred` und `reviewed_transferred` existieren im CHECK-Constraint, werden aber nicht durch einen Transfervertrag implementiert. - Es fehlen Snapshot, Vorschau, Zielvertrag, Stale-State-Prüfung und Transferhistorie. 2. **Idempotenz ist nicht fachlich stabil** - `document_review_log.action_id` entspricht dem zufälligen Queue-Dateinamen. - Derselbe Browserauftrag mit neuer Queue-Datei erhält eine neue ID und ist daher kein stabil idempotenter Request. 3. **Candidate-ID-Kollision** - `candidate_rows()` bildet die ID nur aus Typ, Wert, Einheit, Seite, Abschnitt und Kontext, nicht aus Dokument/Textversion. - Da `document_candidates.id` globaler Primärschlüssel ist, können gleiche Kandidaten verschiedener Dokumente durch `INSERT OR IGNORE` verschwinden. 4. **Duplikate sind nicht entscheidbar** - Exakte Datei-Duplikate setzen nur `duplicate_document_id`; es gibt keinen expliziten Beschluss wie „gleicher Import“, „wiederholter Bericht“, „inhaltlich verwandt“ oder „kein Duplikat“. - Ein exaktes Duplikat beendet die Extraktion früh und bleibt ohne geregelten Abschlussweg. 5. **Wiederholungserkennung ist zu grob** - „Identical“ basiert teilweise auf Jaccard-Tokenmengen und `similarity == 1`; unterschiedliche Reihenfolge oder Multiplizität kann dadurch fälschlich identisch wirken. - Der bereits gespeicherte `section_hash` sollte die einzige Grundlage für `identical` sein. - Nach `text_correction` werden alte `repetition_status`/Vergleichsbezüge kopiert statt neu berechnet. 6. **Stale-Review-Risiko** - Review-Aktionen binden nicht an erwartete Textversion, Candidate-Version oder Dokumentzustand. - Eine Vorschau kann daher nach zwischenzeitlicher Korrektur unbemerkt veraltet sein. 7. **Authentifizierungsgrenze** - Dokument-POSTs prüfen Origin und CSRF, verlangen aber nicht ausdrücklich die authentifizierte V5-Browsersession. Ein CSRF-Cookie allein sollte keine Dokumentmutation autorisieren. - Alle 6I-C-Aktionen müssen zusätzlich `browser_session_is_authenticated()` verlangen. 8. **Worker führt DDL aus** - `apply_document_import()` ruft `apply_document_schema()` und `apply_media_schema()` auf. - Für 6I-C sollte der Worker ausschließlich `assert_schema()` verwenden; Migrationen müssen vorher explizit copy-first ausgeführt werden. ## Minimal-additives 6I-C-Design ### 1. Bestehende Tabellen eng ergänzen `document_candidates` additiv erweitern: ```sql ALTER TABLE document_candidates ADD COLUMN source_text_version INTEGER; ALTER TABLE document_candidates ADD COLUMN candidate_fingerprint TEXT; ALTER TABLE document_candidates ADD COLUMN candidate_revision INTEGER NOT NULL DEFAULT 1; ``` Neuer Index: ```sql CREATE INDEX idx_document_candidate_fingerprint ON document_candidates(candidate_type, candidate_fingerprint); ``` Regel: - `id`: domänenseparierter Digest aus `document_id/intake_id + text_version + candidate_fingerprint` - `candidate_fingerprint`: dokumentunabhängiger Digest für Wiederholungsvergleich - Neue Kandidaten niemals mehr über eine dokumentunabhängige globale ID identifizieren. `document_processing` additiv: ```sql ALTER TABLE document_processing ADD COLUMN reconciliation_revision INTEGER NOT NULL DEFAULT 0; ``` Diese Revision wird bei Textkorrektur, Retry, Candidate-Entscheidung oder Duplikatentscheidung erhöht und dient als Optimistic-Locking-Grenze. ### 2. Explizite Duplikat-/Wiederholungsentscheidung ```sql CREATE TABLE document_duplicate_reviews ( id TEXT PRIMARY KEY, document_id INTEGER NOT NULL REFERENCES dokumente(id), compared_document_id INTEGER NOT NULL REFERENCES dokumente(id), relation TEXT NOT NULL CHECK(relation IN ( 'exact_file_duplicate', 'repeated_document', 'related_document', 'not_duplicate' )), decision TEXT NOT NULL CHECK(decision IN ( 'pending', 'confirmed', 'rejected' )), source_sha256 TEXT NOT NULL, compared_sha256 TEXT NOT NULL, expected_revision INTEGER NOT NULL, idempotency_key TEXT NOT NULL UNIQUE, payload_hash TEXT NOT NULL, decided_at TEXT, created_at TEXT NOT NULL, UNIQUE(document_id, compared_document_id) ); ``` Wichtig: - Datei-Hashgleichheit erzeugt nur `exact_file_duplicate/pending`, niemals automatische Verwerfung. - „Wiederholter Bericht“ und „inhaltlich verwandt“ sind manuelle Entscheidungen. - Identische Seiten werden ausschließlich per exakt gleichem normalisiertem `section_hash` klassifiziert. - Near-match bleibt Hinweis; daraus folgt weder Merge noch medizinische Übernahme. ### 3. Immutable Reconciliation-/Transferplan ```sql CREATE TABLE document_reconciliation_batches ( id TEXT PRIMARY KEY, document_id INTEGER NOT NULL REFERENCES dokumente(id), source_text_version INTEGER NOT NULL, expected_revision INTEGER NOT NULL, contract_version TEXT NOT NULL, preview_hash TEXT NOT NULL, request_hash TEXT NOT NULL, idempotency_key TEXT NOT NULL UNIQUE, status TEXT NOT NULL CHECK(status IN ( 'prepared', 'staged', 'cancelled', 'stale', 'failed' )), created_at TEXT NOT NULL, staged_at TEXT ); ``` ```sql CREATE TABLE document_reconciliation_items ( id TEXT PRIMARY KEY, batch_id TEXT NOT NULL REFERENCES document_reconciliation_batches(id), candidate_id TEXT NOT NULL REFERENCES document_candidates(id), candidate_snapshot_hash TEXT NOT NULL, candidate_type TEXT NOT NULL, disposition TEXT NOT NULL CHECK(disposition IN ( 'stage_new', 'already_present', 'conflict', 'unsupported', 'skip' )), destination_contract TEXT NOT NULL, proposed_payload_json TEXT, target_fingerprint TEXT, status TEXT NOT NULL CHECK(status IN ( 'previewed', 'staged', 'skipped', 'stale' )), UNIQUE(batch_id, candidate_id) ); ``` ```sql CREATE TABLE document_reconciliation_events ( action_id TEXT PRIMARY KEY, batch_id TEXT NOT NULL REFERENCES document_reconciliation_batches(id), idempotency_key TEXT NOT NULL UNIQUE, event_type TEXT NOT NULL CHECK(event_type IN ( 'prepare', 'stage', 'cancel', 'duplicate_decision' )), payload_hash TEXT NOT NULL, processed_at TEXT NOT NULL ); ``` ### 4. Bedeutung von „Transfer“ in 6I-C Der kleinste sichere Scope: - Transfer bedeutet zunächst ausschließlich **immutable Übernahme in `document_reconciliation_items` mit Status `staged`**. - Keine 6I-C-Funktion schreibt in: - `laborwerte` - `medikamente` - `medication_administrations` - `health_events` - `symptom_log` - andere kanonische medizinische Tabellen. - `document_processing.transfer_status` kann anschließend abgeleitet werden: - offene/unstaged bestätigte Kandidaten → `candidates_available` - Teilmenge staged → `partially_transferred` - alle entschiedenen, unterstützten Kandidaten staged → `reviewed_transferred` - „Transferred“ muss in API/UI ausdrücklich als **„in geprüftes Übergabe-Staging übertragen“** bezeichnet werden, nicht als „in Gesundheitsakte gespeichert“. Ein späterer kanonischer Import sollte ein eigener Sprint und eine getrennte, explizit bestätigte CLI/Worker-Aktion mit zieltypspezifischen Adaptern sein. Kein generischer `table/column`-Transfervertrag. ## Funktionen ### Neu ```python candidate_identity( intake_id, text_version, candidate_type, value, unit, page, section, context ) -> tuple[candidate_id, candidate_fingerprint] ``` ```python recompute_page_repetition( connection, document_id, text_version ) ``` - `identical` nur bei Hashgleichheit - Near-match nur als Hinweis - nach Retry und Textkorrektur erneut ausführen ```python build_reconciliation_preview( connection, document_id, candidate_ids, expected_text_version, expected_revision ) -> ReconciliationPreview ``` - ausschließlich feste, allowlist-basierte Lesefunktionen - Ergebnisse: `stage_new`, `already_present`, `conflict`, `unsupported` - kein Schreiben - deterministischer `preview_hash` - unvollständig strukturierte Labor-/Referenzkandidaten standardmäßig `unsupported` ```python stage_reconciliation_batch( connection, payload, action_id ) ``` - `BEGIN IMMEDIATE` - Idempotency-Key plus Payload-Hash prüfen - aktuelle Revision/Textversion gegen Preview prüfen - Preview serverseitig neu berechnen - Batch/Items immutable einfügen - nur Staging- und Auditstatus aktualisieren - keine kanonischen medizinischen Writes ```python apply_duplicate_decision( connection, payload, action_id ) ``` - Hashpaar, erwartete Revision und Richtung prüfen - niemals Dokument oder Original automatisch löschen - „exact duplicate“ darf höchstens Review/Queue-Zustand abschließen ### Wiederverwenden - `validate_document_action()` als Muster, aber 6I-C besser mit eigener Payload `document_reconciliation` - `_canonical_json()` und SHA-256-Payloadhash - private Action Queue und `load_action()` mit Duplicate-Key-Abweisung - `connect_read_only()`, `parse_query()`, opaque Dokumentauflösung - `document_text_versions`, `document_pages`, `document_candidates` - bestehende Original-/Preview-/Proxy-Routen aus 6I-A/B - `document_review_log` weiterhin für Seiten-, Text- und Contentreview; Transferereignisse separat halten ## Routen ### Read-only ```text GET /api/v1/documents/{opaque}/reconciliation ``` Antwort: - aktuelle Textversion/Revision - bestätigte Kandidaten - `already_present/conflict/new/unsupported` - Wiederholungs-/Duplikathinweise - deterministischer `preview_hash` - keine numerischen DB-IDs, Pfade, Hashes oder Dateinamen ```text GET /api/v1/document-reconciliations/{batch_id} ``` - Batchstatus, Items, Stale-Status und Stagingresultat - opaque Batch-ID - strikt `no-store` Bestehende `/review`, `/compare` und `/candidates` weiterverwenden, aber `/compare` um explizite Hash-/Near-match-Semantik ergänzen. ### Write/Queue ```text POST /health-actions/document-reconciliation ``` Payload v1: ```json { "version": 1, "action": "document_reconciliation", "operation": "stage", "document_id": "doc_…", "candidate_ids": ["cand_…"], "expected_text_version": 2, "expected_revision": 5, "preview_hash": "…", "idempotency_key": "…" } ``` Zusätzliche Operationen: - `duplicate_decision` - `cancel_batch` Kein `apply_to_laborwerte` oder generisches kanonisches Ziel in 6I-C. Optional: ```text GET /api/v1/document-reconciliations/status/{idempotency_key} ``` Damit „queued“, „staged“, „stale“ und „rejected“ unterscheidbar sind. Der bestehende einmalige Redirect-Receipt bestätigt weiterhin nur das Queuing. ## Security- und Medical-Safety-Pitfalls - Dokument-POSTs zusätzlich zu Origin/CSRF zwingend an eine gültige authentifizierte Browsersession binden. - Idempotency-Key muss clientstabil und domänensepariert sein; gleicher Key mit anderem Payload-Hash ist ein harter Konflikt. - Preview-Hash allein genügt nicht: Textversion, Candidate-Snapshots und `reconciliation_revision` mitprüfen. - Keine Tabellen-/Spaltennamen, SQL-Fragmente oder Zieltypen aus Clientdaten übernehmen. - JSON-Snapshots über einen versionierten, typabhängigen Validator erzeugen; keine ungeprüften freien Payloads. - Unverknüpfte Laborwert-/Referenzbereich-Kandidaten niemals automatisch zusammenführen. - `already_present` nur bei striktem typabhängigem Schlüssel; fuzzy/semantische Ähnlichkeit ergibt höchstens `conflict`. - Keine Duplikatlöschung und kein Umschreiben von Originalpfaden; gemeinsam genutzte Originalbytes benötigen explizite Referenzzählung, bevor später überhaupt Cleanup erwogen wird. - Mutable Metadaten verändern aktuell öffentliche `api-document-*`-IDs. Reconciliation intern an `document_processing.intake_id` und Snapshotrevision binden. - Worker darf in 6I-C keine DDL ausführen; Migration separat copy-first, Worker nur `assert_schema()`. - Queue- und Receipt-Dateien weiter mit `0700/0600`, `O_NOFOLLOW`, Größenlimit, Duplicate-Key-Abweisung und atomischem Rename behandeln. - Ungeprüfte Preview-/Candidate-Inhalte dürfen weiterhin nicht in FTS, Arztbericht, Zusammenfassungen oder kanonische APIs gelangen. ## Abschluss - **Durchgeführt:** Quellcode-Audit von 6I-A/B-Schema, Extraktion/Reconciliation, Read-API, HTTP-Grenze, Migration und Action-Worker. - **Ergebnis:** Minimaler additiver 6I-C-Vertrag mit expliziter Duplikatentscheidung, versionierter Vorschau, stabiler Idempotenz und immutable Übergabe-Staging. - **Geänderte/erzeugte Dateien:** keine. - **Probleme:** keine Umgebungsprobleme; Repository war auf dem erwarteten Commit und unverändert.