#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ FILE: scripts/report_hashes.py VERSION: 2.1.0 (2025-12-15) STATUS: Active COMPATIBILITY: v2.9.1 (Post-WP14/WP-15b) Zweck: ------- Erstellt Übersicht über Hash-Signaturen in Note-Payloads. Meldet fehlende Soll-Keys für Multi-Hash Change Detection. Funktionsweise: --------------- 1. Lädt alle Notes aus {prefix}_notes 2. Prüft für jede Note das `hashes` Feld 3. Vergleicht vorhandene Keys mit Soll-Keys 4. Meldet fehlende Keys pro Note Ergebnis-Interpretation: ------------------------ - Ausgabe: JSON mit Hash-Übersicht * notes_total: Anzahl geprüfter Notizen * notes_with_missing: Anzahl Notizen mit fehlenden Keys * missing_keys: Liste fehlender Keys pro Note - Exit-Code 0: Keine fehlenden Keys (oder --fail-on-missing nicht gesetzt) - Exit-Code 2: Fehlende Keys gefunden (nur mit --fail-on-missing) Verwendung: ----------- - CI/CD-Validierung der Hash-Integrität - Audit nach Migrationen - Prüfung der Change Detection Funktionalität Hinweise: --------- - Multi-Hash Format: :: - Standard Soll-Keys: body:parsed:canonical, full:parsed:canonical - Kann zusätzliche Keys via --require anfordern Aufruf: ------- python3 -m scripts.report_hashes --prefix mindnet python3 -m scripts.report_hashes --require frontmatter:raw:none --fail-on-missing Parameter: ---------- --prefix TEXT Collection-Präfix (überschreibt ENV COLLECTION_PREFIX) --require KEY ... Zusätzliche Soll-Keys (Default: body:parsed:canonical, full:parsed:canonical) --fail-on-missing Exit-Code 2, wenn fehlende Keys gefunden werden Änderungen: ----------- v2.1.0 (2025-12-15): Dokumentation aktualisiert v1.0.0 (2025-09-10): Initial Release """ from __future__ import annotations import argparse import json import os from typing import List, Dict, Any from qdrant_client.http import models as rest from app.core.database.qdrant import QdrantConfig, get_client def collections(prefix: str): return f"{prefix}_notes", f"{prefix}_chunks", f"{prefix}_edges" def _scroll_all(client, collection: str): out = [] nextp = None while True: pts, nextp = client.scroll(collection_name=collection, with_payload=True, with_vectors=False, limit=256, offset=nextp) if not pts: break out.extend(pts) if nextp is None: break return out def main(): ap = argparse.ArgumentParser() ap.add_argument("--prefix", help="Collection-Prefix (überschreibt ENV COLLECTION_PREFIX)") ap.add_argument("--require", nargs="+", help="Zusätzliche Soll-Keys ::") ap.add_argument("--fail-on-missing", action="store_true", help="Exitcode 2 bei fehlenden Keys") args = ap.parse_args() cfg = QdrantConfig.from_env() if args.prefix: cfg.prefix = args.prefix.strip() client = get_client(cfg) notes_col, _, _ = collections(cfg.prefix) pts = _scroll_all(client, notes_col) required = set(args.require or []) required |= { "body:parsed:canonical", "frontmatter:parsed:canonical", "full:parsed:canonical", } missing_total = 0 for p in pts: pl = p.payload or {} nid = pl.get("note_id") hashes = pl.get("hashes") or {} present = set(hashes.keys()) missing = sorted(list(required - present)) obj = { "note_id": nid, "present_count": len(present), "missing": missing, } print(json.dumps(obj, ensure_ascii=False)) missing_total += len(missing) if args.fail_on_missing and missing_total > 0: raise SystemExit(2) print(json.dumps({"summary_missing_total": missing_total})) if __name__ == "__main__": main()