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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c23bb54a92
commit
865d761fb0
@@ -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
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
<!-- Generated by scripts/gen_status.py — do not edit. -->
|
||||
|
||||
## 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)
|
||||
|
||||
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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 <klonverzeichnis> [--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())
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user