Gate 4, slice 4: 26 open management issues imported from live git.lab (read-only, descriptions included as authorized; GitLab iid = file id, labels/milestone/priority/due/host/area mapped into frontmatter, the import aborts instead of inventing a missing milestone or priority). Two new issues close the F-004 gap where work was really still open (0033 OVERMIND-01, 0034 CFGMON-11 incl. the plaintext npm-token rotation); CFGMON-12/13 already route to verified git.lab issues, MATRIX-05 is done and needs none (agreed with sorb). The three wiki task blocks now reference their issues, roadmap.md hands all counts to the generated STATUS.md and states M1-M5 per ADR-0010 (closing F-001 in the canonical prose), pruefe_prosa joins the CI validate job, and the import protocol under docs/sources/migration/ records every intervention into imported text. Verified: validate 0/0 over 29 issue files, gen_status --check current (distribution line M1 9 - M2 17 - M4 2 plus per-issue milestone and priority), pruefe_prosa 0 errors with clones and 0 errors/15 unchecked citations in offline CI mode, drift check green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
124 lines
4.8 KiB
Python
124 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""import_issues.py — Einmal-Import der offenen management-Issues.
|
|
|
|
Migrationsakte, kein Dauerwerkzeug (Design 2026-08-11, Slice 4;
|
|
ADR-0012). Liest die offenen Issues des Projekts
|
|
axion1337.chat/management read-only von git.lab (Token nur per
|
|
Dateipfad, Wert erscheint nirgends) und schreibt je Issue eine
|
|
kanonische Datei docs/issues/<iid>-<slug>.md. GitLab-iid = Datei-id —
|
|
keine dritte Nummernwelt. Kommentare und Verlauf bleiben auf GitLab;
|
|
der Dateikopf verlinkt dorthin.
|
|
|
|
Abbildung (alt → Schema):
|
|
ohne status-Label → open · status:next → next · status:doing →
|
|
in-progress · status:wartet → waiting (wartegrund: Verweis auf den
|
|
GitLab-Verlauf; Präzisierung im nächsten Refinement) · Meilenstein
|
|
"Mn — …" → Mn · priority:x → x · due_date → due · host:x → host ·
|
|
area:x → area. Fehlt Meilenstein oder Priorität, bricht der Import
|
|
ab — das wäre ein Befund, kein Füllwert.
|
|
|
|
Relative Upload-Pfade in Beschreibungen werden auf absolute
|
|
git.lab-URLs umgeschrieben, damit der Link-Check nicht ins Leere prüft.
|
|
|
|
Usage: python3 import_issues.py <repo-root> [tokenpfad]
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import sys
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
API = "https://git.lab/api/v4/projects/axion1337.chat%2Fmanagement/issues"
|
|
UPLOADS = "https://git.lab/axion1337.chat/management"
|
|
|
|
UMLAUTE = str.maketrans({"ä": "ae", "ö": "oe", "ü": "ue", "ß": "ss",
|
|
"Ä": "ae", "Ö": "oe", "Ü": "ue", "é": "e"})
|
|
|
|
|
|
def slug(titel: str) -> str:
|
|
s = titel.translate(UMLAUTE).lower()
|
|
s = re.sub(r"[^a-z0-9]+", "-", s).strip("-")
|
|
if len(s) > 48:
|
|
s = s[:48].rsplit("-", 1)[0]
|
|
return s or "ohne-titel"
|
|
|
|
|
|
def hole(token: str):
|
|
issues, seite = [], 1
|
|
while True:
|
|
req = urllib.request.Request(
|
|
f"{API}?state=opened&per_page=100&page={seite}",
|
|
headers={"PRIVATE-TOKEN": token})
|
|
with urllib.request.urlopen(req) as antwort:
|
|
batch = json.load(antwort)
|
|
issues += batch
|
|
if len(batch) < 100:
|
|
return sorted(issues, key=lambda i: i["iid"])
|
|
seite += 1
|
|
|
|
|
|
def main() -> int:
|
|
root = Path(sys.argv[1])
|
|
tokenpfad = Path(sys.argv[2] if len(sys.argv) > 2
|
|
else Path.home() / ".config/gitlab-lab/token")
|
|
token = tokenpfad.read_text(encoding="utf-8").strip()
|
|
|
|
ziel = root / "docs/issues"
|
|
geschrieben = []
|
|
for i in hole(token):
|
|
iid, titel, labels = i["iid"], i["title"].strip(), i["labels"]
|
|
ms = (i.get("milestone") or {}).get("title", "")
|
|
m = re.match(r"^(M\d)\b", ms)
|
|
prio = [l.split(":")[1] for l in labels if l.startswith("priority:")]
|
|
if not m or len(prio) != 1:
|
|
sys.exit(f"ABBRUCH: #{iid} ohne eindeutigen Meilenstein/"
|
|
f"Priorität ({ms!r}, {prio!r}) — Befund, kein Füllwert.")
|
|
status = "open"
|
|
wartegrund = ""
|
|
if "status:doing" in labels:
|
|
status = "in-progress"
|
|
elif "status:next" in labels:
|
|
status = "next"
|
|
elif "status:wartet" in labels:
|
|
status = "waiting"
|
|
wartegrund = ("Grund im GitLab-Verlauf benannt (Import "
|
|
"2026-08-11); im nächsten Refinement präzisieren")
|
|
host = [l.split(":")[1] for l in labels if l.startswith("host:")]
|
|
area = [l.split(":")[1] for l in labels if l.startswith("area:")]
|
|
|
|
zeilen = ["---", "type: issue", f'id: "{iid:04d}"',
|
|
f"status: {status}", f"created: {i['created_at'][:10]}",
|
|
f"milestone: {m.group(1)}", f"priority: {prio[0]}"]
|
|
if i.get("due_date"):
|
|
zeilen.append(f"due: {i['due_date']}")
|
|
if host:
|
|
zeilen.append(f"host: {host[0]}")
|
|
if area:
|
|
zeilen.append(f"area: {area[0]}")
|
|
if wartegrund:
|
|
zeilen.append(f"wartegrund: {wartegrund}")
|
|
zeilen += [f'gitlab_iid: "{iid}"', "related: []", "---", ""]
|
|
|
|
beschreibung = (i.get("description") or "").replace("\r\n", "\n")
|
|
beschreibung = beschreibung.replace("](/uploads/",
|
|
f"]({UPLOADS}/uploads/")
|
|
kopf = (f"# {titel}\n\n"
|
|
f"> Import aus [management#{iid}]({i['web_url']}) "
|
|
f"(2026-08-11). Kommentare und Verlauf bleiben dort; "
|
|
f"kanonisch ist ab jetzt diese Datei (ADR-0012).\n\n")
|
|
datei = ziel / f"{iid:04d}-{slug(titel)}.md"
|
|
datei.write_text("\n".join(zeilen) + kopf + beschreibung.rstrip()
|
|
+ "\n", encoding="utf-8")
|
|
geschrieben.append(datei.name)
|
|
|
|
for name in geschrieben:
|
|
print(name)
|
|
print(f"import: {len(geschrieben)} Issues geschrieben")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|