From 865d761fb068152db05524a7930b6cd0507dba6c Mon Sep 17 00:00:00 2001 From: Thore Cimbal Date: Tue, 11 Aug 2026 12:00:00 +0000 Subject: [PATCH] feat: slice 5 - components declared, group checks live, mirror dry-run Gate 4, slice 5: eight component declarations under docs/components/ (filename = canonical slug, F-008 answered by construction; staged dormancy of thread-net-git/threadnet-operating finally representable, game-operating/gameserver as external with their field-test caveats), five pointer-rollout follow-up issues (0035-0039, ADR-0013), gruppenpruefung.py joins the stillstandspruefung family (runtime group list vs declarations, pointer presence, group-wide milestone/priority duty, issue drift, git hygiene since the 2026-08-07 rule boundary, bot exception per ADR-0009) with its own scheduled CI job, and spiegel_issues.py mirrors repo to GitLab (title, state, milestone, priority, due, status label only - never descriptions, never backwards, dry-run by default, GitLab-only issues are reported and never auto-closed). Verified - all four pattern demos fire (acceptance criterion 5, 4/4): A) old CLAUDE.md claims M1-M4 while the frozen export knows M5; B) hygiene over full clone history finds 222 real-clock commits by own identities (matches the frozen Session-1 numbers per repo); C) covered in slices 3/4 (five task blocks, now 0); D) covered in slice 3 (orphaned SHA citations, now resolved/curated). Live run (read-only): exactly the four missing pointers (F-011) and gitops#61 without milestone as red findings, zero drift on all 26 mirrored issues, group list consistent. Mirror dry-run plans 7 creations, 0 updates, wrote nothing. Offline chain green: validate 0/0, gen_status --check current, drift 0, prosa 0. Co-Authored-By: Claude Fable 5 --- .gitlab-ci.yml | 19 ++ STATUS.md | 9 +- docs/components/ThreadNet-Web.md | 14 + docs/components/axion1337.chat-gitops.md | 14 + docs/components/game-operating.md | 14 + docs/components/gameserver.md | 14 + docs/components/management.md | 14 + docs/components/thread-net-git.md | 14 + docs/components/threadnet-call.md | 14 + docs/components/threadnet-operating.md | 14 + ...ut-agents-pointer-axion1337-chat-gitops.md | 23 ++ ...36-rollout-agents-pointer-threadnet-web.md | 23 ++ ...7-rollout-agents-pointer-threadnet-call.md | 23 ++ ...8-rollout-agents-pointer-thread-net-git.md | 23 ++ ...lout-agents-pointer-threadnet-operating.md | 23 ++ scripts/gruppenpruefung.py | 247 ++++++++++++++++++ scripts/spiegel_issues.py | 157 +++++++++++ 17 files changed, 657 insertions(+), 2 deletions(-) create mode 100644 docs/components/ThreadNet-Web.md create mode 100644 docs/components/axion1337.chat-gitops.md create mode 100644 docs/components/game-operating.md create mode 100644 docs/components/gameserver.md create mode 100644 docs/components/management.md create mode 100644 docs/components/thread-net-git.md create mode 100644 docs/components/threadnet-call.md create mode 100644 docs/components/threadnet-operating.md create mode 100644 docs/issues/0035-rollout-agents-pointer-axion1337-chat-gitops.md create mode 100644 docs/issues/0036-rollout-agents-pointer-threadnet-web.md create mode 100644 docs/issues/0037-rollout-agents-pointer-threadnet-call.md create mode 100644 docs/issues/0038-rollout-agents-pointer-thread-net-git.md create mode 100644 docs/issues/0039-rollout-agents-pointer-threadnet-operating.md create mode 100644 scripts/gruppenpruefung.py create mode 100644 scripts/spiegel_issues.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7120645..40a9fd6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -50,3 +50,22 @@ validate: - python3 scripts/pruefe_upstream_drift.py - python3 scripts/pruefe_prosa.py allow_failure: false + +# Verbund-Prüfung (ADR-0012/0013): Gruppenliste vs. docs/components/, +# Pointer-Praesenz, Meilenstein-/Prioritaetspflicht, Issue-Drift, +# Git-Hygiene. Gleiche Regeln wie die Stillstandspruefung: geplant/von +# Hand, rot = Alarm, Abbruch ohne Token. +gruppenpruefung: + stage: pruefen + image: python:3.12-alpine + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" + - if: $CI_PIPELINE_SOURCE == "web" + script: + - | + if [ -z "$GITLAB_TOKEN" ]; then + echo "GITLAB_TOKEN fehlt (Gruppen-Token mit read_api)." + exit 1 + fi + - python3 scripts/gruppenpruefung.py + allow_failure: false diff --git a/STATUS.md b/STATUS.md index 5e6ac0d..c7b2b0e 100644 --- a/STATUS.md +++ b/STATUS.md @@ -2,9 +2,9 @@ -## Issues (28 open, 0 closed) +## Issues (33 open, 0 closed) -Verteilung: M1 9 · M2 17 · M4 2 +Verteilung: M1 9 · M2 22 · M4 2 | Issue | Status | Meilenstein | Priorität | Title | |---|---|---|---|---| @@ -36,6 +36,11 @@ Verteilung: M1 9 · M2 17 · M4 2 | [0032](docs/issues/0032-gameserver-hat-keinen-push-mirror-und-auf-gitea.md) | open | M2 | medium | gameserver hat keinen Push-Mirror — und auf Gitea liegt ein anderer Stand | | [0033](docs/issues/0033-overmind-01-element-desktop-build-lab-registry.md) | open | M2 | low | OVERMIND-01 — element-desktop-build von rohana in die Lab-Registry umziehen | | [0034](docs/issues/0034-cfgmon-11-gitea-ci-rueckbau-abschliessen.md) | open | M2 | medium | CFGMON-11 — Gitea-CI-Rückbau abschließen (sicher rückbaubare Schritte) | +| [0035](docs/issues/0035-rollout-agents-pointer-axion1337-chat-gitops.md) | open | M2 | medium | Rollout Gruppenregeln-Pointer: `axion1337.chat-gitops` | +| [0036](docs/issues/0036-rollout-agents-pointer-threadnet-web.md) | open | M2 | medium | Rollout Gruppenregeln-Pointer: `ThreadNet-Web` | +| [0037](docs/issues/0037-rollout-agents-pointer-threadnet-call.md) | open | M2 | medium | Rollout Gruppenregeln-Pointer: `threadnet-call` | +| [0038](docs/issues/0038-rollout-agents-pointer-thread-net-git.md) | open | M2 | medium | Rollout Gruppenregeln-Pointer: `thread-net-git` | +| [0039](docs/issues/0039-rollout-agents-pointer-threadnet-operating.md) | open | M2 | medium | Rollout Gruppenregeln-Pointer: `threadnet-operating` | ## Active design docs (1) diff --git a/docs/components/ThreadNet-Web.md b/docs/components/ThreadNet-Web.md new file mode 100644 index 0000000..50a5c3f --- /dev/null +++ b/docs/components/ThreadNet-Web.md @@ -0,0 +1,14 @@ +--- +type: component +slug: "ThreadNet-Web" +anzeigename: "ThreadNet Web" +phase: active +gitlab: "axion1337.chat/ThreadNet-Web" +mirror: "rohana.axion1337.de/sorb/ThreadNet-Web" +related: + - "docs/adr/0013-gruppenregeln-kanonisch-mit-pruefung.md" +--- + +# ThreadNet Web + +Element-Web/-Desktop-Fork unter eigener Marke. ⚠️ Slug ist der einzige in CamelCase (F-008) — GitLab behandelt Pfade case-insensitiv; kanonisch ist exakt diese Schreibweise. Ein Rename ist bewusst NICHT Teil der Migration (eigenes Issue bei Bedarf, ADR-0013). diff --git a/docs/components/axion1337.chat-gitops.md b/docs/components/axion1337.chat-gitops.md new file mode 100644 index 0000000..72b6213 --- /dev/null +++ b/docs/components/axion1337.chat-gitops.md @@ -0,0 +1,14 @@ +--- +type: component +slug: "axion1337.chat-gitops" +anzeigename: "ThreadNet Server Suite" +phase: active +gitlab: "axion1337.chat/axion1337.chat-gitops" +mirror: "rohana.axion1337.de/sorb/axion1337.chat-gitops" +related: + - "docs/adr/0013-gruppenregeln-kanonisch-mit-pruefung.md" +--- + +# ThreadNet Server Suite + +ESS-/Flux-Deployment des axion1337.chat-Stacks; Gitea bleibt Flux-Quelle ([Mirror-Topologie](../wiki/architecture/mirror-topologie.md)). Trägt die Hälfte des Gruppen-Backlogs (Feldtest F-009). diff --git a/docs/components/game-operating.md b/docs/components/game-operating.md new file mode 100644 index 0000000..5104b2a --- /dev/null +++ b/docs/components/game-operating.md @@ -0,0 +1,14 @@ +--- +type: component +slug: "game-operating" +anzeigename: "Game-Operating" +phase: external +gitlab: "axion1337.chat/game-operating" +mirror: "rohana.axion1337.de/sorb/game-operating" # privat — anonym nicht lesbar (F-007-Addendum) +related: + - "docs/adr/0013-gruppenregeln-kanonisch-mit-pruefung.md" +--- + +# Game-Operating + +Nicht ThreadNet-bezogen (über geplante Monitoring-Aufnahme hinaus). Mirror existiert **privat** — ein anonymer ls-remote-Fehlschlag ist hier kein Beleg für Nichtexistenz (Feldtest-Lehre, F-007-Addendum). diff --git a/docs/components/gameserver.md b/docs/components/gameserver.md new file mode 100644 index 0000000..4b6aa27 --- /dev/null +++ b/docs/components/gameserver.md @@ -0,0 +1,14 @@ +--- +type: component +slug: "gameserver" +anzeigename: "Gameserver" +phase: external +gitlab: "axion1337.chat/gameserver" +mirror: null # kein Mirror — Issue 0032 +related: + - "docs/adr/0013-gruppenregeln-kanonisch-mit-pruefung.md" +--- + +# Gameserver + +Nicht ThreadNet-bezogen. **Kein Push-Mirror**; auf Gitea liegt ein gleichnamiges Repo mit anderem Stand — verfolgt in [Issue 0032](../issues/0032-gameserver-hat-keinen-push-mirror-und-auf-gitea.md). diff --git a/docs/components/management.md b/docs/components/management.md new file mode 100644 index 0000000..6ddae6b --- /dev/null +++ b/docs/components/management.md @@ -0,0 +1,14 @@ +--- +type: component +slug: "management" +anzeigename: "Management" +phase: active +gitlab: "axion1337.chat/management" +mirror: "rohana.axion1337.de/sorb/management" +related: + - "docs/adr/0013-gruppenregeln-kanonisch-mit-pruefung.md" +--- + +# Management + +Dieses Repo: Steuerung der Gruppe, kanonische Gruppenregeln (AGENTS.md §6), In-Repo-Issues (ADR-0012). diff --git a/docs/components/thread-net-git.md b/docs/components/thread-net-git.md new file mode 100644 index 0000000..f7b38e1 --- /dev/null +++ b/docs/components/thread-net-git.md @@ -0,0 +1,14 @@ +--- +type: component +slug: "thread-net-git" +anzeigename: "ThreadNet Git" +phase: staged +gitlab: "axion1337.chat/thread-net-git" +mirror: "rohana.axion1337.de/sorb/thread-net-git" +related: + - "docs/adr/0013-gruppenregeln-kanonisch-mit-pruefung.md" +--- + +# ThreadNet Git + +Gitea-Betriebskonfiguration. **Bewusst gestaffelt dormant** (sorb, 2026-08-10, Feldtest F-009-Addendum): erst Basis-Funktionsumfang, dann Monitoring-/Security-Ausbau — keine Politur-Umwege. ⚠️ Slug bricht das threadnet-Muster (F-008); Ist-Stand dokumentiert, kein Rename hier. diff --git a/docs/components/threadnet-call.md b/docs/components/threadnet-call.md new file mode 100644 index 0000000..fcdce34 --- /dev/null +++ b/docs/components/threadnet-call.md @@ -0,0 +1,14 @@ +--- +type: component +slug: "threadnet-call" +anzeigename: "ThreadNet Call" +phase: active +gitlab: "axion1337.chat/threadnet-call" +mirror: "rohana.axion1337.de/sorb/threadnet-call" +related: + - "docs/adr/0013-gruppenregeln-kanonisch-mit-pruefung.md" +--- + +# ThreadNet Call + +Element-Call-Fork für ThreadNet. diff --git a/docs/components/threadnet-operating.md b/docs/components/threadnet-operating.md new file mode 100644 index 0000000..60b084d --- /dev/null +++ b/docs/components/threadnet-operating.md @@ -0,0 +1,14 @@ +--- +type: component +slug: "threadnet-operating" +anzeigename: "ThreadNet Operating" +phase: staged +gitlab: "axion1337.chat/threadnet-operating" +mirror: "rohana.axion1337.de/sorb/threadnet-operating" +related: + - "docs/adr/0013-gruppenregeln-kanonisch-mit-pruefung.md" +--- + +# ThreadNet Operating + +CFGMON-Betrieb (Monitoring/Konfiguration). **Bewusst gestaffelt dormant** wie thread-net-git (F-009-Addendum). diff --git a/docs/issues/0035-rollout-agents-pointer-axion1337-chat-gitops.md b/docs/issues/0035-rollout-agents-pointer-axion1337-chat-gitops.md new file mode 100644 index 0000000..fd86496 --- /dev/null +++ b/docs/issues/0035-rollout-agents-pointer-axion1337-chat-gitops.md @@ -0,0 +1,23 @@ +--- +type: issue +id: "0035" +status: open +created: 2026-08-11 +milestone: M2 +priority: medium +related: + - "docs/adr/0013-gruppenregeln-kanonisch-mit-pruefung.md" +--- + +# Rollout Gruppenregeln-Pointer: `axion1337.chat-gitops` + +> Folge-Issue aus [ADR-0013](../adr/0013-gruppenregeln-kanonisch-mit-pruefung.md) +> (Feldtest F-011: 4 von 5 Komponenten trugen keine Pointer-Datei). +> `gitlab_iid` folgt mit dem ersten Spiegel-Lauf. + +In `axion1337.chat-gitops` anlegen: `CLAUDE.md` als Ein-Zeilen-Pointer und ein +`AGENTS.md` mit **nur** Projektspezifika plus Verweis auf die +Gruppenregeln (management-Repo, git.lab + rohana-Mirror-URL). Bestehende +projektspezifische CLAUDE.md-Inhalte (gitops) bleiben erhalten und +rücken unter den Pointer. Danach meldet `gruppenpruefung.py` die +Komponente grün. diff --git a/docs/issues/0036-rollout-agents-pointer-threadnet-web.md b/docs/issues/0036-rollout-agents-pointer-threadnet-web.md new file mode 100644 index 0000000..6fd2488 --- /dev/null +++ b/docs/issues/0036-rollout-agents-pointer-threadnet-web.md @@ -0,0 +1,23 @@ +--- +type: issue +id: "0036" +status: open +created: 2026-08-11 +milestone: M2 +priority: medium +related: + - "docs/adr/0013-gruppenregeln-kanonisch-mit-pruefung.md" +--- + +# Rollout Gruppenregeln-Pointer: `ThreadNet-Web` + +> Folge-Issue aus [ADR-0013](../adr/0013-gruppenregeln-kanonisch-mit-pruefung.md) +> (Feldtest F-011: 4 von 5 Komponenten trugen keine Pointer-Datei). +> `gitlab_iid` folgt mit dem ersten Spiegel-Lauf. + +In `ThreadNet-Web` anlegen: `CLAUDE.md` als Ein-Zeilen-Pointer und ein +`AGENTS.md` mit **nur** Projektspezifika plus Verweis auf die +Gruppenregeln (management-Repo, git.lab + rohana-Mirror-URL). Bestehende +projektspezifische CLAUDE.md-Inhalte (gitops) bleiben erhalten und +rücken unter den Pointer. Danach meldet `gruppenpruefung.py` die +Komponente grün. diff --git a/docs/issues/0037-rollout-agents-pointer-threadnet-call.md b/docs/issues/0037-rollout-agents-pointer-threadnet-call.md new file mode 100644 index 0000000..7757663 --- /dev/null +++ b/docs/issues/0037-rollout-agents-pointer-threadnet-call.md @@ -0,0 +1,23 @@ +--- +type: issue +id: "0037" +status: open +created: 2026-08-11 +milestone: M2 +priority: medium +related: + - "docs/adr/0013-gruppenregeln-kanonisch-mit-pruefung.md" +--- + +# Rollout Gruppenregeln-Pointer: `threadnet-call` + +> Folge-Issue aus [ADR-0013](../adr/0013-gruppenregeln-kanonisch-mit-pruefung.md) +> (Feldtest F-011: 4 von 5 Komponenten trugen keine Pointer-Datei). +> `gitlab_iid` folgt mit dem ersten Spiegel-Lauf. + +In `threadnet-call` anlegen: `CLAUDE.md` als Ein-Zeilen-Pointer und ein +`AGENTS.md` mit **nur** Projektspezifika plus Verweis auf die +Gruppenregeln (management-Repo, git.lab + rohana-Mirror-URL). Bestehende +projektspezifische CLAUDE.md-Inhalte (gitops) bleiben erhalten und +rücken unter den Pointer. Danach meldet `gruppenpruefung.py` die +Komponente grün. diff --git a/docs/issues/0038-rollout-agents-pointer-thread-net-git.md b/docs/issues/0038-rollout-agents-pointer-thread-net-git.md new file mode 100644 index 0000000..a0baa62 --- /dev/null +++ b/docs/issues/0038-rollout-agents-pointer-thread-net-git.md @@ -0,0 +1,23 @@ +--- +type: issue +id: "0038" +status: open +created: 2026-08-11 +milestone: M2 +priority: medium +related: + - "docs/adr/0013-gruppenregeln-kanonisch-mit-pruefung.md" +--- + +# Rollout Gruppenregeln-Pointer: `thread-net-git` + +> Folge-Issue aus [ADR-0013](../adr/0013-gruppenregeln-kanonisch-mit-pruefung.md) +> (Feldtest F-011: 4 von 5 Komponenten trugen keine Pointer-Datei). +> `gitlab_iid` folgt mit dem ersten Spiegel-Lauf. + +In `thread-net-git` anlegen: `CLAUDE.md` als Ein-Zeilen-Pointer und ein +`AGENTS.md` mit **nur** Projektspezifika plus Verweis auf die +Gruppenregeln (management-Repo, git.lab + rohana-Mirror-URL). Bestehende +projektspezifische CLAUDE.md-Inhalte (gitops) bleiben erhalten und +rücken unter den Pointer. Danach meldet `gruppenpruefung.py` die +Komponente grün. diff --git a/docs/issues/0039-rollout-agents-pointer-threadnet-operating.md b/docs/issues/0039-rollout-agents-pointer-threadnet-operating.md new file mode 100644 index 0000000..917a109 --- /dev/null +++ b/docs/issues/0039-rollout-agents-pointer-threadnet-operating.md @@ -0,0 +1,23 @@ +--- +type: issue +id: "0039" +status: open +created: 2026-08-11 +milestone: M2 +priority: medium +related: + - "docs/adr/0013-gruppenregeln-kanonisch-mit-pruefung.md" +--- + +# Rollout Gruppenregeln-Pointer: `threadnet-operating` + +> Folge-Issue aus [ADR-0013](../adr/0013-gruppenregeln-kanonisch-mit-pruefung.md) +> (Feldtest F-011: 4 von 5 Komponenten trugen keine Pointer-Datei). +> `gitlab_iid` folgt mit dem ersten Spiegel-Lauf. + +In `threadnet-operating` anlegen: `CLAUDE.md` als Ein-Zeilen-Pointer und ein +`AGENTS.md` mit **nur** Projektspezifika plus Verweis auf die +Gruppenregeln (management-Repo, git.lab + rohana-Mirror-URL). Bestehende +projektspezifische CLAUDE.md-Inhalte (gitops) bleiben erhalten und +rücken unter den Pointer. Danach meldet `gruppenpruefung.py` die +Komponente grün. diff --git a/scripts/gruppenpruefung.py b/scripts/gruppenpruefung.py new file mode 100644 index 0000000..4be9316 --- /dev/null +++ b/scripts/gruppenpruefung.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +"""gruppenpruefung.py — Verbund-Prüfungen über die Gruppe (Lab-CI). + +Gehört zur Stillstandsprüfungs-Familie: Befunde färben die Pipeline +rot (das ist die Alarmanlage), fehlender Zugang bricht ab statt still +zu überspringen, und jede Prüfung bildet einen real passierten Fall ab +(Design 2026-08-11, Muster A/B; ADR-0012/0013): + + 1. Gruppenliste ↔ docs/components/ — die Projektliste wird zur + Laufzeit gelesen, nie im Code gepflegt (Retro-Lehre); ein neues + Repo wird Befund statt Lücke. (F-008/F-009) + 2. Pointer-Präsenz — jede active/staged-Komponente trägt CLAUDE.md. + (F-011: 4 von 5 fehlten, niemand prüfte) + 3. Meilenstein- und Prioritätspflicht über ALLE offenen + Gruppen-Issues. (realer Fall: gitops#61, entstanden 2026-08-11, + ohne Meilenstein) + 4. Issue-Drift management ↔ docs/issues/ — Titel, Zustand, + Meilenstein, Priorität, Status-Label. (F-001/F-017-Klasse) + 5. Git-Hygiene seit 2026-08-07 — Autor- und Committer-Zeit 12:00:00 + UTC, kanonische Identität; Ausnahme: der Rotations-Bot + (ADR-0009). (F-002/F-003) + +Demo-Modus für den Altbestand (Muster-B-Nachweis, Feldtest F-002): + --hygiene-lokal [--seit JJJJ-MM-TT] +prüft die volle Historie lokaler Klone offline; nur eigene +Identitäten zählen (die fünf aus F-003). + +Usage: GITLAB_TOKEN=… python3 scripts/gruppenpruefung.py [repo-root] + python3 scripts/gruppenpruefung.py --hygiene-lokal DIR [--seit DATUM] +""" +from __future__ import annotations + +import json +import os +import re +import subprocess +import sys +import urllib.parse +import urllib.request +from pathlib import Path + +API = "https://git.lab/api/v4" +GRUPPE = "axion1337.chat" +GRENZE = "2026-08-07" +KANONISCH = ("Thore Cimbal", "cfx@riot.8shield.net") +BOT = "turn-secret-rotation" +EIGENE_MAILS = {"cfx@riot.8shield.net", "gamemaster@axion1337.de", + "cfxqriot.8shield.net", "Scrublord@Mac.Bad"} +STATUS_LABEL = {"open": None, "next": "status:next", + "in-progress": "status:doing", "waiting": "status:wartet"} + +befunde: list[str] = [] +hinweise: list[str] = [] + + +def api(pfad: str, token: str): + daten, seite = [], 1 + while True: + trenner = "&" if "?" in pfad else "?" + req = urllib.request.Request( + f"{API}/{pfad}{trenner}per_page=100&page={seite}", + headers={"PRIVATE-TOKEN": token}) + with urllib.request.urlopen(req) as antwort: + batch = json.load(antwort) + if not isinstance(batch, list): + return batch + daten += batch + if len(batch) < 100: + return daten + seite += 1 + + +def frontmatter(p: Path) -> dict: + zeilen = p.read_text(encoding="utf-8").splitlines() + if not zeilen or zeilen[0] != "---": + return {} + meta = {} + for z in zeilen[1:]: + if z == "---": + break + m = re.match(r"^(\w+):\s*(.*)$", z) + if m: + meta[m.group(1)] = m.group(2).strip().strip('"') + return meta + + +def titel(p: Path) -> str: + for z in p.read_text(encoding="utf-8").splitlines(): + if z.startswith("# "): + return z[2:].strip() + return "" + + +def zeit_ok(iso: str) -> bool: + # GitLab liefert z. B. 2026-08-11T12:00:00.000+00:00 + m = re.match(r"^\d{4}-\d{2}-\d{2}T(\d{2}:\d{2}:\d{2})(?:\.\d+)?" + r"(Z|[+-]\d{2}:?\d{2})$", iso) + if not m: + return False + zeit, offset = m.groups() + return zeit == "12:00:00" and offset in ("Z", "+00:00", "+0000") + + +def hygiene_lokal(klondir: Path, seit: str) -> int: + gesamt = 0 + for repo in sorted(d for d in klondir.iterdir() if (d / ".git").exists()): + umgebung = dict(os.environ, TZ="UTC") # Session-1-Lehre: TZ am git-Prozess + raus = subprocess.run( + ["git", "-C", str(repo), "log", "--all", + f"--since={seit}", "--date=format-local:%H:%M:%S", + "--format=%ae\t%ad\t%cd"], + capture_output=True, text=True, env=umgebung).stdout + zaehler = 0 + for zeile in raus.splitlines(): + mail, ad, cd = zeile.split("\t") + if mail in EIGENE_MAILS and (ad != "12:00:00" or cd != "12:00:00"): + zaehler += 1 + if zaehler: + befunde.append(f"{repo.name}: {zaehler} Commits eigener " + f"Identitäten mit Echtzeit-Stempel (seit {seit})") + gesamt += zaehler + return gesamt + + +def main() -> int: + if "--hygiene-lokal" in sys.argv: + klondir = Path(sys.argv[sys.argv.index("--hygiene-lokal") + 1]) + seit = (sys.argv[sys.argv.index("--seit") + 1] + if "--seit" in sys.argv else GRENZE) + gesamt = hygiene_lokal(klondir, seit) + for b in befunde: + print(f"BEFUND {b}") + print(f"gruppenpruefung --hygiene-lokal: {len(befunde)} Befunde, " + f"{gesamt} Commits betroffen") + return 1 if befunde else 0 + + token = os.environ.get("GITLAB_TOKEN", "") + if not token: + sys.exit("GITLAB_TOKEN fehlt — ohne Lesezugriff kann nichts " + "geprüft werden; Abbruch statt stillem Skip.") + root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd() + + # 1) Gruppenliste (Laufzeit) ↔ docs/components/ + live = {p["path"]: p for p in + api(f"groups/{GRUPPE}/projects?include_subgroups=false", token) + if not p.get("archived")} + erklaert = {} + for f in sorted((root / "docs/components").glob("*.md")): + if f.name == "template.md": + continue + erklaert[frontmatter(f)["slug"]] = frontmatter(f) + for slug in sorted(set(live) - set(erklaert)): + befunde.append(f"Projekt {slug} existiert in der Gruppe, ist aber " + f"in docs/components/ nicht deklariert") + for slug in sorted(set(erklaert) - set(live)): + befunde.append(f"docs/components/{slug}.md deklariert ein Projekt, " + f"das die Gruppe nicht (mehr) führt") + + # 2) Pointer-Präsenz (active/staged, außer management selbst) + for slug, meta in sorted(erklaert.items()): + if meta.get("phase") not in ("active", "staged") or slug == "management": + continue + ref = live.get(slug, {}).get("default_branch", "main") + pfad = urllib.parse.quote(f"{GRUPPE}/{slug}", safe="") + try: + api(f"projects/{pfad}/repository/files/CLAUDE.md?ref={ref}", token) + except Exception: + befunde.append(f"{slug}: keine CLAUDE.md-Pointer-Datei " + f"(F-011; Rollout-Issue vorhanden)") + + # 3) Meilenstein-/Prioritätspflicht gruppenweit + for i in api(f"groups/{GRUPPE}/issues?state=opened", token): + ref = i["references"]["full"] + ms = (i.get("milestone") or {}).get("title", "") + prios = [l for l in i["labels"] if l.startswith("priority:")] + if not re.match(r"^M[1-5]\b", ms): + befunde.append(f"{ref}: offenes Issue ohne Meilenstein " + f"M1–M5 ({ms or 'keiner'})") + if len(prios) != 1: + befunde.append(f"{ref}: {len(prios)} priority-Labels statt 1") + + # 4) Issue-Drift management ↔ docs/issues/ + gl = {i["iid"]: i for i in + api(f"projects/{urllib.parse.quote(GRUPPE + '/management', safe='')}" + f"/issues?state=opened", token)} + dateien = {} + for f in sorted((root / "docs/issues").glob("[0-9]*.md")): + meta = frontmatter(f) + if meta.get("gitlab_iid"): + dateien[int(meta["gitlab_iid"])] = (f, meta) + elif meta.get("status") not in ("done", "rejected"): + hinweise.append(f"{f.name}: noch nicht gespiegelt " + f"(gitlab_iid fehlt — erwartet bis zum " + f"ersten Spiegel-Lauf)") + for iid in sorted(set(gl) - set(dateien)): + befunde.append(f"management#{iid} ist offen auf GitLab, hat aber " + f"keine kanonische Datei (zweites Backlog!)") + for iid, (f, meta) in sorted(dateien.items()): + if iid not in gl: + if meta.get("status") not in ("done", "rejected"): + befunde.append(f"{f.name}: offen im Repo, aber auf GitLab " + f"geschlossen/fehlend — nachziehen") + continue + i = gl[iid] + if titel(f) != i["title"].strip(): + befunde.append(f"{f.name}: Titel weicht von GitLab ab") + ms = (i.get("milestone") or {}).get("title", "") + if not ms.startswith(meta.get("milestone", "")): + befunde.append(f"{f.name}: Meilenstein {meta.get('milestone')} " + f"↔ GitLab {ms or 'keiner'}") + if f"priority:{meta.get('priority')}" not in i["labels"]: + befunde.append(f"{f.name}: Priorität {meta.get('priority')} " + f"fehlt auf GitLab") + soll = STATUS_LABEL.get(meta.get("status")) + ist = [l for l in i["labels"] if l.startswith("status:")] + if (soll and soll not in ist) or (not soll and ist): + befunde.append(f"{f.name}: Status {meta.get('status')} ↔ " + f"GitLab-Labels {ist or 'keine'}") + + # 5) Git-Hygiene seit der Regel-Grenze (API) + for slug, meta in sorted(erklaert.items()): + if meta.get("phase") == "external": + continue + pfad = urllib.parse.quote(f"{GRUPPE}/{slug}", safe="") + for c in api(f"projects/{pfad}/repository/commits" + f"?since={GRENZE}T00:00:00Z&all=true", token): + if BOT in (c.get("author_name") or ""): + continue + wer = f"{c.get('author_name')} <{c.get('author_email')}>" + if not zeit_ok(c["authored_date"]) or not zeit_ok(c["committed_date"]): + befunde.append(f"{slug} {c['short_id']}: Echtzeit-Stempel " + f"({wer})") + elif (c.get("author_name"), c.get("author_email")) != KANONISCH: + befunde.append(f"{slug} {c['short_id']}: nicht-kanonische " + f"Identität {wer}") + + for b in befunde: + print(f"BEFUND {b}") + for h in hinweise: + print(f"HINWEIS {h}") + print(f"gruppenpruefung: {len(befunde)} Befunde, " + f"{len(hinweise)} Hinweise") + return 1 if befunde else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/spiegel_issues.py b/scripts/spiegel_issues.py new file mode 100644 index 0000000..2992860 --- /dev/null +++ b/scripts/spiegel_issues.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +"""spiegel_issues.py — Repo → GitLab, ein deterministischer Spiegel. + +docs/issues/ ist kanonisch (ADR-0012); dieses Skript bespielt die +GitLab-Ansicht, damit Board, Meilensteine und Labels weiterarbeiten. +Gespiegelt werden **nur** Titel, Zustand, Meilenstein, Priorität, +Fälligkeit und Status-Label — nie Beschreibungen (der Migrations- +Fußtext und die Kommentare auf GitLab bleiben unangetastet), und nie +in Gegenrichtung. + +Default ist **Dry-Run**: druckt jeden geplanten API-Aufruf und ändert +nichts. `--ausfuehren` schreibt wirklich — das stößt nur sorb an. +GitLab-seitig offene Issues ohne kanonische Datei werden gemeldet, +nie automatisch geschlossen. + +Usage: python3 scripts/spiegel_issues.py [repo-root] [--ausfuehren] + (Token: ~/.config/gitlab-lab/token oder $GITLAB_TOKEN) +""" +from __future__ import annotations + +import json +import os +import re +import sys +import urllib.parse +import urllib.request +from pathlib import Path + +API = "https://git.lab/api/v4" +PROJEKT = urllib.parse.quote("axion1337.chat/management", safe="") +GRUPPE = "axion1337.chat" +STATUS_LABEL = {"open": None, "next": "status:next", + "in-progress": "status:doing", "waiting": "status:wartet"} + + +def token_lesen() -> str: + if os.environ.get("GITLAB_TOKEN"): + return os.environ["GITLAB_TOKEN"] + return (Path.home() / ".config/gitlab-lab/token").read_text( + encoding="utf-8").strip() + + +def api(pfad: str, token: str, methode: str = "GET", daten: dict | None = None): + body = json.dumps(daten).encode() if daten is not None else None + req = urllib.request.Request( + f"{API}/{pfad}", data=body, method=methode, + headers={"PRIVATE-TOKEN": token, "Content-Type": "application/json"}) + with urllib.request.urlopen(req) as antwort: + return json.load(antwort) + + +def alle(pfad: str, token: str): + daten, seite = [], 1 + while True: + trenner = "&" if "?" in pfad else "?" + batch = api(f"{pfad}{trenner}per_page=100&page={seite}", token) + daten += batch + if len(batch) < 100: + return daten + seite += 1 + + +def frontmatter_und_titel(p: Path): + zeilen = p.read_text(encoding="utf-8").splitlines() + meta, titel, i = {}, "", 1 + while i < len(zeilen) and zeilen[i] != "---": + m = re.match(r"^(\w+):\s*(.*)$", zeilen[i]) + if m: + meta[m.group(1)] = m.group(2).strip().strip('"') + i += 1 + for z in zeilen[i:]: + if z.startswith("# "): + titel = z[2:].strip() + break + return meta, titel + + +def main() -> int: + argv = [a for a in sys.argv[1:] if a != "--ausfuehren"] + scharf = "--ausfuehren" in sys.argv + root = Path(argv[0]) if argv else Path.cwd() + token = token_lesen() + + meilensteine = {re.match(r"^(M\d)\b", m["title"]).group(1): m["id"] + for m in alle(f"groups/{GRUPPE}/milestones", token) + if re.match(r"^M\d\b", m["title"])} + gitlab = {i["iid"]: i for i in + alle(f"projects/{PROJEKT}/issues?state=opened", token)} + + plan: list[tuple[str, str, dict]] = [] + gespiegelt: set[int] = set() + for f in sorted((root / "docs/issues").glob("[0-9]*.md")): + meta, titel = frontmatter_und_titel(f) + offen = meta.get("status") not in ("done", "rejected") + labels = [f"priority:{meta['priority']}"] + if STATUS_LABEL.get(meta.get("status")): + labels.append(STATUS_LABEL[meta["status"]]) + for feld in ("host", "area"): + if meta.get(feld): + labels.append(f"{feld}:{meta[feld]}") + soll = {"title": titel, "labels": sorted(labels), + "milestone_id": meilensteine.get(meta.get("milestone")), + "due_date": meta.get("due") or None, + "state_event": None if offen else "close"} + + iid = int(meta["gitlab_iid"]) if meta.get("gitlab_iid") else None + if iid is None: + if offen: + plan.append(("ANLEGEN", f.name, + {k: v for k, v in soll.items() + if k != "state_event" and v is not None})) + continue + gespiegelt.add(iid) + ist = gitlab.get(iid) + if ist is None: + if offen: + plan.append(("MELDEN", f.name, + {"grund": f"#{iid} auf GitLab nicht offen, " + f"Datei aber {meta.get('status')}"})) + continue + delta = {} + if ist["title"].strip() != titel: + delta["title"] = titel + ist_labels = sorted(l for l in ist["labels"] + if l.split(":")[0] in + ("priority", "status", "host", "area")) + if ist_labels != soll["labels"]: + delta["labels"] = soll["labels"] + if (ist.get("milestone") or {}).get("id") != soll["milestone_id"]: + delta["milestone_id"] = soll["milestone_id"] + if (ist.get("due_date") or None) != soll["due_date"]: + delta["due_date"] = soll["due_date"] + if not offen: + delta["state_event"] = "close" + if delta: + plan.append(("ÄNDERN", f"{f.name} (#{iid})", delta)) + + for iid in sorted(set(gitlab) - gespiegelt): + plan.append(("MELDEN", f"management#{iid}", + {"grund": "offen auf GitLab ohne kanonische Datei — " + "wird NICHT automatisch geschlossen"})) + + modus = "AUSFÜHREN" if scharf else "DRY-RUN" + for aktion, wer, daten in plan: + print(f"[{modus}] {aktion} {wer}: {json.dumps(daten, ensure_ascii=False)}") + if scharf and aktion == "ANLEGEN": + api(f"projects/{PROJEKT}/issues", token, "POST", daten) + elif scharf and aktion == "ÄNDERN": + iid = int(wer.rsplit("#", 1)[1].rstrip(")")) + api(f"projects/{PROJEKT}/issues/{iid}", token, "PUT", daten) + print(f"spiegel_issues [{modus}]: {len(plan)} Aktionen " + f"({sum(1 for a, _, _ in plan if a == 'MELDEN')} Meldungen)") + return 0 + + +if __name__ == "__main__": + sys.exit(main())