Explorer
/tmp/aa040_report.py
← Zurück ↓ Download
from pathlib import Path
import hashlib
report='''# AA-040 – Vollständige Ausschluss-, Skip- und Queue-Analyse

## Prüfrahmen

Ausschließlich read-only geprüft:

```text
Importer: /opt/struktur/obsidian-graphiti-import/importer.py
Konfiguration: /opt/struktur/obsidian-graphiti-import/config.json
Vault: /opt/obsidian-vault
Importer-Datenbank: /opt/struktur/obsidian-graphiti-import/obsidian_graphiti_import.db
Graphiti-Requestdatenbank: /opt/struktur/graphiti/request-data/requests.db
Aktiver Hermes-Dashboard-Endpunkt: http://127.0.0.1:9119
```

Importer-SHA-256 zum Prüfzeitpunkt:

```text
ef97492bf25ef2e6333f34e49f5d5e6b2c4e655df224bf873c575e8c7a10e983
```

Konfigurations-SHA-256:

```text
57a3b1971312b76c0bb34ab728730f5da3d8a9d4bb320aa783a2452ee2ac2820
```

Keine Datei, Datenbank, Queue, Episode oder Konfiguration wurde verändert. Kein Import wurde gestartet.

## 1. Tatsächliche Konfiguration

```text
vault=/opt/obsidian-vault
db=/opt/struktur/obsidian-graphiti-import/obsidian_graphiti_import.db
graphiti_url=http://127.0.0.1:8644
extensions=[".md"]
min_chars=100
max_attempts=5
```

### allow_roots

Die erste relative Pfadkomponente muss exakt, case-sensitiv einem dieser Werte entsprechen:

```text
01-Kontext
05-Ressourcen
Architecture
Agent-Solutions
Solutions
Runbooks
Systemstruktur
KI-Kurse
85_Technik
```

`allow_roots` ist keine rekursive Include-Liste für einzelne Dateien, sondern eine Prüfung von `rel.parts[0]`.

### deny_patterns

Die Prüfung erfolgt case-insensitiv als Teilstring-Suche im gesamten relativen Pfad:

```text
.obsidian
.git
template
vorlage
cache
backup
archive
archiv
draft
private
no-graphiti
exclude-from-graphiti
05_SESSIONS
YouTube-Research
YouTube Research
00-Staging
03-Projekte
06-Daily-Notes
07-Archiv
LP-CONTEXT
LP-SPECIFIC
AG-CONTEXT
AG-SPECIFIC
SYNC-LP-AG
_pipeline.log
```

### Dateiendung

Nur `.md`, ebenfalls case-insensitiv über `p.suffix.lower()`.

### Größenlimit

Es gibt **kein maximales Dateigrößenlimit**. `size` wird gespeichert, aber nicht als Ausschlussbedingung verwendet. Das einzige Inhaltslimit ist `min_chars=100` nach `text.strip()`.

## 2. Vollständige Ausschluss- und Skip-Tabelle

| Ausschluss-/Skip-Grund | Exakte Bedingung | Codezeile/Funktion | Beispiel | Zähler/Status | Dauerhaft oder temporär |
|---|---|---|---|---|---|
| Denylist / Dateityp | `p.suffix.lower() not in cfg['extensions']` **oder** ein Deny-Pattern ist Teilstring des relativen Pfads | `allowed()`, Zeile 128 | `youtube_transcripts.db`, `03-Projekte/...md`, `.obsidian/...` | `excluded` im aktuellen Scan | Scope-Ausschluss; bei Konfigurations-/Pfadänderung potenziell wieder relevant |
| Nicht allowlisted | `allow_roots` leer **oder** `rel.parts[0] not in allow_roots` | `allowed()`, Zeile 129 | `dottore_evaluation_2026-06-11.md` im Vault-Root | `excluded` im aktuellen Scan | Scope-Ausschluss |
| Zu kurzer Inhalt | `len(t.strip()) < cfg['min_chars']`, aktuell `<100` | `allowed()`, Zeile 131 | Markdown-Datei mit 99 oder weniger Nicht-Whitespace-Zeichen | `excluded` im aktuellen Scan | Inhaltsabhängig; bei späterem Inhalt wieder zulässig |
| Inhaltliches Opt-out | Frontmatter-/Zeilenregex erkennt `draft: true/yes`, `private: true/yes`, `no-graphiti: true/yes` oder `exclude-from-graphiti: true/yes` | `allowed()`, Zeile 131 | `no-graphiti: true` | `excluded` im aktuellen Scan | Inhaltsabhängig |
| Gleicher Inhalt/Hash | Vorhandener Datensatz mit gleichem `content_hash` | `scan()`, Zeilen 140–142 | Datei unverändert gegenüber bestehendem Datensatz | nicht `excluded`; kein neuer Queue-Eintrag | Temporärer Skip des aktuellen Scans; bestehender Status bleibt |
| Geänderte Datei, aber nicht sicher requeuebar | Neuer Hash, aber alter Status nicht in `discovered/queued/retry/failed`, oder Episode-/Request-Referenz vorhanden, oder aktiver Request | `scan()`, Zeilen 143–150 | bereits importierter/aktiver Datensatz mit geändertem Inhalt | nicht `excluded`; bestehender Datensatz bleibt | Temporär bzw. explicit reconciliation erforderlich |
| Datei nicht mehr vorhanden | `relative_path` aus DB fehlt im Vault und Status nicht `deleted/superseded` | `scan()`, Zeilen 154–155 | gelöschte Markdown-Datei | Status `deleted` | Bis Datei wieder vorhanden bzw. Datensatz bereinigt |
| Existing episode, genau eine Übereinstimmung | `/episodes/check` findet genau eine Episode | `run()`, Zeilen 220–225 | Episode passt über Name, Import-Key, Pfad/Hash | `verified`, danach `done`; kein neuer POST | Kein neuer Import erforderlich |
| Mehrere passende Episoden | `/episodes/check` findet mehr als eine Episode | `run()`, Zeilen 221–224 | mehrere Episoden für denselben Import-Key/Pfad/Hash | Status `duplicate`, `errors += 1` | Terminaler Konflikt; nicht normal verarbeitbar |
| Normaler Statusfilter | Ohne explizite IDs werden nur `status in ('queued','retry')` mit `attempts < max_attempts` ausgewählt | `run()`, Zeilen 214–215 | `failed`, `done`, `duplicate`, `processing` | nicht `excluded`; nicht ausgewählt | Temporär/Statuslogik |
| Fehlgeschlagene Queue normal | `failed` ist im normalen Auswahlquery nicht enthalten | `run()`, Zeile 215 | Queue mit `status='failed'` | nicht `excluded`; nicht ausgewählt | Bis expliziter Force-Auswahl oder Statusänderung |
| Retry-Limit erreicht | `attempts >= max_attempts`, aktuell `>=5` | `run()`, Zeilen 213–215 | `retry` mit fünf Versuchen | nicht ausgewählt | Bis expliziter `force_id`-Override |
| Exakte Force-Auswahl | Bei `--force-id` werden alle IDs ausgewählt, deren Status nicht `done`, `duplicate` oder `verified` ist | `run()`, Zeilen 210–213 | `--ids 17 --force-id 17` | nicht `excluded`; wird verarbeitet | Temporärer Override; kann auch `failed`/andere Status erfassen |
| Force-Auswahl ungültig | `force_id` ohne `ids`, mehrere Force-IDs oder Force-ID nicht exakt in einer einzigen ID | `validate_force_selection()`, Zeilen 185–194 | `--force-id 17` ohne `--ids 17` | CLI-Fehler, Returncode 2 | Dauer des Aufrufs |
| Run-Block aktiv | Run-Block aktiv und keine `force_ids` | `run()`, Zeilen 203–206 | normaler Lauf während Block | kein Scan/keine Queue-Auswahl | Temporär bis Block geändert |
| Provider-Block aktiv | `provider_state.json` ist blockiert und `next_allowed_at` noch nicht erreicht | `provider_block_active()`/`run()`, Zeilen 74–85, 203–205 | Rate-Limit-Zustand | kein Scan/keine Queue-Auswahl | Bis Resetzeit bzw. State-Änderung |
| Import-Lock belegt | `import.lock` kann nicht exklusiv gelockt werden | `ImportLock.acquire()`, Zeilen 29–36 | anderer Import läuft | CLI-Abbruch, kein `excluded` | Temporär |
| Maintenance-Modus | Graphiti antwortet 503 | `post()`/`run()`, Zeilen 163–167 und 236–237 | aktiver Maintenance-Lock | Queue bleibt Status, Phase `maintenance_mode`; Run-Block | Temporär |
| HTTP-/Provider-/Timeout-Fehler | POST oder Prüfung scheitert | `post()`/`run()`, Zeilen 163–181 und 234–240 | HTTP 429, 500, 504, Timeout | meist `failed`, `provider_limited` oder Fehlerphase | Retry-/Fehlerlogik, nicht Scope-`excluded` |

## 3. Hidden-/Systemfiles

Es gibt keine separate Prüfung auf:

```text
hidden file
hidden directory
Systemfile
Unix-Mode
Windows-Attribute
```

Solche Dateien werden durch `rglob('*')` grundsätzlich gefunden. Sie werden nur ausgeschlossen, wenn Dateiendung, Pfad-Teilstring, Allow-Root oder Inhaltsregel greift. `.obsidian` und `.git` sind keine spezielle Dateisystemlogik, sondern normale Deny-Pattern-Treffer.

## 4. Hash- und Änderungserkennung

Für jede allowlisted Datei wird der SHA-256 des gelesenen Inhalts berechnet:

```python
h=hashlib.sha256(raw.encode()).hexdigest()
```

Der Import-Key ist:

```text
SHA256("obsidian|" + normalisierter_relativer_Pfad + "|" + content_hash)
```

Regeln:

1. gleicher relativer Pfad + gleicher Content-Hash: kein neuer Queue-Eintrag;
2. geänderter Hash und alter Status `discovered`, `queued`, `retry` oder `failed`, keine Episode/Referenz, kein aktiver Request: Datensatz wird auf `queued` zurückgesetzt;
3. geänderter Hash bei importiertem/aktivem/anderem unsicherem Datensatz: bestehender Datensatz wird behalten; keine automatische Versionierungs-/Reconciliation-Aktion;
4. fehlende Datei: Status wird auf `deleted` gesetzt.

Es gibt keine Größen- oder mtime-basierte Änderungserkennung anstelle des Hashes.

## 5. Status- und Queue-Logik

### Normaler Lauf

```sql
select * from files
where status in ("queued","retry")
and attempts < max_attempts
order by id
limit ?
```

`max_attempts` ist aktuell 5.

### Explizite Force-Auswahl

```sql
select * from files
where id in (...)
and status not in ("done","duplicate","verified")
order by id
```

Damit kann `--force-id` auch `failed` und andere nicht-terminale Status erfassen. Die ID-Validierung verlangt eine exakt passende einzelne `--ids`-Auswahl.

### Statusübergänge bei normaler Verarbeitung

```text
preflight
processing
graphiti_created
verified
done
```

Bei mehreren passenden Episoden:

```text
duplicate
```

Bei normalen Fehlern:

```text
failed
```

Bei Rate-Limit:

```text
provider_limited
```

Bei Maintenance:

```text
maintenance_deferred
```

## 6. Exakte Berechnung von `excluded`

Im produktiven Importer wird `excluded` ausschließlich in `scan()` berechnet:

```python
scanned += 1
ok, val = allowed(p, CFG)
if not ok:
    excluded += 1
    continue
```

`excluded` ist somit die Anzahl der im aktuellen Vault-Scan gefundenen Dateien, für die `allowed()` den ersten Ausschlussgrund liefert.

Es ist **nicht**:

- Anzahl aller `failed`-Queues;
- Anzahl aller `retry`-Queues;
- Anzahl der bereits importierten Dateien;
- Anzahl gleicher Hashes;
- Anzahl vorhandener Episoden;
- Anzahl wegen `max_attempts` nicht ausgewählter Queues;
- Anzahl von Dateien, die wegen Run-Block/Provider-Block nicht gestartet wurden;
- Anzahl von Status- oder Hash-Skips.

Bei einem Lauf mit expliziten IDs ist `scan_enabled=False`; der Importer setzt dann mechanisch:

```python
scanned=allowed=excluded=0
```

Das erklärt, warum kontrollierte `--ids ... --force-id ...`-Läufe `excluded=0` melden, unabhängig vom Vault-Inhalt.

Der letzte aktuelle Importer-Lauf war ein expliziter Einzelaufruf und enthielt daher:

```text
scanned=0
allowed=0
excluded=0
done=1
errors=0
```

### Erstgrund statt Mehrfachzählung

Mehrere Ausschlussbedingungen für dieselbe Datei sind möglich. Der Importer zählt sie aber nicht mehrfach:

1. Dateiendung oder Deny-Pattern wird gemeinsam als `denylist` geprüft;
2. nur wenn das nicht greift, wird `not-allowlisted` geprüft;
3. nur wenn beides nicht greift, wird `content` geprüft;
4. genau eine Datei erhöht `excluded` höchstens einmal.

Damit ist `excluded` eine First-Match-Zählung nach Code-Reihenfolge. Der Wert fasst mehrere mögliche Ursachen in einer einzigen Kategorie zusammen und enthält keine Ursache-pro-Datei-Aufschlüsselung.

Read-only Nachbildung am Prüfzeitpunkt:

```text
Vault-Dateien gescannt: 2221
First-reason denylist: 1613
First-reason not-allowlisted: 423
First-reason content: 1
allowed: 184
Lesefehler: 0
Dateien mit mehreren gleichzeitig zutreffenden Rohbedingungen: 1573
```

Diese 2221er-Zahl ist ein unabhängiger read-only Snapshot und kein neuer Importlauf. Sie wurde nicht in die Importer-Datenbank geschrieben.

## 7. Dashboard-Einordnung

Der produktive Importer speichert `excluded` in der Tabelle `runs` als Feld pro Scanlauf:

```text
runs.excluded
```

Der aktive Hermes-Dashboard-Dienst läuft unter:

```text
hermes-dashboard.service
HERMES_HOME=/home/hermes/.hermes/profiles/hermesvps
Port=9119
```

Die ohne Authentifizierung geprüften vermuteten Pipeline-Routen antworteten mit HTTP 401; ein aktueller Dashboard-Payload mit `obsidian.excluded` konnte daher nicht direkt aus dem aktiven Dashboard gelesen werden.

Im vorhandenen Pipeline-Aggregator-Staging-Code wird `obsidian.excluded` separat als `files_total - import_relevant` berechnet. Das ist eine Aggregator-Sicht und darf nicht automatisch mit `runs.excluded` gleichgesetzt werden, solange die produktive Dashboard-Quelle nicht direkt nachgewiesen ist.

Daher gilt fachlich:

```text
Importer runs.excluded = First-Match-Scope-Ausschlüsse des konkreten Scanlaufs
Aggregator obsidian.excluded = eigener Filesystem-/Scope-Snapshot, sofern diese Quelle produktiv verwendet wird
```

Diese beiden Zahlen sind getrennt zu behandeln.

## 8. Antworten auf die Kernfragen

| Frage | Ergebnis |
|---|---|
| Mehrere Gründe für dieselbe Datei möglich? | Ja, Rohbedingungen können gleichzeitig zutreffen. |
| Mehrfach in `excluded` gezählt? | Nein, höchstens einmal; First-Match-Reihenfolge. |
| Ist `excluded` nur Scope? | Ja, im Importer ist es Scope-/Scan-Ausschluss, nicht Status-/Retry-/Duplikataggregation. |
| Fasst `excluded` mehrere Ursachen zusammen? | Ja, mindestens Denylist/Endung, nicht allowlisted und Content werden auf eine Zahl projiziert. |
| Gibt es Größenlimits? | Nein, kein maximales Dateigrößenlimit. |
| Gibt es Hidden-/Systemfile-Regeln? | Keine separaten; nur normale Pfad-/Endungs-/Inhaltsprüfungen. |
| Sind `failed` normal queue-berechtigt? | Nein; normal nur `queued` und `retry` mit attempts < 5. |
| Kann Force `failed` auswählen? | Ja, bei exakt validierter `--ids X --force-id X`-Auswahl. |
| Werden gleiche Hashes als excluded gezählt? | Nein, sie werden im Scan übersprungen, bleiben aber allowed. |
| Werden vorhandene Episoden als excluded gezählt? | Nein; bei genau einer Episode erfolgt `verified` → `done`, bei mehreren `duplicate`. |

## Abschluss

```text
Importer-Ausschlusslogik: vollständig nachgewiesen
allow_roots/deny_patterns: aktuell aus config.json dokumentiert
excluded-Berechnung: First-Match-Scope-Zähler pro Scanlauf
Mehrfachgründe: möglich, aber nicht mehrfach gezählt
Status-/Hash-/Episode-Skips: nicht in excluded enthalten
Dateien verändert: NEIN
Import gestartet: NEIN
```

Arbeitsauftrag AA-040 erledigt.'''
Path('/opt/struktur/knowledge-pipeline-aggregator-staging/AA-040.md').write_text(report)
print(hashlib.sha256(report.encode()).hexdigest())