Explorer
/proc/422/root/tmp/build_baseline_report.py
← Zurück ↓ Download
import json, html
from pathlib import Path
D=json.load(open('/tmp/wissenspipeline_baseline_audit.json'))
DB=D['db']; M=D['fk_matrix']; G=D['fk_groups']; Q=D['queue_open']
lines=[]
def add(x=''): lines.append(x)
add('# Wissenspipeline – technische Baseline: FK, Queue-Recovery und Writer-Ownership')
add(); add('**Erhebungszeitpunkt:** 2026-09-02, 20:15 UTC  '); add('**Modus:** ausschließlich read-only; keine produktiven Änderungen ausgeführt.')
add(); add('## 1. Executive Summary'); add()
add('- Die Live-DB ist `/opt/struktur/youtube-research/knowledge.db`, 155.013.120 Byte, SQLite 3.45.1, WAL. `PRAGMA integrity_check` ergibt `ok`; `PRAGMA foreign_keys` ergibt `0`.')
add('- `PRAGMA foreign_key_check` liefert exakt **485 Verstöße**: **279 in `chapters` → `videos`** und **206 in `video_links` → `videos`**. Alle geprüften Parent-Schlüssel fehlen in `videos`; die technische Ursache (Legacy, Löschung, Migration oder Writerfehler) ist aus der FK-Prüfung allein nicht unterscheidbar.')
add('- Die aktuelle Queue enthält 1.291 Zeilen: 1.276 `done`, 2 `processing`, 9 `queued`, 4 `retry_wait`. Es gibt keine `failed_permanent`-Zeile. Die 2 `processing`-Leases sind abgelaufen; die Zeilen hängen.')
add('- AA-049 ist als 5-Minuten-Timer aktiv. Der Eventlog enthält bis 20:15 UTC wiederholte `NO_DUE_YOUTUBE`-Läufe. Das belegt den Scheduler, nicht die vollständige Abarbeitung der Queue.')
add('- Der E2E-Worker und die YouTube-FastAPI schreiben weiterhin direkt in fachliche SQLite-Tabellen. Der YouTube-Graphiti-Import ist auf den AA-049-Single-Writer konzentriert; der generische Obsidian-Timer bleibt ein unabhängiger schreibfähiger Parallelpfad.')
add('- Es wurden **keine** FK-Reparaturen, Requeues, Lock-Löschungen, Statusänderungen, Writer-/Timer-Deaktivierungen, Schemaänderungen oder Graph-/Neo4j-Writes ausgeführt.')
add(); add('### Verifizierte Live-Abfragen'); add(); add('```sql'); add('PRAGMA integrity_check;'); add('PRAGMA foreign_key_check;'); add('PRAGMA foreign_keys;'); add("SELECT graphiti_status, COUNT(*) FROM graphiti_import_queue GROUP BY graphiti_status;"); add("SELECT * FROM graphiti_import_queue WHERE graphiti_status <> 'done' ORDER BY id;"); add('```'); add()
add('## 2. Prüfrahmen und unveränderte Live-Basis'); add()
add('| Feld | Ergebnis |'); add('|---|---|'); add(f"| DB | `{DB['path']}` |"); add(f"| Größe | {DB['size']} Byte |"); add(f"| SQLite | {DB['sqlite']} |"); add(f"| Journal | `{DB['journal']}` |"); add(f"| FK-Connection-Default dieser Audit-Verbindung | `{DB['foreign_keys']}` |"); add(f"| `integrity_check` | `{DB['integrity']}` |"); add('| FK-Verstöße | 485 |'); add('| Queue-Zeilen | 1.291 |'); add('| Graphiti-Episoden live | 3.192 über `/episodes/inventory` |'); add()
add('Die Audit-Verbindung wurde nach der Abfrage geschlossen. Es gab keine Transaktion mit Schreiboperation, kein `VACUUM`, kein `REINDEX`, kein Checkpoint und keinen HTTP-POST auf `/episodes`.')
add()
add('## 3. Vollständige FK-Verstoß-Matrix'); add()
add('### 3.1 Technische Gesamtklassifikation'); add()
add('| Tabelle | Anzahl Verstöße | Referenz | Child-FK | Parent-Datensatz | Technisch belegte Ursache | Datenverlust erkennbar | Reparierbarkeit |'); add('|---|---:|---|---|---|---|---|---|'); add('| `chapters` | 279 | `videos(id)` | `chapters.video_id` | fehlt für alle geprüften Werte | Parent fehlt; Root Cause nicht aus FK-Prüfung ableitbar | Nein, nur verwaiste Child-Zeilen nachgewiesen | Parent-/Video-Zuordnung muss separat rekonstruiert werden |'); add('| `video_links` | 206 | `videos(id)` | `video_links.video_id` | fehlt für alle geprüften Werte | Parent fehlt; Root Cause nicht aus FK-Prüfung ableitbar | Nein, nur verwaiste Child-Zeilen nachgewiesen | URL-/Video-Zuordnung muss separat rekonstruiert werden |'); add()
add('Die Anforderungskategorien A–G werden bewusst nicht spekulativ befüllt: **A (Parent fehlt tatsächlich) ist der direkt beobachtete Zustand**. Ob zusätzlich B (Legacy), C (Deletion ohne Cascade), D (Writer), E (Migration), F (tolerierter Altbestand) oder G (andere Ursache) zutrifft, ist mit `foreign_key_check` allein nicht beweisbar. Eine Zuordnung zu B–G wäre eine Vermutung. Die Datenstruktur zeigt allerdings, dass beide betroffenen Tabellen `ON DELETE NO ACTION` verwenden; das ist ein technischer Risikofaktor, aber kein Beweis für die konkrete Entstehung dieser 485 Zeilen.')
add(); add('### 3.2 Abfrageergebnis nach Constraint'); add(); add('| Tabelle | Parent-Tabelle | FK-ID | Constraint | Anzahl |'); add('|---|---|---:|---|---:|'); add('| `chapters` | `videos` | 0 | `chapters.video_id → videos.id`, `ON DELETE NO ACTION` | 279 |'); add('| `video_links` | `videos` | 0 | `video_links.video_id → videos.id`, `ON DELETE NO ACTION` | 206 |'); add()
add('Es gibt 19 unterschiedliche verwaiste `chapters.video_id`-Werte und 27 unterschiedliche verwaiste `video_links.video_id`-Werte. Die Child-PK-Bereiche der Verstöße sind `chapters.id` 219–1443 und `video_links.id` 151–1189. Diese Werte und die vollständige Einzelmatrix folgen unten.')
add()
add('### 3.3 Vollständige Einzelmatrix (485 Zeilen)'); add(); add('| Nr. | Tabelle | Child-PK/rowid | FK-Spalte | Fremdschlüsselwert | Referenzierte Tabelle/Spalte | Parent fehlt |'); add('|---:|---|---:|---|---:|---|---|')
for i,x in enumerate(M,1):
    rv=x.get('row_values') or {}; val=rv.get('video_id','NULL'); pk=rv.get('id',x['rowid'])
    add(f"| {i} | `{x['table']}` | {pk}/{x['rowid']} | `video_id` | {val} | `{x['parent_table']}.id` | {x['parent_missing']} |")
add()
add('Die Matrix wurde direkt aus `PRAGMA foreign_key_check` und `PRAGMA foreign_key_list(chapters/video_links)` erzeugt. `rowid` und Primärschlüssel sind bei diesen gewöhnlichen SQLite-Tabellen identisch. Für jede Zeile wurde zusätzlich ein read-only Lookup `SELECT 1 FROM videos WHERE id=?` durchgeführt; kein Parent wurde gefunden.')
add()
add('## 4. FK-Enforcement und Öffnungspfade'); add()
add('| Komponente | Öffnet/benutzt DB | FK-Verhalten im Code | Schreibfähigkeit | Befund bei späterem Enforcement |'); add('|---|---|---|---|---|')
add('| `youtube-research.service` / `app.py` | ja, über `db.py` und App-Verbindungen | `db.py:35` setzt `PRAGMA foreign_keys=ON`; App enthält weitere direkte `sqlite3.connect`-Pfade, deren Session-PRAGMA nicht überall zentral nachgewiesen ist | ja | zunächst Pfad-by-Pfad testen; bestehende Orphan-Daten würden nicht rückwirkend repariert, neue ungültige Writes würden scheitern |')
add('| `e2e_worker.py` | ja, feste DB | `e2e_worker.py:22` setzt explizit `foreign_keys=ON` | ja | bereits strenger als globale Audit-Verbindung; Insert-/Delete-Reihenfolge und Trigger testen |')
add('| `youtube_graphiti_queue.py` | ja | setzt `busy_timeout`, aber kein `foreign_keys=ON` | ja, Queue und optional Schema-/Index-Setup | Queue-Pfad könnte mit FK-ON anders fehlschlagen, fachliche Queue-Tabelle hat aber keine sichtbare FK auf `videos` |')
add('| Registry-/Reconciliation-Code | ja, je nach Pfad | nicht einheitlich; Staging-/Legacy-Code und Produktionspfad sind nicht identisch | ja | vor Aktivierung alle produktiven Entry-Points separat testen |')
add('| Legacy-/Batch-Skripte | teilweise | heterogen; viele direkte `sqlite3.connect`-/Commit-Pfade | teils ja | nicht global aktivieren, bevor keine Laufzeit-/Dry-Run-Matrix existiert |')
add('| `content-extraction` | eigene CE-DB; liest YouTube-Quelle | produktiver Dienst ist als read-only Analyse beschrieben | laut Unit nicht kanonischer Knowledge-DB-Writer | kein direkter FK-Befund für Knowledge-DB |')
add()
add('### Nachweisbare aktive DB-Prozesse'); add(); add('- `youtube-research.service`: active/running, PID 4001613, `/opt/struktur/youtube-research/venv/bin/uvicorn app:app`, gestartet 2026-09-02 18:09:05 UTC.')
add('- `youtube-research-e2e-worker.service`: active/running, PID 2116675, `/opt/struktur/youtube-research/venv/bin/python /opt/struktur/youtube-research/e2e_worker.py`, gestartet 06:12:44 UTC.')
add('- AA-049-Queue-Service: Oneshot-Aufruf des Queue-Workers; Timer aktiv, daher periodischer DB-/Graph-Schreibpfad.')
add('- Generischer Obsidian-Timer: aktiv/waiting; konkreter Service-Unit-Inhalt war wegen Dateirechten nicht lesbar, der Timer selbst und Queue-Wirkung sind über systemd/DB belegt.')
add('- Kein aktiver `knowledge-pipeline-aggregator.service` nachgewiesen: systemd meldet `not-found`.')
add()
add('### Voraussetzungen für spätere sichere Aktivierung'); add('1. Die 485 Orphans read-only einzeln klassifizieren und Rekonstruktionsbelege sichern.'); add('2. Jeden produktiven Connection-Entry-Point identifizieren und `foreign_keys=ON` verbindlich setzen.'); add('3. Testdatenbankkopie mit FK-ON sowie realen Insert-/Update-/Delete-Sequenzen testen.'); add('4. Erst danach kontrollierte Aktivierung pro Dienst, mit Backup, Rollback und Post-Change-FK-Audit.'); add()
add('## 5. Queue-Baseline'); add()
add('| Status | Zeilen |'); add('|---|---:|');
for x in D['queue_counts']: add(f"| `{x['graphiti_status']}` | {x['n']} |")
add('| **gesamt** | **1.291** |'); add(); add('Offene Gesamtmenge: **15 Zeilen**. Davon 2 `processing`, 9 `queued`, 4 `retry_wait`. Die älteste offene Zeile ist ID 347 vom 2026-07-18. Der Live-Eventlog `/opt/struktur/reports/aa049-worker-events.jsonl` hat 2.301 Zeilen; der letzte Eintrag bis 20:15 UTC ist `NO_DUE_YOUTUBE`.')
add()
add('## 6. Detailanalyse Queue 1092'); add();
for qid in (1092,1268,347):
 x=D['queue_'+str(qid)]
 add(f'### Queue-ID {qid}'); add(); add('| Feld | Wert |'); add('|---|---|')
 fields=['source_type','video_id','obsidian_path','graphiti_status','created_at','updated_at','locked_at','lock_owner','lease_expires_at','heartbeat_at','graphiti_attempts','attempt_count','next_attempt_at','last_attempt_at','last_error_class','last_http_status','last_error_message','graphiti_episode_id','source_version_id','import_identity','reconciliation_record_id','remote_outcome','unknown_since','last_lookup_at','lookup_attempt_count','post_request_id','completed_at']
 for f in fields:
  v=x.get(f); sv=str(v).replace('|','\\|').replace('\n',' ')
  if f in ('last_error_message',): sv=sv[:900]
  add(f'| `{f}` | `{sv}` |')
 add()
 if qid in (1092,1268):
  add('**Graphiti-Nachweis:** Die exakte `import_identity` wurde im vollständigen Live-Inventar (`GET /episodes/inventory`, 3.192 Episoden) nicht gefunden. Der vorhandene Endpoint `/episodes/check` liefert wegen seiner zusätzlichen unspezifischen Namens-/Key-Suche 20 allgemeine Treffer, aber keinen exakten Treffer; diese 20 Treffer sind daher kein Episode-Nachweis für diese Queue-ID.')
  add('**Bewertung:** `processing` ist verwaist: `lock_owner=aa043-single-writer`, Lease und Heartbeat liegen in der Vergangenheit, `graphiti_episode_id` ist NULL, `remote_outcome=UNKNOWN_REMOTE_OUTCOME`, letzter HTTP-Status 500, `episode_created=false` im Fehlerpayload. Ein Doppelimport ist nicht als bereits erfolgte Episode belegt; wegen UNKNOWN_REMOTE_OUTCOME darf ohne erneuten Exact-Identity-Check nicht automatisch requeued werden.')
 else:
  add('**Bewertung:** Legacy-/Pilot-Rückstand. Keine Sperre, kein Lease, keine Episode-ID; Retry-Zeitpunkt liegt bereits in der Vergangenheit. Die Zeile ist technisch fällig, aber wegen sechs Versuchen und altem HTTP-500-Fehler nicht automatisch als sicher wiederholbar einzustufen.')
 add()
add('### Ursachen der beiden hängenden Jobs'); add(); add('- Queue 1092: Graphiti-Worker-Validierungsfehler `EntityNode.labels` erwartet Liste, erhielt String `Person`; `episode_created=false`; Request-ID und Fehler sind im Queue-Fehlerpayload enthalten, werden hier nicht als Geheimnis weiterverarbeitet.')
add('- Queue 1268: Graphiti-Worker-Fehler `graphiti_worker_failed: \'name\'`; `episode_created` ist im Payload nicht als erfolgreich belegt; Queue blieb in `processing`.')
add('- In beiden Zeilen steht `graphiti_attempts=0` und `attempt_count=0`, obwohl ein HTTP-500-Fehler dokumentiert ist. Das zeigt eine Zustands-/Zählerspannung zwischen älteren Retry-Feldern und dem neuen Unknown-Outcome-Pfad.')
add()
add('## 7. Vollständige offene Queue-Inventur'); add(); add('Die folgenden 15 Zeilen sind der vollständige aktuelle Filter `graphiti_status != \'done\'`.'); add(); add('| ID | Typ | Video-ID | Status | Erstellt | Aktualisiert | fällig/Retry | Owner | Lease bis | Attempts | HTTP | Episode-ID | Fehlerklasse |'); add('|---:|---|---|---|---|---|---|---|---|---:|---:|---|---|')
for x in Q:
 vals=[x.get(k) for k in ['id','source_type','video_id','graphiti_status','created_at','updated_at','next_attempt_at','lock_owner','lease_expires_at','attempt_count','last_http_status','graphiti_episode_id','last_error_class']]
 vals=[str(v or '—').replace('|','\\|').replace('\n',' ') for v in vals]
 add('| '+' | '.join(vals)+' |')
add(); add('### Technische Fälligkeit und Auswahl'); add(); add('- `queued`-Zeilen haben keinen `next_attempt_at`; der Scheduler kann sie grundsätzlich auswählen, sofern sein Worker sie nicht wegen Source-Typ/Scope ausschließt. Die neun allgemeinen Obsidian-Zeilen sind nicht YouTube-Kandidaten und werden vom YouTube-spezifischen AA-049-Teil ausgeschlossen.')
add('- `retry_wait`-Zeilen haben Retry-Zeitpunkte am 2026-07-19, also längst fällig. Ihre sechs Versuche und alten HTTP-500-/Timeout-Zustände sprechen für einen alten Legacy-Rückstand; ein neuer Import wurde nicht ausgelöst.')
add('- `processing`-Zeilen haben keine gültige aktuelle Lease mehr. Der aktuelle AA-049-Eventlog meldet dennoch `NO_DUE_YOUTUBE`, nicht `RECOVERED` oder `REQUEUED`. Eine automatische Lease-Recovery ist daher im beobachteten Livezustand nicht nachgewiesen.')
add('- Ob ein offener Inhalt „anderweitig erfolgreich verarbeitet“ wurde, ist für die beiden `processing`-Identitäten im vollständigen Episode-Inventar nicht nachgewiesen. Für die vier alten Retry-Zeilen ist ebenfalls keine exakte Episode-ID in der Queue vermerkt.')
add()
add('## 8. Recovery- und Retry-Logik'); add()
add('### Nachweis aus Code'); add(); add('- `aa043_single_writer.py` (verfügbare AA-043-Fassung) reserviert einen berechtigten Job atomar per `UPDATE ... WHERE graphiti_status IN (queued,retry_wait,lease_expired,review_required)`, setzt Owner, Token, Heartbeat und eine 20-Minuten-Lease und committet vor dem HTTP-POST.')
add('- Vor dem POST wird die Graphiti-Inventur nach `import_identity` durchsucht. Bei genau einem Treffer ist ein `ADOPT_EXISTING` vorgesehen; bei mehreren Treffern wird abgebrochen.')
add('- Nach erfolgreichem POST wird Episode-UUID zusammen mit Queue-/Version-/Registry-Updates geschrieben; danach `WRITE_CONFIRMED` und Lockfreigabe.')
add('- Bei jeder Exception wird die Queue wieder auf `processing` mit `UNKNOWN_REMOTE_OUTCOME` gesetzt. Das schützt vor falscher Erfolgsmeldung, erzeugt aber ohne separaten Lease-Recovery-Job einen hängenden Zustand.')
add('- `youtube_graphiti_queue.py` verwendet `INSERT OR IGNORE` und Identitäts-/Pfad-/Hashschutz; dieses Modul schreibt nur die Queue und führt laut aktuellem Quelltext keinen Graphiti-POST aus.')
add('- Der Graphiti-Service führt seinerseits Request-/Heartbeat-/Deadline-State in `/opt/struktur/graphiti/request-data`; die Health-Abfrage meldete aktuell 0 aktive und 0 stale Requests. Das ist kein Beweis, dass die externen Queue-Leases korrekt recovered wurden.')
add()
add('### Bewertung'); add(); add('**Die Recovery-Logik ist nicht ausreichend für einen belastbaren Dauerbetrieb.** Positiv sind atomisches Claiming, Owner-/Token-Felder, Lease, Exact-Identity-Prüfung, Idempotency-Identitäten und der Schutz vor falschem Erfolg. Die konkrete Lücke ist jedoch real: Ein Worker-Absturz, Timeout oder HTTP-500 nach Claim lässt die Queue bei `processing` und `UNKNOWN_REMOTE_OUTCOME`; im Livebestand sind genau solche verwaisten Zustände 1092 und 1268 sichtbar. Ein nachweisbar aktiver automatischer Lease-Recovery-/Reconciliation-Schritt, der `processing` nach Lease-Ablauf sicher zu `lease_expired`/Review überführt, ist nicht belegt.')
add(); add('**Besonders riskant:** Bei „POST erfolgreich, Queue-Update verloren“ kann ein Wiederholungsversuch eine zweite Episode erzeugen, wenn Exact-Identity nicht zuverlässig matcht. Der vorhandene Check ist sicherheitsfördernd, aber der Endpoint liefert unspezifische Treffer; deshalb ist eine exakte, deterministische Identity-Prüfung vor jedem Recovery erforderlich.')
add()
add('## 9. Live-Writer-Inventur'); add(); add('### Writer-Matrix'); add(); add('| Writer | Prozess/Service | Ziel | Tabelle/API | aktuell aktiv | produktiv schreibend | Ownership eindeutig | Parallelrisiko |'); add('|---|---|---|---|---|---|---|---|')
rows=[
('YouTube Research FastAPI','`youtube-research.service`, PID 4001613','YouTube-Masterbestand','`videos`, `chapters`, `video_links`, FTS, UI-/Jobtabellen','JA','JA','NEIN, mehrere direkte App-/DB-Pfade','hoch für fachliche SQLite-Daten'),
('E2E-Worker','`youtube-research-e2e-worker.service`, PID 2116675','Transcript/Extraction/Artifact','`videos`, `transcript_segments`, `e2e_jobs`, `e2e_extractions`, `e2e_rejections`; CE-DB; Obsidian','JA','JA','teilweise','mittel; eigene Tabellen plus Obsidian/Queue'),
('Queue-Enqueuer','`youtube_graphiti_queue.py`, via E2E/Importer','Graphiti-Queue','`graphiti_import_queue` via `INSERT OR IGNORE`','JA via Caller','JA','für Queue-Identity ja','mittel; mehrere Scanner möglich'),
('AA-049 Queue Worker / AA-043 Single Writer','`aa049-queue.timer` → `aa049-queue.service`','Graphiti-Episoden und Queue-State','Queue-Status; Graphiti `POST /episodes`','JA, Timer waiting','JA','für YouTube-Queue weitgehend ja','mittel; Unknown-Outcome-Recovery fehlt'),
('Generischer Obsidian-Importer','`obsidian-graphiti-import.timer`','Obsidian → Graphiti','Queue und Graphiti indirekt','Timer JA; Service zwischen Läufen dead','potenziell JA','NEIN','hoch für Source/Derived-Rückkopplung'),
('Graphiti-Service','`graphiti-service`, live 8644','Neo4j/Graph','`POST /episodes`, intern Neo4j; Request-DB','JA, Health ok','JA','API zentral, Quellen nicht vollständig','mittel; jede externe POST-Quelle relevant'),
('Registry/Reconciliation-Code','Staging/Importer-Pfade, kein aktiver eigener Dienst nachgewiesen','Registry/Versions/Decisions','`source_processing_registry`, `source_versions`, `reconciliation_decisions`','Code vorhanden; aktiver Prozess nicht vollständig bewiesen','potenziell JA','NEIN','mittel/hoch bei manueller Ausführung'),
('Legacy Batch-/Bridge-Skripte','Dateien vorhanden, kein aktueller Prozessnachweis','DB/Queue/Graphiti je nach Skript','diverse `INSERT`/`UPDATE`/POST-Pfade','NEIN nach Prozessnachweis','historisch/potenziell','NEIN','nur bei Start/Fehlkonfiguration'),
('Content Extraction','`content-extraction.service`, PID 2116678','eigene CE-Projektion','CE-DB; laut Unit read-only Quelle','JA','für Knowledge-DB nicht nachgewiesen','JA für read-only Rolle','niedrig für Knowledge-DB'),
]
for r in rows: add('| '+' | '.join(r)+' |')
add(); add('### Konkrete Codebefunde'); add(); add('- `/opt/struktur/youtube-research/e2e_worker.py:22` setzt `PRAGMA foreign_keys=ON` und schreibt direkt in mehrere Knowledge-/E2E-Tabellen.')
add('- `/opt/struktur/youtube-research/app.py` enthält direkte App-Schreibpfade, unter anderem Video-, Kapitel-, Link-, FTS- und Library-Updates; die FastAPI läuft produktiv.')
add('- `/opt/struktur/youtube-research/db.py:35` setzt FK-ON in der zentralen DB-Hilfsschicht, aber direkte zusätzliche App-Verbindungen verhindern die Aussage, dass jede Verbindung identisch konfiguriert ist.')
add('- `/opt/struktur/graphiti/youtube_graphiti_queue.py:65–87` schreibt die Queue per `INSERT OR IGNORE`, nicht direkt Graphiti.')
add('- `/opt/struktur/graphiti/service/main.py:666` stellt den produktiven `POST /episodes`-Write-Endpoint bereit.')
add('- Die tatsächlich referenzierte Produktionsdatei `/opt/struktur/obsidian-graphiti-import/aa049_queue_worker.py` war als Datei wegen restriktiver Dateirechte nicht direkt lesbar. Der Dienstpfad, Live-Queue-Felder, AA-049-Eventlog und die aktive systemd-Konfiguration belegen aber seine Ausführung. Die exakte Implementierung dieser geschützten Datei wird daher nicht über die zugängliche AA-043-Staging-Fassung hinaus behauptet.')
add()
add('## 10. Obsidian-Parallelpfad'); add(); add('- Der E2E-Worker schreibt automatisch nach `/opt/obsidian-vault/YouTube-Research/Automatisch/*.md` und versieht die Datei mit `youtube_id`, Transcript-/Content-Hash und Extraction-Version.')
add('- `youtube_graphiti_queue.py` scannt den gesamten Root `/opt/obsidian-vault/YouTube-Research` rekursiv nach Markdown-Dateien und nimmt YouTube-Dateien mit `youtube_id` in `graphiti_import_queue` auf.')
add('- Der generische Queue-Bestand enthält `source_type=obsidian-general` für relative Vaultpfade wie `Solutions/...`, `Architecture/...` und `Systemstruktur/...` und wird getrennt von YouTube-IDs geführt.')
add('- Die Scope-Policy erlaubt nachweisbar `Welle-1`, `Welle-2` und `Pilot`, sperrt Rohtranskripte, `.env`, Credentials und bestimmte private Pfade. Der E2E-Pfad `Automatisch` ist in dieser Allowlist nicht enthalten; die Queue enthält ihn trotzdem. Das ist eine nicht aufgelöste Scope-/Ownership-Diskrepanz.')
add('- Damit können automatisch erzeugte E2E-Markdown-Dateien grundsätzlich wieder eingelesen werden. Hash-/Identity-Unique-Indizes reduzieren identische Reimports, verhindern aber nicht nachweisbar jede semantische Doppelzählung, insbesondere bei geänderten/neu projizierten Markdown-Dateien.')
add('- Für Queue 1092 und 1268 sind keine exakten Episode-Matches im vollständigen Inventar nachgewiesen. Für bereits erfolgreiche Inhalte bleibt das allgemeine Risiko bestehen, dass Source und Projection separat als Episoden materialisiert werden.')
add()
add('## 11. Single-Writer-Bewertung'); add(); add('### A. YouTube → Graphiti'); add('Der aktive AA-049-Pfad konzentriert die YouTube-Queue-POSTs auf den AA-043-Single-Writer. Das ist im Timer-/Unit-Nachweis und in den Queue-Feldern `lock_owner=aa043-single-writer` belegt. **Für diesen Queue-Pfad: ja, konzentriert.** Eine globale Aussage über alle historischen Skripte ist nicht zulässig.')
add(); add('### B. Gesamt-Knowledge-DB'); add('**Nein, mehrere direkte Writer existieren weiterhin:** YouTube-FastAPI, E2E-Worker, DB-Hilfsschicht/App-Pfade, Registry-/Reconciliation-Code und potenziell Legacy-Skripte. Die fachlichen Tabellen sind nicht global hinter einem Knowledge Service gekapselt.')
add(); add('### C. Obsidian → Graphiti'); add('**Ja, ein unabhängiger generischer Parallelpfad existiert weiterhin:** `obsidian-graphiti-import.timer` ist aktiv. Er ist nicht identisch mit dem AA-049-YouTube-Queue-Pfad.')
add(); add('### D. Legacy'); add('Legacy-/Batch-/Bridge-Dateien sind zahlreich vorhanden, aber ein aktuell laufender Prozessnachweis für die meisten wurde nicht gefunden. Sie sind daher **schreibfähig vorhanden, aber nicht als aktuell aktiv behauptet**. Aktiv nachgewiesen sind YouTube-FastAPI, E2E-Worker, AA-049-Timer/Oneshot, Graphiti-Service und der generische Obsidian-Timer.')
add()
add('## 12. Risiken und priorisierter Repair-Plan'); add(); add('| Priorität | Problem | Ursache/Nachweis | Betroffene Daten | Vorgeschlagene Reparatur | Risiko | Backup | Test/Rollback |'); add('|---|---|---|---|---|---|---|---|')
add('| KRITISCH | FK-Orphans | 485 `foreign_key_check`; FK-Default 0; 279 `chapters`, 206 `video_links` | Child-Zeilen und fehlende Video-Parents | zunächst read-only Herkunfts-/Rekonstruktionsmatrix; erst danach gezielte Parent-Rekonstruktion oder kontrollierte Quarantäne | Fehlzuordnung oder Datenverlust bei falscher Reparatur | DB+WAL konsistent sichern | Kopie, FK-ON, FK-/Integrity-Audit; Rollback durch unveränderte Originalkopie |')
add('| KRITISCH | Verwaiste Queue-Leases | IDs 1092/1268: Lease abgelaufen, `UNKNOWN_REMOTE_OUTCOME`, HTTP 500, keine Episode exakt gefunden | 2 Queue-Zeilen, potenziell Graph-Wiederholungen | Lease-Recovery-State definieren, Exact-Identity-Check, dann einzeln Review/Recovery | Doppel-Episode oder falscher Erfolg | DB/Queue und Graphiti-Request-State sichern | Dry-run, Episode-Match, Einzeltest, Status- und Graph-Audit; Rollback per DB-Backup |')
add('| NORMAL | Alter Retry-Rückstand | IDs 347/759/849/859: 6 Versuche, Retry-Zeitpunkte seit 19.07., alte 500/Timeouts | 4 Retry-Zeilen | Ursache/Scope prüfen; nicht blind requeue’n; danach Einzelentscheid | erneute Provider-/Graph-Last | Queue-/DB-Backup | Einzel-Run mit Backoff; Statussnapshot vorher/nachher |')
add('| KRITISCH | Globales Writer-Ownership | FastAPI/E2E/Registry/Obsidian/Graphiti schreiben in parallelen Pfaden | fachliche DB, Queue, Graph | Writer-Vertrag und zentrale Ownership festlegen, danach Altpfade kontrolliert sperren/umleiten | Produktionsausfall oder Doppelwrites | Code-/Config-/DB-Backup | Shadow-/Dry-run, Prozessinventur, Rollback auf vorherige Units |')
add('| NORMAL | Scope-Rückkopplung | E2E schreibt `Automatisch`; Scanner läuft rekursiv; Allowlist nennt Automatisch nicht | Obsidian-/Graphiti-Projektionen | Source/Derived technisch trennen und Automatisch explizit behandeln | semantische Doppelbestätigung | Vault-/Queue-Backup | Hash-/Identity-Test gegen reale Stichprobe |')
add('| NORMAL | Kein nachgewiesener Lease-Recovery-Worker | Code setzt Unknown Outcome, Live-Zeilen bleiben processing | verwaiste Jobs | Lease-Recovery als separaten, reviewpflichtigen Schritt implementieren | falsche Adoption/Requeue | DB-/Graph-Backup | Absturzsimulation in Testkopie |')
add('| OPTIONAL | Einheitliche Graph-Metriken | Inventory zählt Episoden, nicht Entities/Relationships | Observability | read-only Cypher-/Metrikendpunkt ergänzen | gering | Konfigurationsbackup | nur read-only |')
add()
add('### Bewusst nicht durchgeführte Maßnahmen'); add('- keine Orphan-Reparatur; keine Parent-Erzeugung; keine Löschung'); add('- kein `PRAGMA foreign_keys=ON` global aktiviert'); add('- kein Queue-Status, Lease, Lock, Retry- oder Timestamp geändert'); add('- kein Requeue und kein Graphiti-POST'); add('- kein Writer/Timer deaktiviert oder neu gestartet'); add('- keine Scope-Policy geändert'); add('- kein SQLite-Schema geändert'); add('- keine Graphiti-Episode und kein Neo4j-Datensatz verändert'); add()
add('## 13. Entscheidungen von Carlo'); add(); add('Eine Entscheidung ist erforderlich für:'); add('1. fachliche Behandlung der 485 FK-Orphans: rekonstruieren, quarantänisieren oder bewusst tolerieren;'); add('2. Freigabe einzelner Queue-Recovery-Tests für 1092, 1268 und den alten Retry-Rückstand;'); add('3. endgültige Ownership: nur AA-049 für YouTube und welcher kontrollierte Pfad für allgemeine Obsidian-Dokumente;'); add('4. Source-/Derived-Regel für `Automatisch`;'); add('5. vollständiger Restore-Test inklusive Graphiti/Neo4j und Auswahl des Backup-Verfahrens.')
add(); add('## 14. Nachweisquellen'); add(); add('- Live-DB: `/opt/struktur/youtube-research/knowledge.db`'); add('- FK-/Queue-Audit-Artefakt dieser Erhebung: temporär `/tmp/wissenspipeline_baseline_audit.json` (read-only erzeugt)'); add('- Queue-Events: `/opt/struktur/reports/aa049-worker-events.jsonl`'); add('- AA-049-Unit/Timer: `/etc/systemd/system/aa049-queue.service`, `/etc/systemd/system/aa049-queue.timer`'); add('- E2E-Worker: `/opt/struktur/youtube-research/e2e_worker.py`'); add('- YouTube-App/DB-Schicht: `/opt/struktur/youtube-research/app.py`, `/opt/struktur/youtube-research/db.py`'); add('- Queue-Enqueuer: `/opt/struktur/graphiti/youtube_graphiti_queue.py`'); add('- Graphiti-Service: `/opt/struktur/graphiti/service/main.py`'); add('- Scope Policy: `/opt/struktur/graphiti/scope_policy_checker.py`'); add('- Obsidian-Vault: `/opt/obsidian-vault/YouTube-Research`'); add('- Live Graphiti-Tests: `GET /health`, `GET /episodes/inventory`, `POST /episodes/check`; letzterer wurde wegen unspezifischer Treffer nicht als exakter Episode-Nachweis verwendet.')
add(); add('Arbeitsauftrag Wissenspipeline-Baseline erledigt.')
Path('/home/hermes/Wissenspipeline-Baseline-FK-Queue-Writer.md').write_text('\n'.join(lines)+'\n',encoding='utf-8')
print('written',len(lines), 'lines', Path('/home/hermes/Wissenspipeline-Baseline-FK-Queue-Writer.md').stat().st_size,'bytes')