.vault-tools (ohne trading-plattform-pruefung und watermarks-remover), .claude/agents, .claude/skills, .claude/workflows, .claude/settings.json. Co-Authored-By: Claude Opus 5.5 <[email protected]>
491 lines
20 KiB
Python
491 lines
20 KiB
Python
"""Hängt verwaiste Notizen einmalig an ihren Hub an: nur anhängen, nie ersetzen.
|
|
|
|
Ersteller: Pascal Michel
|
|
Erstellt: 2026-09-29
|
|
Plan: docs/plans/2026-09-29-waisen/ im selben Ordner
|
|
|
|
Ohne --anwenden ist es ein Probelauf: Er listet je Zieldatei den Text, der
|
|
angehängt würde, und schreibt nichts. Mit --anwenden kopiert das Skript jede
|
|
Zieldatei zuerst in den Sicherungsordner außerhalb des Vaults, prüft jede
|
|
Kopie Byte für Byte und hängt erst danach an. Das Zeilenende jeder Datei
|
|
bleibt, wie es war. Kein Commit, kein git add.
|
|
|
|
REGELN, alle über Muster, ohne Namen einzelner Projekte oder Kunden
|
|
Code Projekte/<p>/docs/plans/<ordner>/<datei>.md
|
|
00-uebersicht.md desselben Ordners bekommt eine Zeile mit relativem Link
|
|
aufs Projekt-README, wenn sie es nicht schon verlinkt, und den Abschnitt
|
|
"Dateien in diesem Ordner" mit relativen Links auf jede .md im Ordner.
|
|
Taskdateien fasst das Skript nie an.
|
|
Code Projekte/<p>/CHANGELOG.md und jede Datei unter Code Projekte/<p>/docs/
|
|
außerhalb von Planordnern
|
|
eine Zeile mit relativem Link aufs Projekt-README.
|
|
docs/plans/<ordner>/<datei>.md an der Vault-Wurzel
|
|
wie ein Planordner in Code Projekte, aber die Zeile heißt "Vault:" und
|
|
verlinkt README.md der Vault-Wurzel.
|
|
jede andere Datei unter docs/ an der Vault-Wurzel
|
|
eine Zeile mit relativem Link auf README.md der Vault-Wurzel.
|
|
IT Workspace/**/_arbeitsstand/**
|
|
Planordner wie oben, aber der Link nach oben ist ein Wikilink auf die
|
|
einzige _MOC-*.md im Ordner über _arbeitsstand; jede andere Datei
|
|
bekommt eine Zeile mit diesem Wikilink.
|
|
IT Workspace/**/findings/<datei>.md
|
|
der Bericht im Ordner über findings (ABSCHLUSSBERICHT.md, sonst
|
|
FULL-AUDIT-REPORT.md) bekommt den Abschnitt "Befunddateien dieses Laufs"
|
|
mit einem Wikilink mit vollem Pfad je Datei.
|
|
AI/secondbrain/<ordner>/JJJJ-MM-TT-<thema>.md
|
|
eine Zeile "Tagesnotiz: [[Inbox/JJJJ-MM-TT]]", nur wenn die Tagesnotiz
|
|
existiert.
|
|
Alles andere kommt auf die Liste für den Koordinator. Geraten wird nichts.
|
|
|
|
ÜBERSPRUNGEN UND AUFGELISTET wird eine Zieldatei, die
|
|
* in einem Git-Repo im Vault liegt, auch dem der Vault-Wurzel, und dort in
|
|
irgendeinem Arbeitsbaum uncommittet geändert oder neu ist oder auf einem
|
|
anderen lokalen Zweig seit dem gemeinsamen Stand geändert wurde; was das
|
|
Repo ignoriert, zählt nicht als geändert,
|
|
* nicht als UTF-8 lesbar ist oder gemischte Zeilenenden hat,
|
|
* die anzuhängende Überschrift oder Linkzeile schon enthält.
|
|
Ein Git-Repo, das sich nicht prüfen lässt, sperrt alle seine Zieldateien.
|
|
|
|
BENANNTE LÜCKEN
|
|
* Welche Notiz eine Waise ist, entscheidet waisen.py mit dessen Lücken.
|
|
* Nur lokale Zweige zählen, entfernte nicht.
|
|
* Eine Datei, die zwischen dem Lesen und dem Anhängen geändert wird, bricht
|
|
den Lauf ab; was bis dahin angehängt war, nennt die Ausgabe.
|
|
|
|
Exit 0: Lauf durch. Exit 2: abgebrochen, die Ursache steht in der Ausgabe.
|
|
|
|
Aufruf:
|
|
python .vault-tools/linkcheck/waisen_bestand.py --vault D:/Claude
|
|
python .vault-tools/linkcheck/waisen_bestand.py --vault D:/Claude --anwenden --sicherung <ordner außerhalb des Vaults>
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import traceback
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
from urllib.parse import quote
|
|
|
|
HIER = Path(__file__).resolve().parent
|
|
if str(HIER) not in sys.path:
|
|
sys.path.insert(0, str(HIER))
|
|
|
|
try:
|
|
import waisen
|
|
_IMPORTFEHLER = ""
|
|
except Exception as _fehler:
|
|
waisen = None
|
|
_IMPORTFEHLER = f"{type(_fehler).__name__}: {_fehler}"
|
|
|
|
ORDNER_KOPF = "## Dateien in diesem Ordner"
|
|
BEFUND_KOPF = "## Befunddateien dieses Laufs"
|
|
BERICHTE = ("ABSCHLUSSBERICHT.md", "FULL-AUDIT-REPORT.md")
|
|
_DATUM = re.compile(r"^(\d{4}-\d{2}-\d{2})")
|
|
GESCHRIEBEN = []
|
|
|
|
|
|
class Abbruch(Exception):
|
|
"""Der Lauf kann nicht sicher weiter."""
|
|
|
|
|
|
@dataclass
|
|
class Ziel:
|
|
rel: str
|
|
zeilen: list = field(default_factory=list)
|
|
ordner: Optional[str] = None
|
|
befunde: Optional[str] = None
|
|
ausloeser: list = field(default_factory=list)
|
|
hubs: set = field(default_factory=set)
|
|
gelistet: list = field(default_factory=list)
|
|
|
|
|
|
def existiert(vault: Path, rel: str) -> bool:
|
|
return (vault / rel).is_file()
|
|
|
|
|
|
def rel_link(von: str, zu: str) -> str:
|
|
start = von.split("/")[:-1]
|
|
ziel = zu.split("/")
|
|
gemeinsam = 0
|
|
while gemeinsam < min(len(start), len(ziel) - 1) and start[gemeinsam] == ziel[gemeinsam]:
|
|
gemeinsam += 1
|
|
teile = [".."] * (len(start) - gemeinsam) + ziel[gemeinsam:]
|
|
return quote("/".join(teile), safe="/")
|
|
|
|
|
|
def _code_projekt(rel: str, teile: list, vault: Path) -> tuple:
|
|
readme = "/".join(teile[:2]) + "/README.md"
|
|
innen = teile[2:]
|
|
if innen[:2] == ["docs", "plans"] and len(innen) == 4:
|
|
uebersicht = "/".join(teile[:-1]) + "/00-uebersicht.md"
|
|
if not existiert(vault, uebersicht):
|
|
return ("rest", "", "Planordner ohne 00-uebersicht.md", "")
|
|
if not existiert(vault, readme):
|
|
return ("rest", "", f"{readme} fehlt", "")
|
|
return ("ordner", uebersicht, f"Projekt: [README]({rel_link(uebersicht, readme)})", readme)
|
|
if innen[:2] == ["docs", "plans"] and len(innen) > 4:
|
|
return ("rest", "", "Plandatei tiefer als ein Ordner unter docs/plans", "")
|
|
if innen == ["CHANGELOG.md"] or (innen[:1] == ["docs"] and len(innen) >= 2):
|
|
if not existiert(vault, readme):
|
|
return ("rest", "", f"{readme} fehlt", "")
|
|
return ("zeile", rel, f"Projekt: [README]({rel_link(rel, readme)})", readme)
|
|
return ("rest", "", "keine Regel passt", "")
|
|
|
|
|
|
def _arbeitsstand(rel: str, teile: list, vault: Path) -> tuple:
|
|
stelle = teile.index("_arbeitsstand")
|
|
projekt = "/".join(teile[:stelle])
|
|
try:
|
|
mocs = sorted(
|
|
name for name in os.listdir(vault / projekt)
|
|
if name.startswith("_MOC-") and name.lower().endswith(".md") and (vault / projekt / name).is_file()
|
|
)
|
|
except OSError as fehler:
|
|
return ("rest", "", f"Projektordner nicht lesbar ({fehler})", "")
|
|
if len(mocs) != 1:
|
|
return ("rest", "", f"{len(mocs)} _MOC-Dateien in {projekt}, erwartet genau eine", "")
|
|
moc = f"{projekt}/{mocs[0]}"
|
|
zeile = f"Projekt: [[{moc[:-3]}]]"
|
|
innen = teile[stelle + 1:]
|
|
if innen[:2] == ["docs", "plans"] and len(innen) == 4:
|
|
uebersicht = "/".join(teile[:-1]) + "/00-uebersicht.md"
|
|
if not existiert(vault, uebersicht):
|
|
return ("rest", "", "Planordner ohne 00-uebersicht.md", "")
|
|
return ("ordner", uebersicht, zeile, moc)
|
|
if innen[:2] == ["docs", "plans"] and len(innen) > 4:
|
|
return ("rest", "", "Plandatei tiefer als ein Ordner unter docs/plans", "")
|
|
return ("zeile", rel, zeile, moc)
|
|
|
|
|
|
def _befund(rel: str, teile: list, vault: Path) -> tuple:
|
|
lauf = "/".join(teile[:-2])
|
|
for name in BERICHTE:
|
|
if existiert(vault, f"{lauf}/{name}"):
|
|
bericht = f"{lauf}/{name}"
|
|
return ("befund", bericht, "/".join(teile[:-1]), bericht)
|
|
return ("rest", "", f"kein Bericht ({' oder '.join(BERICHTE)}) in {lauf}", "")
|
|
|
|
|
|
def _dump(rel: str, teile: list, vault: Path) -> tuple:
|
|
treffer = _DATUM.match(teile[3])
|
|
if not treffer:
|
|
return ("rest", "", "Dump ohne Datum am Anfang des Dateinamens", "")
|
|
tag = treffer.group(1)
|
|
if not existiert(vault, f"Inbox/{tag}.md"):
|
|
return ("rest", "", f"Tagesnotiz Inbox/{tag}.md fehlt", "")
|
|
return ("zeile", rel, f"Tagesnotiz: [[Inbox/{tag}]]", f"Inbox/{tag}.md")
|
|
|
|
|
|
def _wurzel_docs(rel: str, teile: list, vault: Path) -> tuple:
|
|
# Pläne und Specs unter docs/ an der Vault-Wurzel: Hub ist README.md dort.
|
|
hub = "README.md"
|
|
if not existiert(vault, hub):
|
|
return ("rest", "", "README.md der Vault-Wurzel fehlt", "")
|
|
if teile[1] == "plans" and len(teile) == 4:
|
|
kopf = "/".join(teile[:-1]) + "/00-uebersicht.md"
|
|
if not existiert(vault, kopf):
|
|
return ("rest", "", "Planordner ohne 00-uebersicht.md", "")
|
|
return ("ordner", kopf, f"Vault: [README]({rel_link(kopf, hub)})", hub)
|
|
if teile[1] == "plans" and len(teile) > 4:
|
|
return ("rest", "", "Plandatei tiefer als ein Ordner unter docs/plans", "")
|
|
return ("zeile", rel, f"Vault: [README]({rel_link(rel, hub)})", hub)
|
|
|
|
|
|
def klassifiziere(rel: str, vault: Path) -> tuple:
|
|
"""Liefert (art, ziel, wert, hub). art ist zeile, ordner, befund oder rest."""
|
|
teile = rel.split("/")
|
|
if teile[0] == "Code Projekte" and len(teile) >= 3:
|
|
return _code_projekt(rel, teile, vault)
|
|
if teile[0] == "IT Workspace" and "_arbeitsstand" in teile[1:-1]:
|
|
return _arbeitsstand(rel, teile, vault)
|
|
if teile[0] == "IT Workspace" and len(teile) >= 3 and teile[-2] == "findings":
|
|
return _befund(rel, teile, vault)
|
|
if teile[:2] == ["AI", "secondbrain"] and len(teile) == 4:
|
|
return _dump(rel, teile, vault)
|
|
if teile[0] == "docs" and len(teile) >= 2:
|
|
return _wurzel_docs(rel, teile, vault)
|
|
return ("rest", "", "keine Regel passt", "")
|
|
|
|
|
|
def plane(vault: Path, ergebnis) -> tuple:
|
|
ziele = {}
|
|
rest = []
|
|
for rel in ergebnis.waisen:
|
|
art, ziel_rel, wert, hub = klassifiziere(rel, vault)
|
|
if art == "rest":
|
|
rest.append((rel, wert))
|
|
continue
|
|
ziel = ziele.setdefault(ziel_rel, Ziel(ziel_rel))
|
|
ziel.ausloeser.append(rel)
|
|
ziel.hubs.add(hub)
|
|
if art == "befund":
|
|
ziel.befunde = wert
|
|
continue
|
|
if art == "ordner":
|
|
ziel.ordner = ziel_rel.rsplit("/", 1)[0]
|
|
if hub in ergebnis.ausgehend.get(ziel_rel, set()):
|
|
continue
|
|
if wert not in ziel.zeilen:
|
|
ziel.zeilen.append(wert)
|
|
return ziele, rest
|
|
|
|
|
|
def md_dateien(vault: Path, ordner: str) -> list:
|
|
pfad = vault / ordner
|
|
return sorted(
|
|
name for name in os.listdir(pfad)
|
|
if name.lower().endswith(".md") and not name.startswith(".") and (pfad / name).is_file()
|
|
)
|
|
|
|
|
|
def anhang(vault: Path, ziel: Ziel) -> list:
|
|
zeilen = list(ziel.zeilen)
|
|
if ziel.ordner is not None:
|
|
namen = [n for n in md_dateien(vault, ziel.ordner) if n != "00-uebersicht.md"]
|
|
if namen:
|
|
if zeilen:
|
|
zeilen.append("")
|
|
zeilen += [ORDNER_KOPF, ""]
|
|
zeilen += [f"- [{n}]({quote(n, safe='')})" for n in namen]
|
|
ziel.gelistet += [f"{ziel.ordner}/{n}" for n in namen]
|
|
if ziel.befunde is not None:
|
|
namen = md_dateien(vault, ziel.befunde)
|
|
if namen:
|
|
if zeilen:
|
|
zeilen.append("")
|
|
zeilen += [BEFUND_KOPF, ""]
|
|
zeilen += [f"- [[{ziel.befunde}/{n[:-3]}]]" for n in namen]
|
|
ziel.gelistet += [f"{ziel.befunde}/{n}" for n in namen]
|
|
return zeilen
|
|
|
|
|
|
def pruefe_ziel(vault: Path, rel: str, zeilen: list) -> tuple:
|
|
"""Liefert (Grund zum Überspringen oder None, alter Inhalt, Zeilenende)."""
|
|
alt = (vault / rel).read_bytes()
|
|
try:
|
|
text = alt.decode("utf-8")
|
|
except UnicodeDecodeError:
|
|
return "nicht als UTF-8 lesbar", alt, ""
|
|
crlf = alt.count(b"\r\n")
|
|
lf = alt.count(b"\n") - crlf
|
|
if crlf and lf:
|
|
return "gemischte Zeilenenden", alt, ""
|
|
ende = "\r\n" if crlf else "\n"
|
|
vorhanden = text.replace("\r\n", "\n").split("\n")
|
|
for zeile in zeilen:
|
|
if zeile and not zeile.startswith("- "):
|
|
if zeile in vorhanden:
|
|
return f"enthält schon die Zeile {zeile!r}", alt, ende
|
|
return None, alt, ende
|
|
|
|
|
|
def anhang_bytes(alt: bytes, zeilen: list, ende: str) -> bytes:
|
|
vorne = ende if alt and not alt.endswith(b"\n") else ""
|
|
return (vorne + ende + ende.join(zeilen) + ende).encode("utf-8")
|
|
|
|
|
|
def repo_von(vault: Path, rel: str) -> Optional[Path]:
|
|
ordner = (vault / rel).parent
|
|
# Auch die Vault-Wurzel kann ein Repo sein; über ihr wird nicht gesucht.
|
|
while ordner == vault or vault in ordner.parents:
|
|
if (ordner / ".git").exists():
|
|
return ordner
|
|
if ordner == vault:
|
|
return None
|
|
ordner = ordner.parent
|
|
return None
|
|
|
|
|
|
def git(ordner: Path, *argumente: str) -> bytes:
|
|
umgebung = dict(os.environ)
|
|
# Kein Index-Auffrischen: der Probelauf schreibt auch in .git nichts.
|
|
umgebung["GIT_OPTIONAL_LOCKS"] = "0"
|
|
try:
|
|
lauf = subprocess.run(
|
|
["git", "-C", str(ordner), *argumente],
|
|
capture_output=True,
|
|
stdin=subprocess.DEVNULL,
|
|
timeout=120,
|
|
env=umgebung,
|
|
)
|
|
except (OSError, subprocess.TimeoutExpired) as fehler:
|
|
raise Abbruch(f"git {' '.join(argumente)} in {ordner}: {fehler}") from fehler
|
|
if lauf.returncode != 0:
|
|
meldung = lauf.stderr.decode("utf-8", errors="replace").strip()
|
|
raise Abbruch(f"git {' '.join(argumente)} in {ordner}: Exit {lauf.returncode}: {meldung}")
|
|
return lauf.stdout
|
|
|
|
|
|
def geaenderte_pfade(repo: Path) -> dict:
|
|
"""Pfad relativ zum Repo, klein geschrieben, und wo er geändert ist."""
|
|
wo = {}
|
|
liste = git(repo, "worktree", "list", "--porcelain").decode("utf-8", errors="replace")
|
|
baeume = [z[len("worktree "):].strip() for z in liste.split("\n") if z.startswith("worktree ")]
|
|
if not baeume:
|
|
raise Abbruch("git worktree list nennt keinen Arbeitsbaum")
|
|
for baum in baeume:
|
|
roh = git(Path(baum), "status", "--porcelain", "-z", "--untracked-files=all")
|
|
eintraege = roh.decode("utf-8", errors="replace").split("\0")
|
|
i = 0
|
|
while i < len(eintraege):
|
|
eintrag = eintraege[i]
|
|
i += 1
|
|
if len(eintrag) < 4:
|
|
continue
|
|
wo.setdefault(eintrag[3:].lower(), f"Arbeitsbaum {baum}")
|
|
if "R" in eintrag[:2] or "C" in eintrag[:2]:
|
|
if i < len(eintraege) and eintraege[i]:
|
|
wo.setdefault(eintraege[i].lower(), f"Arbeitsbaum {baum}")
|
|
i += 1
|
|
aktuell = git(repo, "rev-parse", "--abbrev-ref", "HEAD").decode("utf-8", errors="replace").strip()
|
|
zweige = git(repo, "for-each-ref", "--format=%(refname:short)", "refs/heads/").decode("utf-8", errors="replace")
|
|
for zweig in (z.strip() for z in zweige.split("\n")):
|
|
if not zweig or zweig == aktuell:
|
|
continue
|
|
diff = git(repo, "diff", "--name-only", "-z", f"HEAD...{zweig}", "--")
|
|
for pfad in diff.decode("utf-8", errors="replace").split("\0"):
|
|
if pfad:
|
|
wo.setdefault(pfad.lower(), f"Zweig {zweig}")
|
|
return wo
|
|
|
|
|
|
def pruefe_alle(vault: Path, ziele: dict) -> tuple:
|
|
repos = {}
|
|
fertig = []
|
|
uebersprungen = []
|
|
for rel in sorted(ziele):
|
|
ziel = ziele[rel]
|
|
zeilen = anhang(vault, ziel)
|
|
repo = repo_von(vault, rel)
|
|
if repo is not None:
|
|
if repo not in repos:
|
|
try:
|
|
repos[repo] = geaenderte_pfade(repo)
|
|
except Abbruch as fehler:
|
|
repos[repo] = str(fehler)
|
|
stand = repos[repo]
|
|
if isinstance(stand, str):
|
|
uebersprungen.append((ziel, f"Git-Repo nicht prüfbar: {stand}"))
|
|
continue
|
|
innen = (vault / rel).relative_to(repo).as_posix().lower()
|
|
if innen in stand:
|
|
uebersprungen.append((ziel, f"in Git geändert ({stand[innen]})"))
|
|
continue
|
|
grund, alt, ende = pruefe_ziel(vault, rel, zeilen)
|
|
if grund:
|
|
uebersprungen.append((ziel, grund))
|
|
continue
|
|
fertig.append((ziel, zeilen, alt, ende))
|
|
return fertig, uebersprungen
|
|
|
|
|
|
def zeige(vault: Path, anwenden: bool, vorher: int, fertig: list, uebersprungen: list, rest: list, mitverbunden: int) -> None:
|
|
print(f"# Bestandslauf - {'Anwenden' if anwenden else 'Probelauf'} - {vault}")
|
|
print(f"Waisen außerhalb der Ausnahmen vorher: {vorher}")
|
|
print()
|
|
print(f"## Anhänge ({len(fertig)} Dateien)")
|
|
for ziel, zeilen, _alt, ende in fertig:
|
|
art = "CRLF" if ende == "\r\n" else "LF"
|
|
print(f"ZIEL {ziel.rel} [{art}] wegen {len(ziel.ausloeser)} Waise(n)")
|
|
for zeile in zeilen:
|
|
print(f" | {zeile}")
|
|
print()
|
|
print(f"## Übersprungen ({len(uebersprungen)})")
|
|
for ziel, grund in uebersprungen:
|
|
print(f"UEBERSPRUNGEN {ziel.rel}: {grund} (betrifft {len(ziel.ausloeser)} Waise(n))")
|
|
print()
|
|
print(f"## Liste für den Koordinator ({len(rest)})")
|
|
for rel, grund in rest:
|
|
print(f"REST {rel}: {grund}")
|
|
print(f"Durch Anhänge mitverbunden, ohne eigene Regel: {mitverbunden}")
|
|
|
|
|
|
def sichern_und_anhaengen(vault: Path, sicherung: Path, fertig: list) -> None:
|
|
sicherung.mkdir(parents=True)
|
|
for ziel, _zeilen, alt, _ende in fertig:
|
|
kopie = sicherung / ziel.rel
|
|
kopie.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(vault / ziel.rel, kopie)
|
|
if kopie.read_bytes() != alt:
|
|
raise Abbruch(f"Sicherung weicht vom Original ab: {kopie}")
|
|
for ziel, zeilen, alt, ende in fertig:
|
|
pfad = vault / ziel.rel
|
|
if pfad.read_bytes() != alt:
|
|
raise Abbruch(f"{ziel.rel} hat sich seit dem Lesen geändert")
|
|
neu = anhang_bytes(alt, zeilen, ende)
|
|
with open(pfad, "ab") as datei:
|
|
datei.write(neu)
|
|
GESCHRIEBEN.append(ziel.rel)
|
|
if pfad.read_bytes() != alt + neu:
|
|
raise Abbruch(f"{ziel.rel}: Inhalt nach dem Anhängen weicht ab")
|
|
(sicherung / "GEAENDERT.txt").write_text("\n".join(GESCHRIEBEN) + "\n", encoding="utf-8", newline="\n")
|
|
|
|
|
|
def lauf(vault: Path, ausnahmen: Path, anwenden: bool, sicherung) -> int:
|
|
GESCHRIEBEN.clear()
|
|
if waisen is None:
|
|
raise Abbruch(f"waisen.py nicht importierbar ({_IMPORTFEHLER})")
|
|
sicherung_pfad = None
|
|
if anwenden:
|
|
if not sicherung:
|
|
raise Abbruch("--anwenden braucht --sicherung mit einem Ordner außerhalb des Vaults")
|
|
sicherung_pfad = Path(os.path.abspath(sicherung)).resolve()
|
|
if sicherung_pfad == vault or vault in sicherung_pfad.parents:
|
|
raise Abbruch(f"Sicherungsordner liegt im Vault: {sicherung_pfad}")
|
|
if sicherung_pfad.exists():
|
|
raise Abbruch(f"Sicherungsordner existiert schon: {sicherung_pfad}")
|
|
ergebnis = waisen.analysiere(vault, ausnahmen)
|
|
ziele, rest = plane(vault, ergebnis)
|
|
fertig, uebersprungen = pruefe_alle(vault, ziele)
|
|
verbunden = set()
|
|
for ziel, _zeilen, _alt, _ende in fertig:
|
|
verbunden.add(ziel.rel)
|
|
verbunden.update(ziel.hubs)
|
|
verbunden.update(ziel.gelistet)
|
|
anzahl = len(rest)
|
|
rest = [(rel, grund) for rel, grund in rest if rel not in verbunden]
|
|
zeige(vault, anwenden, len(ergebnis.waisen), fertig, uebersprungen, rest, anzahl - len(rest))
|
|
if anwenden:
|
|
sichern_und_anhaengen(vault, sicherung_pfad, fertig)
|
|
nachher = waisen.analysiere(vault, ausnahmen)
|
|
print()
|
|
print(f"Angehängt an {len(GESCHRIEBEN)} Dateien. Sicherung und GEAENDERT.txt: {sicherung_pfad}")
|
|
print(f"Waisen außerhalb der Ausnahmen nachher: {len(nachher.waisen)}")
|
|
return 0
|
|
|
|
|
|
def main(argv=None) -> int:
|
|
zerleger = argparse.ArgumentParser(description="Hängt verwaiste Notizen einmalig an ihren Hub an")
|
|
zerleger.add_argument("--vault", required=True)
|
|
zerleger.add_argument("--ausnahmen", default=str(HIER / "waisen-ausnahmen.txt"))
|
|
zerleger.add_argument("--anwenden", action="store_true")
|
|
zerleger.add_argument("--sicherung")
|
|
args = zerleger.parse_args(argv)
|
|
vault = Path(os.path.abspath(args.vault)).resolve()
|
|
try:
|
|
return lauf(vault, Path(args.ausnahmen), args.anwenden, args.sicherung)
|
|
except Exception as fehler:
|
|
erwartet = isinstance(fehler, Abbruch) or (waisen is not None and isinstance(fehler, waisen.Abbruch))
|
|
art = "" if erwartet else f"{type(fehler).__name__}: "
|
|
print(f"Bestandslauf - ABGEBROCHEN: {art}{fehler}")
|
|
if not erwartet:
|
|
print(traceback.format_exc())
|
|
if GESCHRIEBEN:
|
|
print(f"Vor dem Abbruch schon angehängt ({len(GESCHRIEBEN)}), die Sicherung liegt vor:")
|
|
for rel in GESCHRIEBEN:
|
|
print(f" - {rel}")
|
|
return 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
except AttributeError:
|
|
pass
|
|
sys.exit(main())
|