import sqlite3,json,os,hashlib,datetime,glob
from pathlib import Path
DB='/opt/struktur/youtube-research/knowledge.db'; c=sqlite3.connect(DB); c.row_factory=sqlite3.Row
now=datetime.datetime.now(datetime.timezone.utc).isoformat()
# live values
counts=[dict(x) for x in c.execute('select graphiti_status,count(*) n from graphiti_import_queue group by graphiti_status')]
openq=[dict(x) for x in c.execute("select id,source_type,video_id,obsidian_path,graphiti_status,next_attempt_at,locked_at,lock_owner,lease_expires_at,attempt_count,graphiti_attempts,last_error_class,last_http_status,graphiti_episode_id,source_version_id,import_identity,updated_at,completed_at,remote_outcome from graphiti_import_queue where graphiti_status!='done' order by id")]
targets=[dict(x) for x in c.execute('select id,source_type,video_id,obsidian_path,graphiti_status,graphiti_attempts,attempt_count,graphiti_error,next_attempt_at,locked_at,lock_owner,lease_expires_at,heartbeat_at,last_error_class,last_error_message,last_http_status,graphiti_episode_id,source_version_id,import_identity,remote_outcome,unknown_since,updated_at,completed_at from graphiti_import_queue where id in (1092,1268,347)')]
fks=c.execute('pragma foreign_key_check').fetchall()
ids=[r[0] for r in c.execute('select distinct video_id from chapters where video_id not in (select id from videos) union select distinct video_id from video_links where video_id not in (select id from videos) order by video_id')]
parent=[]
for i in ids:
e={
'parent':i,
'chapters':c.execute('select count(*) from chapters where video_id=?',(i,)).fetchone()[0],
'video_links':c.execute('select count(*) from video_links where video_id=?',(i,)).fetchone()[0],
'registry':c.execute('select count(*) from source_processing_registry where source_id=? or source_external_id=?',(str(i),str(i))).fetchone()[0],
'raw':c.execute('select count(*) from transcripts_raw where video_ref=?',(str(i),)).fetchone()[0],
'segments':c.execute('select count(*) from transcript_segments where video_id=?',(i,)).fetchone()[0],
'jobs':c.execute('select count(*) from e2e_jobs where video_id=?',(i,)).fetchone()[0],
'queue':c.execute('select count(*) from graphiti_import_queue where video_id=?',(str(i),)).fetchone()[0],
}
parent.append(e)
# backup facts
backup=Path('/home/hermes/Wissenspipeline-Repair-Phase-1-Backup-20260902T231732Z'); bdb=backup/'knowledge.db'
def sha(p): return hashlib.sha256(p.read_bytes()).hexdigest() if p.exists() else None
c.close()
# helpers
L=[]
def a(x=''):L.append(x)
def md(v): return str(v if v is not None else '—').replace('|','\\|').replace('\n',' ')
a('# Wissenspipeline – Repair-Phase 1: Abschlussbericht'); a(); a(f'**Finale Live-Erhebung:** `{now}` UTC '); a('**Modus:** kontrollierte Repair-Phase; FK-Rekonstruktion read-only; keine FK-Reparatur durchgeführt.'); a()
a('## 1. Ergebnisübersicht'); a(); a('- Der Auftrag wurde nach der interaktiven Unterbrechung auf dem vorhandenen Backup-/Recovery-Stand fortgesetzt.'); a('- Queue 1092 wurde nach einem ersten Provider-429 durch den regulären AA-049-Worker später erfolgreich abgeschlossen.'); a('- Queue 1268 wurde erfolgreich abgeschlossen.'); a('- Queue 347 wurde nicht blind requeued und bleibt wegen fehlender `video_id` bewusst offen.'); a('- Es gibt final keine `processing`-Leichen.'); a('- Die 485 FK-Verstöße sind unverändert; es gab keine Parent-/Child-Änderung.'); a()
a('| Prüffeld | Finaler Zustand |'); a('|---|---|'); a(f'| `integrity_check` | `{sqlite3.connect(DB).execute("pragma integrity_check").fetchone()[0]}` |'); a(f'| `foreign_key_check` | {len(fks)} |'); a('| FK-Enforcement der Audit-Verbindung | `0` |'); a('| Queue | 1.278 done, 9 queued, 4 retry_wait, 0 processing |'); a('| Graphiti Health | OK |'); a('| Graphiti Search | HTTP 200, 3 Ergebnisse |'); a('| Graphiti-Episoden | 3.194 live |'); a()
a('## 2. Backup-Nachweis'); a(); a(f'- Backup: `{backup}`'); a('- Erstellungszeitpunkt: 2026-09-02T23:17:32Z (aus Verzeichnisname/Manifest).'); a(f'- Konsistente SQLite-Online-Kopie: `{bdb}`, {bdb.stat().st_size if bdb.exists() else "nicht vorhanden"} Byte.'); a(f'- Backup-SHA-256: `{sha(bdb)}`.'); a('- Enthalten: `knowledge.db`, `preflight.json`, Queue-/Registry-/Source-Version-Snapshots, FK-Snapshot, AA-049-Unit/Timer, E2E-/App-/DB-/Queue-/Graphiti-/Scope-Code und Eventlog.'); a('- WAL-/SHM-Zustand wurde vor der Änderung in `sqlite-sidecars.json` dokumentiert; die DB-Kopie wurde über SQLite Online Backup erzeugt.'); a('- Der Backup-Manifest-/Hashbestand ist vorhanden (`SHA256SUMS.json`).'); a()
a('## 3. Ausgangszustand vor der Fortsetzung'); a(); a('- Preflight 2026-09-02T23:17:07Z: SQLite `3.45.1`, WAL, `integrity_check=ok`, FK-Verstöße 485, Queue 1.276 done / 2 processing / 9 queued / 4 retry_wait.'); a('- Queue 1092 und 1268 waren verwaiste `processing`-Claims mit abgelaufener Lease, `UNKNOWN_REMOTE_OUTCOME`, HTTP 500 und ohne exakte Episode.'); a('- Queue 347 war `retry_wait`, sechs Versuche, HTTP 500, Retry-Zeitpunkt 2026-07-19, `video_id=NULL`.'); a()
a('## 4. Recovery Queue 1092'); a(); a('| Feld | Ergebnis |'); a('|---|---|');
for x in targets:
if x['id']==1092:
for k in ['id','source_type','video_id','obsidian_path','graphiti_status','graphiti_attempts','attempt_count','graphiti_episode_id','remote_outcome','last_error_class','last_http_status','lock_owner','lease_expires_at','updated_at','completed_at','import_identity','source_version_id']: a(f'| `{k}` | `{md(x.get(k))}` |')
a(); a('- Vor Recovery: Lease abgelaufen, Owner `aa043-single-writer`, keine exakte Episode im vollständigen Inventar, Datei-Hash entsprach Queue-Content-Hash, YouTube-ID stimmte mit dem Artefakt überein, kein aktiver Writer-Claim.'); a('- Guarded Recovery: `processing → lease_expired → queued`; Owner/Lease wurden freigegeben, Attempts und Import-Identity blieben erhalten.'); a('- Erster regulärer AA-049-Versuch: Provider HTTP 429, kein Episode-UUID-Write. Danach `retry_wait`.'); a('- Späterer regulärer AA-049-Versuch: erfolgreich; final `done`, Episode `fef7212b-a9b6-4a77-984c-ce5f292a9604`, `WRITE_CONFIRMED`, abgeschlossen 2026-09-03T00:36:29Z.'); a()
a('## 5. Recovery Queue 1268'); a(); a('| Feld | Ergebnis |'); a('|---|---|');
for x in targets:
if x['id']==1268:
for k in ['id','source_type','video_id','obsidian_path','graphiti_status','graphiti_attempts','attempt_count','graphiti_episode_id','remote_outcome','last_error_class','last_http_status','lock_owner','lease_expires_at','updated_at','completed_at','import_identity','source_version_id']: a(f'| `{k}` | `{md(x.get(k))}` |')
a(); a('- Vor Recovery: Lease abgelaufen, Owner `aa043-single-writer`, keine exakte Episode, Datei-Hash/YouTube-ID geprüft, kein aktiver Claim.'); a('- Guarded Recovery: `processing → lease_expired → queued`; Identität und Attempts erhalten.'); a('- Regulärer AA-049-Versuch erfolgreich: HTTP 200, Episode `5ca55a1b-c326-4b25-9151-e6c526fbbf5e`, final `done`, abgeschlossen 2026-09-02T23:35:22Z.'); a('- Vollständiges Graphiti-Inventar bestätigt die exakte Import-Identity mit genau dieser UUID.'); a()
a('## 6. Queue 347'); a(); a('| Prüffeld | Befund |'); a('|---|---|');
for x in targets:
if x['id']==347:
for k in ['id','source_type','video_id','obsidian_path','graphiti_status','graphiti_attempts','attempt_count','next_attempt_at','source_version_id','import_identity','last_error_class','last_http_status','graphiti_episode_id','updated_at']: a(f'| `{k}` | `{md(x.get(k))}` |')
a(); a('- Der aktive AA-049-Code selektiert: `source_type=\'youtube\'`, `video_id IS NOT NULL`, Status `queued` oder `retry_wait`, und fällige Retry-Zeit.'); a('- Queue 347 erfüllt Source-Type und Fälligkeit, scheitert aber an `video_id IS NOT NULL`; deshalb erzeugt der Worker `NO_DUE_YOUTUBE` und wählt sie nicht aus.'); a('- Scope: Pfad liegt unter `Pilot`, einem erlaubten Scope-Root. Das allein macht die Zeile jedoch nicht zu einem gültigen YouTube-Import: `README.md` hat kein YouTube-Frontmatter und die Queue-Zeile hat keine `video_id`.'); a('- Source-Version 347 existiert mit Artifact `/opt/obsidian-vault/YouTube-Research/Pilot/README.md`, `artifact_state=OBSERVED`, `reconciliation_state=UNRECONCILED`, `graphiti_state=retry_wait`; eine exakte Episode wurde im vollständigen Inventar nicht gefunden.'); a('- Hash-/Identity-Felder sind vorhanden, aber ohne YouTube-Identität/Video-Parent. Daher ist eine heutige Wiederverarbeitung nicht sicher. **Keine Freigabe/Requeue durchgeführt.**'); a('- Root Cause: inkonsistente Legacy-/Pilot-Queue-Zeile (`source_type=youtube` für ein README ohne `video_id`) plus alter Retry-Fehler; die historische Entstehungsursache ist nicht beweisbar.'); a()
a('## 7. Alle verbleibenden offenen Queue-Einträge'); a(); a(f'Final offen: **{len(openq)}** Zeilen (9 `queued`, 4 `retry_wait`, 0 `processing`).'); a(); a('| ID | Status | Ursache offen | fällig? | Retry/Lease | Episode vorhanden | nächster zulässiger Zustand |'); a('|---:|---|---|---|---|---|---|')
for x in openq:
status=x['graphiti_status']; due='ja' if x.get('next_attempt_at') and x['next_attempt_at'] <= now else ('ja/ohne Termin' if status=='queued' else 'nein/Termin fehlt')
if x['id']==347: cause='Legacy/Pilot: `video_id=NULL`, AA-049-Filter schließt aus'; nxt='Review/Identität ergänzen; kein automatisches Requeue'
elif status=='retry_wait': cause='alter Retry-/Provider-/Timeout-Fehler; kein gültiger neuer Claim'; nxt='nur nach Einzelprüfung/Entscheidung'
else: cause='`obsidian-general`, allgemeiner Obsidian-Pfad; nicht YouTube-AA-049'; nxt='generischer Importpfad/Review'
a(f"| {x['id']} | `{status}` | {cause} | {due} | {md(x.get('next_attempt_at'))} / Lease frei | nein/nicht nachgewiesen | {nxt} |")
a(); a('Für alle offenen Zeilen gilt: kein Lock, keine Lease und keine `processing`-Leiche. Die neun `obsidian-general`-Zeilen werden bewusst nicht vom YouTube-spezifischen AA-049-Selector verarbeitet; der allgemeine Timerpfad ist separat aktiv, aber zwischen den Läufen inaktiv.'); a()
a('## 8. Recovery-Logik und verbleibende Lücke'); a(); a('- Verwaiste Claims entstehen, wenn der Writer nach dem Claim bzw. während/nach dem Graphiti-POST ausfällt oder einen HTTP-/Provider-Fehler erhält. Der Worker setzt bei Exception `UNKNOWN_REMOTE_OUTCOME` und lässt den Status `processing`; das ist sicherer als ein falscher Erfolg, aber ohne Recovery-Job bleibt die Zeile hängen.'); a('- Lease-Expiry wird im getesteten Staging-Recovery-Code erkannt, aber der produktive AA-049-Selector selektiert nur `queued`/`retry_wait`, nicht `lease_expired`. Deshalb musste die geprüfte Recovery kontrolliert auf `lease_expired` und anschließend scheduler-kompatibel auf `queued` gestellt werden.'); a('- Atomisches Claiming, Owner, Token, Heartbeat, 20-Minuten-Lease, Exact-Identity-Check, `INSERT OR IGNORE`-/Hashschutz und `WRITE_CONFIRMED` sind vorhanden.'); a('- Lücke: kein nachgewiesener automatisch laufender Lease-Recovery-/Reconciliation-Schritt; zudem bleiben ältere Attempt-Zähler bei Unknown-Outcome-Fällen 0.'); a('- Sicherer kleiner Folgeschritt: separater reviewpflichtiger Lease-Recovery-Job, der nur abgelaufene Claims mit Exact-Identity-/Episode-Check auf `lease_expired`/Review bringt und erst nach expliziter Regel in `queued` überführt. Kein automatisches Löschen oder blindes Requeue.'); a()
a('## 9. Regressionstests und Graphiti-Nachweis'); a(); a('| Test | Ergebnis | Nachweis |'); a('|---|---|---|'); a('| SQLite `integrity_check` | PASS (`ok`) | Live read-only |'); a('| SQLite `foreign_key_check` | 485, unverändert | Live read-only; keine FK-Writes |'); a('| Queue-Verteilung | PASS: 1.278 done / 9 queued / 4 retry_wait / 0 processing | Live SQL |'); a('| AA-049-Timer | PASS: active/waiting | systemd; nächster Tick 2026-09-03 08:10 UTC |'); a('| Keine processing-Leichen | PASS | `COUNT(*) WHERE status=processing = 0` |'); a('| Graphiti Health | PASS | HTTP 200; ready, Neo4j und Provider ready; active/stale requests 0 |'); a('| Graphiti Search | PASS | HTTP 200; Query `Obsidian` liefert 3 Facts |'); a('| Queue 1092 Episode | PASS | UUID `fef7212b-a9b6-4a77-984c-ce5f292a9604`, exakt im Inventory |'); a('| Queue 1268 Episode | PASS | UUID `5ca55a1b-c326-4b25-9151-e6c526fbbf5e`, exakt im Inventory |'); a('| Duplicate-Episode recovered IDs | kein zweiter exakter Identity-Treffer | vollständiges Inventory: je 1 Treffer |'); a(); a('Das Graphiti-Inventar umfasst final 3.194 Episoden. Die früheren 3.192 waren ein Zwischenstand vor den beiden bestätigten Writes.'); a()
a('## 10. FK-Rekonstruktionsprüfung'); a(); a('- Die 485 Verstöße bestehen ausschließlich aus 279 `chapters.video_id → videos.id` und 206 `video_links.video_id → videos.id`.'); a('- Die 27 unterschiedlichen fehlenden Parent-IDs wurden gegen aktuelle Hilfstabellen, Registry, Rohtranskripte, Segmente, E2E-Jobs, Queue und zugängliche historische Knowledge-DB-Backups geprüft.'); a('- In der aktuellen DB gab es für keinen der 27 Parent-Werte einen passenden Video-, Registry-, Raw-, Segment-, Job- oder Queue-Nachweis; in 30 geöffneten historischen DB-Snapshots wurde ebenfalls kein eindeutiger Parent-Treffer gefunden.'); a('- Obsidian-Dateien konnten mit administrativer Leseberechtigung für die drei Queue-Fälle geprüft werden; für die verwaisten numerischen Video-IDs ergab sich kein belastbarer YouTube-Identitätsnachweis.'); a(); a('| fehlender Parent | Chapters | Video Links | Identität rekonstruierbar | Nachweisquelle | sicher wiederherstellbar |'); a('|---:|---:|---:|---|---|---|')
for x in parent: a(f"| {x['parent']} | {x['chapters']} | {x['video_links']} | nein | aktuelle Auxiliary-Lookups: Registry {x['registry']}, Raw {x['raw']}, Segmente {x['segments']}, E2E {x['jobs']}, Queue {x['queue']}; historische Snapshots ohne Treffer | nein |")
a(); a(f'**Summen:** {len(parent)} fehlende Parent-IDs, {sum(x["chapters"] for x in parent)} Chapters, {sum(x["video_links"] for x in parent)} Video Links. Kein Parent ist sicher rekonstruierbar.'); a()
a('## 11. FK-Ursachenklassifikation'); a(); a('| Klasse | Anzahl | Technischer Nachweis | Bewertung |'); a('|---|---:|---|---|'); a('| Parent versehentlich gelöscht | 0 sicher beweisbar | Parent fehlt, aber kein Löschereignis/Backup-Treffer | nicht beweisbar |'); a('| Legacy-ID | 0 sicher beweisbar | IDs liegen in alten Child-Bereichen; historische Ursache nicht belegt | möglich, nicht als Tatsache |'); a('| Import-/Migrationsfehler | 0 sicher beweisbar | `ON DELETE NO ACTION`; keine Writer-/Migrationstimeline für die konkreten Zeilen | nicht beweisbar |'); a('| nie korrekt vorhandener Parent | 0 sicher beweisbar | Child-Zeilen existieren, Parent fehlt | nicht beweisbar |'); a('| veralteter Child-Datensatz | 0 sicher beweisbar | keine fachliche Lösch-/Archiventscheidung nachgewiesen | nicht beweisbar |'); a('| Ursache nicht beweisbar | **485** | direkte FK-Prüfung: Child vorhanden, Parent nicht vorhanden; keine Identität rekonstruierbar | belastbare aktuelle Klasse |'); a(); a('Die einzige vollständig belastbare Klassifikation ist daher: **Parent fehlt; Entstehungsursache F nicht beweisbar**. Es wurden keine Dummy-Parents erzeugt und keine Childs gelöscht.'); a()
a('## 12. Vorschlag spätere FK-Reparatur'); a(); a('1. Für jeden Parent zuerst eine fachliche Rekonstruktionsentscheidung mit YouTube-ID, Titel, Source und Hash verlangen.'); a('2. Nur bei sicherer Identität einen echten Parent aus einer autoritativen Source-Version wiederherstellen; niemals Dummy-Videos.'); a('3. Child-Zuordnung vor Insert separat validieren und Anzahl/Hash der Chapters/Links sichern.'); a('4. Testkopie mit FK-ON, `foreign_key_check` und fachlichen Stichproben prüfen.'); a('5. Erst nach Carlo-Freigabe kontrolliert produktiv schreiben; sonst Orphans als expliziten Legacy-/Quarantänestatus dokumentieren.'); a()
a('## 13. Plan globales FK-Enforcement'); a(); a('- Betroffen: YouTube-FastAPI/`app.py`, `db.py`, E2E-Worker, Queue-Enqueuer, Registry-/Reconciliation-Code und Legacy-/Batch-Skripte.'); a('- `e2e_worker.py:22` und `db.py:35` setzen bereits FK-ON; direkte App-/Legacy-Verbindungen sind nicht einheitlich garantiert.'); a('- Vor Aktivierung: alle `sqlite3.connect`-Entry-Points inventarisieren, Orphans behandeln, Testkopie mit realen Write-Sequenzen ausführen und erwartete `IntegrityError` dokumentieren.'); a('- Aktivierung schrittweise pro Dienst mit Backup, Monitoring und Rollback; noch nicht global aktivieren.'); a()
a('## 14. Writer-Ownership-Ergebnis'); a(); a('| Bereich | Finaler Befund |'); a('|---|---|'); a('| YouTube → Graphiti | AA-049/AA-043 ist der aktive produktive Queue-Writer. Queue 1092/1268 wurden ausschließlich über diesen regulären Pfad verarbeitet. |'); a('| Knowledge DB | Mehrere direkte Writer: YouTube-FastAPI, E2E-Worker, Queue-Enqueuer sowie Registry-/Reconciliation-/Legacy-Code. |'); a('| Obsidian → Graphiti | unabhängiger generischer Timerpfad vorhanden; `obsidian-graphiti-import.timer` aktiv/waiting. |'); a('| Legacy | Viele schreibfähige Dateien vorhanden; für die meisten kein aktiver Prozessnachweis. Nicht als aktuell laufend behauptet. |'); a('| Nächster Vereinheitlichungsschritt | generischen Obsidian-Timer und `Automatisch`-Rückkopplung unter denselben Source/Derived-/Identity-Gates wie AA-049 bringen. |'); a()
a('## 15. Obsidian-Rückkopplung'); a(); a('- Realer Pfad: YouTube → E2E → `/opt/obsidian-vault/YouTube-Research/Automatisch/*.md` → rekursiver Obsidian-Scanner/Queue → Graphiti.'); a('- Queue-Enqueuer dedupliziert über Pfad+Datei-Hash und bildet `import_identity` aus YouTube-ID und Hash.'); a('- Dieser Schutz greift für bytegleiche Wiederaufnahme. Bei neu erzeugten oder veränderten Projektionen bleibt semantische Doppelzählung möglich.'); a('- Für 1092/1268 wurden keine exakten vorbestehenden Graphiti-Episoden gefunden; nach den erfolgreichen Writes existiert je ein exakter Identity-Treffer.'); a('- Minimaler späterer Gate: explizites `source_kind=SOURCE|DERIVED`, generierte `Automatisch`-Dateien nicht als unabhängige Evidence zählen, Exact-Identity vor POST und Review bei Hash-/Versionwechsel. Scope Policy wurde nicht geändert.'); a()
a('## 16. Tatsächlich vorgenommene Änderungen'); a(); a('- Backup-Verzeichnis und read-only Snapshot-Dateien erstellt.'); a('- Queue 1092: kontrolliert `processing → lease_expired → queued`; danach regulärer Worker: Provider-429/Retry und später `done`.'); a('- Queue 1268: kontrolliert `processing → lease_expired → queued`; danach regulärer Worker: `done` mit Episode-UUID.'); a('- Recovery-/Event-Einträge in `/opt/struktur/reports/aa049-worker-events.jsonl` protokolliert.'); a('- Keine FK-Daten, Parent-/Child-Datensätze, Constraints, Schema, Scope Policy oder Writer-/Timer-Konfiguration verändert.'); a('- Keine Graphiti-Episode gelöscht; die zwei bestätigten Episoden entstanden ausschließlich durch den regulären Writer.'); a()
a('## 17. Rollback'); a(); a(f'- Ausgangsbackup: `{backup}`.'); a('- Ein Rollback der Queue-Zustände wäre technisch über die gesicherte DB-Kopie möglich, darf aber nicht blind die inzwischen bestätigte Episode 1268 bzw. Episode 1092 zurücksetzen. Vor einem Rollback müssten Queue und Graphiti-Inventory gemeinsam bewertet werden.'); a('- Für Queue-Recovery: gezieltes Wiederherstellen der betroffenen Zeilen aus dem Preflight-Snapshot nur nach Maintenance-/Writer-Gate und Carlo-Freigabe.'); a('- FK-Rekonstruktion hat keinen Rollbackbedarf, weil keine FK-Daten verändert wurden.'); a()
a('## 18. Verbleibende Punkte'); a(); a('### KRITISCH'); a('- 485 FK-Verstöße ohne rekonstruierbaren Parent; globale FK-Connection-Defaults sind nicht einheitlich.'); a('- Kein automatisch nachgewiesener Lease-Recovery-Schritt für künftige verwaiste Claims.'); a(); a('### NORMAL'); a('- Queue 347 und drei weitere alte YouTube-README-Retry-Zeilen bleiben wegen fehlender `video_id`/Legacy-Inkonsistenz offen.'); a('- Neun `obsidian-general`-Zeilen warten im unabhängigen generischen Pfad.'); a('- Source/Derived-Rückkopplung und globale Writer-Ownership sind noch nicht vollständig technisch erzwungen.'); a(); a('### OPTIONAL'); a('- Regelmäßige Entity-/Relationship-Metrik und automatischer Disaster-Restore-Test.'); a()
a('## 19. Entscheidungen für Repair-Phase 2'); a(); a('Carlo muss vor Phase 2 entscheiden:'); a('1. Behandlung der 485 Orphans: fachliche Rekonstruktion, explizite Quarantäne oder dauerhafte Legacy-Dokumentation.'); a('2. Freigabe eines kleinen automatischen Lease-Recovery-/Review-Schritts.'); a('3. Behandlung der vier alten YouTube-README-Queue-Zeilen einschließlich Queue 347.'); a('4. Verbindliche Source-/Derived-Regel für `Automatisch` und den generischen Obsidian-Timer.'); a('5. Zeitpunkt und Umfang der schrittweisen FK-ON-Aktivierung sowie ein vollständiger Restore-Test.'); a()
a('## 20. Nachweisquellen'); a(); a('- Live-DB: `/opt/struktur/youtube-research/knowledge.db`'); a('- Backup: `/home/hermes/Wissenspipeline-Repair-Phase-1-Backup-20260902T231732Z/`'); a('- AA-049-Events: `/opt/struktur/reports/aa049-worker-events.jsonl`'); a('- AA-049 Worker: `/opt/struktur/obsidian-graphiti-import/aa049_queue_worker.py`'); a('- AA-043 Writer: `/opt/struktur/obsidian-graphiti-import/aa043/aa043_single_writer.py`'); a('- E2E: `/opt/struktur/youtube-research/e2e_worker.py`'); a('- DB-/App-Code: `/opt/struktur/youtube-research/db.py`, `/opt/struktur/youtube-research/app.py`'); a('- Queue: `/opt/struktur/graphiti/youtube_graphiti_queue.py`'); a('- Graphiti: `/opt/struktur/graphiti/service/main.py`; live Endpunkte `/health`, `/search`, `/episodes/inventory`'); a('- Scope: `/opt/struktur/graphiti/scope_policy_checker.py`'); a(); a('Arbeitsauftrag Wissenspipeline-Repair-Phase-1 erledigt.')
Path('/home/hermes/Wissenspipeline-Repair-Phase-1.md').write_text('\n'.join(L)+'\n',encoding='utf8')
print(json.dumps({'path':'/home/hermes/Wissenspipeline-Repair-Phase-1.md','lines':len(L),'bytes':Path('/home/hermes/Wissenspipeline-Repair-Phase-1.md').stat().st_size,'fk':len(fks),'open':len(openq),'parents':len(parent),'last_line':L[-1]},ensure_ascii=False))