Erste Fassung: Claude-Werkzeuge aus dem Vault
.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]>
This commit is contained in:
commit
fa1c26b407
137 files changed
+43926
No files matched your search
@@ -0,0 +1,318 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Zieht den Vault auf das Frontmatter-Vokabular aus AI/Regelwerk/frontmatter.md.
|
||||
|
||||
Ersteller: Pascal Michel
|
||||
Erstellt: 2026-09-03
|
||||
|
||||
Drei Dinge, in dieser Reihenfolge:
|
||||
1 Synonyme umbenennen (projekt -> project, task/aufgaben -> todo, ...)
|
||||
2 fehlende type-Werte je Dateiart ergaenzen
|
||||
3 fehlende status-Werte aus dem Standard des Typs ergaenzen
|
||||
|
||||
Vorgabe der Werte: AI/Regelwerk/frontmatter.md. Die Tabellen unten sind die
|
||||
maschinenlesbare Fassung davon - aendert sich die Regel, aendert sich hier.
|
||||
|
||||
AUSGENOMMEN, bewusst:
|
||||
* IT Workspace/ - Artikel mit alter Tag-Konvention, siehe navigation.md
|
||||
* CLAUDE.md - der Stamm ist keine Notiz
|
||||
* MEMORY.md - Verzeichnisse, keine Inhalte
|
||||
* die Spezialbereiche isms-*, bookstack-*, iam-projekt, netz-projekt bleiben unberuehrt
|
||||
|
||||
Ohne --anwenden passiert nichts. Der Trockenlauf ist die Voreinstellung.
|
||||
|
||||
Aufruf:
|
||||
python .vault-tools/frontmatter-migration/migriere.py --vault .
|
||||
python .vault-tools/frontmatter-migration/migriere.py --vault . --anwenden
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import datetime as _dt
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
TOOLKIT = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
"obsidian-vault-navigator", "scripts")
|
||||
|
||||
# ---- Vorgaben aus AI/Regelwerk/frontmatter.md ------------------------------
|
||||
|
||||
UMBENENNEN = {
|
||||
"projekt": "project",
|
||||
"task": "todo",
|
||||
"aufgaben": "todo",
|
||||
"project-todo": "todo",
|
||||
"anleitung": "guide",
|
||||
"checklist": "guide",
|
||||
"architecture": "codedoku",
|
||||
"arbeitsdokument": "note",
|
||||
"reference": "memory", # betraf ausschliesslich Gedaechtnisdumps
|
||||
}
|
||||
|
||||
STATUS_UMBENENNEN = {"approved": "done", "abgeloest": "done"}
|
||||
|
||||
STANDARD_STATUS = {
|
||||
"moc": "reference", "daily": "open", "session": "done", "memory": "reference",
|
||||
"note": "active", "konzept": "draft", "resource": "reference", "guide": "active",
|
||||
"project": "active", "spec": "draft", "plan": "active", "plan-task": "draft",
|
||||
"todo": "active", "lessons": "active", "changelog": "active", "codedoku": "active",
|
||||
}
|
||||
|
||||
SPEZIAL = {"isms-dokument", "isms-kapitel", "bookstack-seite", "bookstack-kapitel",
|
||||
"bookstack-buchbeschreibung", "iam-projekt", "netz-projekt"}
|
||||
|
||||
AUSGENOMMEN_ORDNER = (
|
||||
"IT Workspace/", # alte Tag-Konvention, siehe navigation.md
|
||||
"Bookstack/", # Staging mit eigener #review-Konvention
|
||||
".claude/", # Skills und Agenten des Harness
|
||||
)
|
||||
|
||||
# Diese Pfadbestandteile schliessen aus, egal wo sie stehen. Grund: Es sind
|
||||
# KEINE Vault-Notizen, sondern Code-Artefakte mit eigenem Schema. Ein
|
||||
# type: note in einer SKILL.md kann den Skill-Lader stoeren, und die Dateien
|
||||
# unter tools/testdaten/ sind Prueffaelle - wer sie anfasst, faelscht Tests.
|
||||
AUSGENOMMEN_TEIL = ("/skill/", "/skills/", "/plugin/", "/tools/testdaten/")
|
||||
|
||||
AUSGENOMMEN_DATEI = ("CLAUDE.md",)
|
||||
AUSGENOMMEN_NAME = ("MEMORY.md", "SKILL.md")
|
||||
|
||||
|
||||
# Importierte Normen- und Referenzvolltexte. Fuer sie taugt die maschinelle
|
||||
# Ableitung nicht: der Titel waere der Dateiname, die Zusammenfassung eine
|
||||
# Seitenzahl aus dem Amtsblatt. Deshalb hier von Hand, an einer Stelle.
|
||||
HANDARBEIT = {
|
||||
"Security - Compliance Projekte/active - BSI/C52026en.md": dict(
|
||||
typ="resource", status="reference",
|
||||
titel="BSI C5:2026 — Cloud Computing Compliance Criteria Catalogue",
|
||||
summary="Volltext des BSI-Kriterienkatalogs C5:2026 mit allen 174 Kriterien, englische Fassung. Grundlage des Nachweis-Mappings in den Compliance-Projekten."),
|
||||
"Security - Compliance Projekte/active - NIS2/NIS2_Richtlinie_EU_Recht.md": dict(
|
||||
typ="resource", status="reference",
|
||||
titel="NIS2-Richtlinie (EU) 2022/2555 — Volltext",
|
||||
summary="Volltext der NIS2-Richtlinie aus dem Amtsblatt der Europaeischen Union. Referenzmaterial, noch ohne eigene Auswertung."),
|
||||
"Security - Compliance Projekte/active - NIST/NIST_Richtlinie.md": dict(
|
||||
typ="resource", status="reference",
|
||||
titel="NIST Cybersecurity Framework 2.0 — Volltext",
|
||||
summary="Volltext des NIST Cybersecurity Framework in der Fassung vom 2024-02-26. Referenzmaterial, noch ohne eigene Auswertung."),
|
||||
"Security - Compliance Projekte/active - ISO27001 - 9001/DIN_EN_ISO_9001-2015.md": dict(
|
||||
typ="resource", status="reference",
|
||||
titel="DIN EN ISO 9001:2015 — Volltext",
|
||||
summary="Volltext der DIN EN ISO 9001:2015. Achtung: Die ISO 27001 liegt trotz Ordnername nicht vor."),
|
||||
"Security - Compliance Projekte/active - HIPAA/hipaa201303.md": dict(
|
||||
typ="resource", status="reference",
|
||||
titel="HIPAA — Omnibus Final Rule 2013, Volltext",
|
||||
summary="Volltext der HIPAA-Regelung in der Fassung von 2013. Referenzmaterial ohne eigene Auswertung."),
|
||||
}
|
||||
|
||||
|
||||
def _yaml(wert):
|
||||
"""YAML-sicherer Skalar. Doppelpunkte werden gequotet, nicht ersetzt -
|
||||
'C5:2026' zu 'C5 -2026' zu verstuemmeln war der erste, falsche Versuch."""
|
||||
wert = " ".join(str(wert).split())
|
||||
if any(z in wert for z in ':#[]{}&*!|>%@`"') or wert.startswith(("-", "?")):
|
||||
return '"%s"' % wert.replace('"', "'")
|
||||
return wert
|
||||
|
||||
|
||||
def typ_aus_pfad(rel):
|
||||
"""Typ je Dateiart. Reihenfolge zaehlt: spezifisch vor allgemein."""
|
||||
name = os.path.basename(rel)
|
||||
if rel.startswith("AI/sessions/"):
|
||||
return "session"
|
||||
if rel.startswith("AI/secondbrain/"):
|
||||
return "memory"
|
||||
if rel.startswith("AI/Regelwerk/"):
|
||||
return "resource"
|
||||
if rel.startswith("Inbox/") and re.match(r"^\d{4}-\d{2}-\d{2}\.md$", name):
|
||||
return "daily"
|
||||
if name.startswith("_MOC-") or name == "Wissenslandkarte.md":
|
||||
return "moc"
|
||||
if "/docs/superpowers/specs/" in rel:
|
||||
return "spec"
|
||||
if "/docs/code/" in rel:
|
||||
return "codedoku"
|
||||
if "/docs/plans/" in rel:
|
||||
# Planordner mit mehreren Tasks: Einzeldateien sind plan-task
|
||||
rest = rel.split("/docs/plans/", 1)[1]
|
||||
return "plan-task" if "/" in rest else "plan"
|
||||
if name == "README.md":
|
||||
# Nur direkt im Projektordner: "Code Projekte/<projekt>/README.md".
|
||||
# Tiefer liegende README-Dateien beschreiben ein Unterverzeichnis.
|
||||
return "project" if rel.count("/") == 2 else "note"
|
||||
if name == "todo.md":
|
||||
return "todo"
|
||||
if name == "CHANGELOG.md":
|
||||
return "changelog"
|
||||
if name == "lessons.md":
|
||||
return "lessons"
|
||||
if name == "Administrator-Anleitung.md":
|
||||
return "guide"
|
||||
return "note"
|
||||
|
||||
|
||||
# ---- Mechanik --------------------------------------------------------------
|
||||
|
||||
FM_RE = re.compile(r"\A---\s*\n(.*?)\n---\s*\n", re.DOTALL)
|
||||
FELD_RE = re.compile(r"^(\w+):\s*(.*)$")
|
||||
REIHENFOLGE = ("uid", "type", "title", "status", "summary", "tags", "created", "updated")
|
||||
|
||||
|
||||
def ausgenommen(rel):
|
||||
if rel in AUSGENOMMEN_DATEI or os.path.basename(rel) in AUSGENOMMEN_NAME:
|
||||
return True
|
||||
if any(rel.startswith(o) for o in AUSGENOMMEN_ORDNER):
|
||||
return True
|
||||
return any(teil in "/" + rel for teil in AUSGENOMMEN_TEIL)
|
||||
|
||||
|
||||
def lies_block(text):
|
||||
m = FM_RE.match(text)
|
||||
if not m:
|
||||
return None, text
|
||||
zeilen = m.group(1).split("\n")
|
||||
return zeilen, text[m.end():]
|
||||
|
||||
|
||||
def feld(zeilen, name):
|
||||
for z in zeilen:
|
||||
m = FELD_RE.match(z)
|
||||
if m and m.group(1) == name:
|
||||
return m.group(2).strip()
|
||||
return ""
|
||||
|
||||
|
||||
def setze(zeilen, name, wert):
|
||||
"""Setzt oder ergaenzt ein Feld und haelt die Reihenfolge ein."""
|
||||
for i, z in enumerate(zeilen):
|
||||
m = FELD_RE.match(z)
|
||||
if m and m.group(1) == name:
|
||||
zeilen[i] = "%s: %s" % (name, wert)
|
||||
return zeilen
|
||||
# neu einsortieren
|
||||
pos = REIHENFOLGE.index(name) if name in REIHENFOLGE else len(REIHENFOLGE)
|
||||
einfuege = len(zeilen)
|
||||
for i, z in enumerate(zeilen):
|
||||
m = FELD_RE.match(z)
|
||||
if not m:
|
||||
continue
|
||||
p = REIHENFOLGE.index(m.group(1)) if m.group(1) in REIHENFOLGE else len(REIHENFOLGE)
|
||||
if p > pos:
|
||||
einfuege = i
|
||||
break
|
||||
zeilen.insert(einfuege, "%s: %s" % (name, wert))
|
||||
return zeilen
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description="Frontmatter auf das Vokabular ziehen")
|
||||
p.add_argument("--vault", default=".")
|
||||
p.add_argument("--anwenden", action="store_true")
|
||||
p.add_argument("--neu-anlegen", dest="neu_anlegen", action="store_true",
|
||||
help="auch Dateien ganz ohne Frontmatter versehen")
|
||||
args = p.parse_args()
|
||||
vault = os.path.abspath(os.path.expanduser(args.vault))
|
||||
|
||||
if TOOLKIT not in sys.path:
|
||||
sys.path.insert(0, TOOLKIT)
|
||||
try:
|
||||
import vaultlib as V
|
||||
except ImportError as exc:
|
||||
sys.stderr.write("ABBRUCH: vaultlib nicht importierbar (%s)\n" % exc)
|
||||
return 2
|
||||
|
||||
aenderungen = []
|
||||
try:
|
||||
for ap, rel in V.walk_vault(vault):
|
||||
if not rel.endswith(".md") or ausgenommen(rel):
|
||||
continue
|
||||
text = io.open(ap, encoding="utf-8").read()
|
||||
zeilen, rumpf = lies_block(text)
|
||||
if zeilen is None:
|
||||
if not args.neu_anlegen:
|
||||
continue
|
||||
# Vollstaendiges Frontmatter anlegen. Titel und Zusammenfassung
|
||||
# werden mit den Funktionen des Toolkits ABGELEITET, nicht
|
||||
# erfunden; uid und Daten aus der Dateizeit.
|
||||
hand = HANDARBEIT.get(rel)
|
||||
if hand:
|
||||
typ = hand["typ"]
|
||||
titel = hand["titel"]
|
||||
zusammenfassung = hand["summary"]
|
||||
else:
|
||||
typ = typ_aus_pfad(rel)
|
||||
titel = V.derive_title({}, text, rel)
|
||||
zusammenfassung = V.derive_summary(text) or titel
|
||||
datum = _dt.date.fromtimestamp(os.path.getmtime(ap)).isoformat()
|
||||
kennung = "%s-%s" % (datum.replace("-", ""), V.slugify(titel)[:40])
|
||||
neue = [
|
||||
"uid: %s" % kennung,
|
||||
"type: %s" % typ,
|
||||
'title: %s' % _yaml(titel),
|
||||
"status: %s" % (hand["status"] if hand else STANDARD_STATUS.get(typ, "active")),
|
||||
'summary: %s' % _yaml(zusammenfassung[:300]),
|
||||
"tags: [%s]" % typ,
|
||||
"created: %s" % datum,
|
||||
"updated: %s" % datum,
|
||||
]
|
||||
neu_text = "---\n" + "\n".join(neue) + "\n---\n\n" + text
|
||||
probe, probe_rumpf = lies_block(neu_text)
|
||||
if probe is None or probe_rumpf.lstrip("\n") != text.lstrip("\n"):
|
||||
raise RuntimeError("Anlegen veraendert den Inhalt: %s" % rel)
|
||||
aenderungen.append((ap, rel, ["Frontmatter angelegt (%s)" % typ], neu_text))
|
||||
continue
|
||||
alt_typ = feld(zeilen, "type")
|
||||
if alt_typ in SPEZIAL:
|
||||
continue
|
||||
was = []
|
||||
neu_typ = UMBENENNEN.get(alt_typ, alt_typ)
|
||||
if not neu_typ:
|
||||
neu_typ = typ_aus_pfad(rel)
|
||||
was.append("type fehlte -> %s" % neu_typ)
|
||||
elif neu_typ != alt_typ:
|
||||
was.append("type %s -> %s" % (alt_typ, neu_typ))
|
||||
alt_st = feld(zeilen, "status")
|
||||
neu_st = STATUS_UMBENENNEN.get(alt_st, alt_st)
|
||||
if not neu_st:
|
||||
neu_st = STANDARD_STATUS.get(neu_typ, "active")
|
||||
was.append("status fehlte -> %s" % neu_st)
|
||||
elif neu_st != alt_st:
|
||||
was.append("status %s -> %s" % (alt_st, neu_st))
|
||||
if not was:
|
||||
continue
|
||||
zeilen = setze(list(zeilen), "type", neu_typ)
|
||||
zeilen = setze(zeilen, "status", neu_st)
|
||||
neu_text = "---\n" + "\n".join(zeilen) + "\n---\n" + rumpf
|
||||
|
||||
# Selbstkontrolle vor dem Schreiben: Der Rumpf muss Zeichen fuer
|
||||
# Zeichen derselbe sein, und das neue Frontmatter muss die
|
||||
# gesetzten Werte auch wirklich tragen. Sonst ist es ein Befund.
|
||||
probe_zeilen, probe_rumpf = lies_block(neu_text)
|
||||
if probe_rumpf != rumpf:
|
||||
raise RuntimeError("Rumpf veraendert: %s" % rel)
|
||||
if probe_zeilen is None:
|
||||
raise RuntimeError("Frontmatter nicht mehr lesbar: %s" % rel)
|
||||
if feld(probe_zeilen, "type") != neu_typ or feld(probe_zeilen, "status") != neu_st:
|
||||
raise RuntimeError("Werte nicht angekommen: %s" % rel)
|
||||
|
||||
aenderungen.append((ap, rel, was, neu_text))
|
||||
except Exception:
|
||||
sys.stdout.write("# ABGEBROCHEN\n\n" + traceback.format_exc())
|
||||
sys.stdout.write("\nBis dahin gesammelt: %d Aenderung(en)\n" % len(aenderungen))
|
||||
return 2
|
||||
|
||||
sys.stdout.write("# Frontmatter-Migration%s\n\n"
|
||||
% ("" if args.anwenden else " (TROCKENLAUF - nichts wird geschrieben)"))
|
||||
sys.stdout.write("%d Datei(en) betroffen\n\n" % len(aenderungen))
|
||||
for _, rel, was, _ in aenderungen:
|
||||
sys.stdout.write(" - %-78s %s\n" % (rel[:78], "; ".join(was)))
|
||||
|
||||
if args.anwenden:
|
||||
for ap, rel, _, neu_text in aenderungen:
|
||||
io.open(ap, "w", encoding="utf-8", newline="\n").write(neu_text)
|
||||
sys.stdout.write("\n%d Datei(en) geschrieben.\n" % len(aenderungen))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user