SSoT: Status-Marker aus den TP-Headern entfernt, Sync-Werkzeug auf Anlege-Richtung festgelegt
Die `### TPnn`-Header trugen weiterhin Status-Anspruche (▸ ✅ FERTIG …, ▸ ⏳ CUTOVER …),
obwohl Index/Status seit dem TP36-Cutover in Gitea liegt. Das war eine dritte Statusquelle,
die den Labels nachlief — Header-Marker datierten teils Wochen vor dem Issue-Update.
- 75 Header (32 aktiv + 43 Archiv) auf `### TPnn · Gitea #N — <Titel> — <Caveat>` umgestellt.
Caveats, Warnungen, Querverweise und Archiv-Links bleiben erhalten, nur der Status faellt weg.
Status-Marker im Fliesstext (Messchronik, Unterpunkte) bleiben — sie beschreiben Historie.
- TP47 fehlte als Gitea-Issue komplett (nur in der Datei seit 808d3a1) -> als #74 angelegt.
- bin/gitea-tp-sync.py schrieb in die falsche Richtung: der Abgleich leitete den Zielzustand
aus dem Datei-Marker ab (fest ✅) und haette #24 (TP34, bewusst status:dropped) auf status:done
gezogen. Jetzt werden nur noch FEHLENDE Archiv-Issues angelegt, bestehende nie angefasst.
- CLAUDE.md: Header-Form + Werkzeug-Richtung festgehalten.
- Announcement 2026-10-03 für die naechste Session.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,22 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TP36 (reframed) — teilprojekte.md Index/Status → Gitea Issues (admin/bf-infra).
|
||||
TP36 (reframed) — Archiv-Backfill: fehlende Archiv-TPs als Gitea-Issues anlegen.
|
||||
|
||||
Idempotenter Sync (get-or-create, Match ueber "TPnn" im Titel):
|
||||
- Archiv-Header in docs/teilprojekte-archiv.md -> closed Issues (nur TPs, die in Gitea
|
||||
NICHT offen sind; ein reopentes TP mit Archiv-Zeiger bleibt offen).
|
||||
- Labels status:* (Marker) + thema:* (Gruppierung) werden vorab get-or-create.
|
||||
🔴 RICHTUNG (seit 2026-10-03): **Gitea ist die SSoT fuer Index/Status.** Dieses Werkzeug
|
||||
liest die Datei NUR, um zu pruefen, ob jedes archivierte TP ein Issue hat. Es schreibt
|
||||
**ausschliesslich** NEUE Issues fuer noch nicht existierende Archiv-TPs — existierende
|
||||
Issues werden NIE angefasst (kein Label-, kein State-Patch).
|
||||
|
||||
Warum das wichtig ist: die Vorgaenger-Version glich auch bestehende Issues ab und leitete
|
||||
den Zielzustand aus dem Datei-Marker ab (fest `✅`). Damit konnte sie ein bewusst gesetztes
|
||||
Label ueberschreiben — so wurde #24 (TP34, `status:dropped`) auf `status:done` und #32
|
||||
(TP43) auf ein falsches `thema:` gezogen. Die Richtung Datei → Gitea fuer *Status* ist
|
||||
seit dem TP36-Cutover (2026-09-07) falsch; sie bleibt hier nur fuer das *Anlegen* erhalten.
|
||||
|
||||
Abgrenzung:
|
||||
- Aktive TPs: werden DIREKT als Gitea-Issue gepflegt (nicht hier).
|
||||
- Status/Thema/State bestehender Issues: aendert nur der Mensch im Board.
|
||||
- Per-TP-Prosa: bleibt in docs/teilprojekte.md / teilprojekte-archiv.md.
|
||||
|
||||
SSoT-Modell (seit TP36-Cutover 2026-09-07): Gitea fuehrt Index+Status — aktive TPs werden
|
||||
DIREKT als Gitea-Issue angelegt/gepflegt, NICHT mehr aus einer Datei-Tabelle abgeleitet
|
||||
(die Quick-Index-Tabelle wurde aus teilprojekte.md entfernt). Dieses Werkzeug haelt nur noch
|
||||
die ARCHIVIERTEN (closed) TPs mit teilprojekte-archiv.md idempotent abgeglichen und fasst
|
||||
offene Issues nie an. Die per-TP-Prosa bleibt in docs/teilprojekte.md / teilprojekte-archiv.md.
|
||||
Token: ~/.hermes/.env (GITEA_ACCESS_TOKEN, GITEA_HOST) — wird NIE ausgegeben.
|
||||
|
||||
Aufruf:
|
||||
python3 bin/gitea-tp-sync.py # Dry-Run (default) — zeigt, was passieren wuerde
|
||||
python3 bin/gitea-tp-sync.py --commit # legt Labels + Issues an / gleicht sie ab
|
||||
python3 bin/gitea-tp-sync.py # Dry-Run (default) — zeigt, was fehlt
|
||||
python3 bin/gitea-tp-sync.py --commit # legt fehlende Archiv-Issues an
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
@@ -33,16 +39,6 @@ HERMES = Path.home() / ".hermes" / ".env"
|
||||
OWNER_REPO = "admin/bf-infra"
|
||||
COMMIT = "--commit" in sys.argv
|
||||
|
||||
# Marker -> (status-label, gitea-state)
|
||||
MARKER = {
|
||||
"🔜": ("status:todo", "open"),
|
||||
"⏳": ("status:wip", "open"),
|
||||
"⏸️": ("status:paused", "open"),
|
||||
"💡": ("status:idea", "open"),
|
||||
"🟢": ("status:concept", "open"),
|
||||
"✅": ("status:done", "closed"),
|
||||
"❌": ("status:dropped", "closed"),
|
||||
}
|
||||
LABEL_COLORS = {
|
||||
"status:todo": "#ededed", "status:wip": "#fbca04", "status:paused": "#c5def5",
|
||||
"status:idea": "#d4c5f9", "status:concept": "#bfdadc", "status:done": "#0e8a16",
|
||||
@@ -105,23 +101,31 @@ def api(method, path, body=None):
|
||||
|
||||
# --------------------------------------------------------------------------- parse
|
||||
def parse_archiv(exclude_nums):
|
||||
"""Archiv-Header -> archivierte TPs. exclude_nums = TP-Nummern, die in Gitea OFFEN sind
|
||||
(reopente TPs mit Archiv-Zeiger) -> die bleiben offen, nicht als closed abgleichen."""
|
||||
"""Archiv-Header -> archivierte TPs, die ein Issue bekommen SOLLEN.
|
||||
|
||||
Header-Form seit 2026-10-03: `### TP25 · Gitea #17 — WLAN-Stabilisierung — …`
|
||||
Der Status-Anspruch (frueher `▸ ✅ FERTIG …`) ist entfernt; die Datei traegt keinen
|
||||
Status mehr. Der Gitea-Zeiger `· Gitea #N` ist die Bruecke, falls er schon da ist.
|
||||
|
||||
exclude_nums = TP-Nummern, die in Gitea OFFEN sind -> die brauchen kein neues Issue.
|
||||
"""
|
||||
out = []
|
||||
hdr = re.compile(r"^#{2,4}\s*(TP[\d.\-A-Za-z]+)\s*—\s*(.+?)\s*▸", )
|
||||
hdr = re.compile(r"^#{2,4}\s*(TP[\d.\-A-Za-z]+)\b\s*(?:·\s*Gitea\s*#(\d+)\s*)?—\s*(.+?)\s*$")
|
||||
seen = set()
|
||||
for line in ARCHIV.read_text().splitlines():
|
||||
m = hdr.match(line)
|
||||
if not m:
|
||||
continue
|
||||
tp, title = m.groups()
|
||||
tp, issue_no, title = m.groups()
|
||||
num = tp[2:]
|
||||
if num in exclude_nums or num in seen:
|
||||
continue
|
||||
seen.add(num)
|
||||
short = re.sub(r"\*\*|`", "", title).strip()[:80]
|
||||
out.append({"tp": tp, "num": num, "marker": "✅",
|
||||
"title": short, "date": "", "src": "archiv"})
|
||||
# Caveat hinter dem zweiten ` — ` abschneiden; nur der Kurztitel gehoert ins Issue.
|
||||
short = re.split(r"\s+—\s+", title)[0]
|
||||
short = re.sub(r"\*\*|`", "", short).strip()[:80]
|
||||
out.append({"tp": tp, "num": num, "issue": issue_no,
|
||||
"title": short, "src": "archiv"})
|
||||
return out
|
||||
|
||||
|
||||
@@ -173,43 +177,44 @@ def main():
|
||||
print(f"Geparst: {len(items)} archivierte TP (aktive TPs = SSoT in Gitea: "
|
||||
f"{len(open_nums)} offen, nicht angetastet)\n")
|
||||
|
||||
created = updated = 0
|
||||
created = skipped = 0
|
||||
for it in items:
|
||||
status_label, state = MARKER.get(it["marker"], ("status:todo", "open"))
|
||||
# Archiv-TPs sind abgeschlossen -> neu angelegte Issues gehen direkt auf closed.
|
||||
status_label, state = "status:done", "closed"
|
||||
labels = [status_label]
|
||||
th = THEME.get(it["num"])
|
||||
if th:
|
||||
labels.append(f"thema:{th}")
|
||||
title = f"{it['tp']} — {it['title']}"
|
||||
body = (f"Steuerungs-SSoT: Index/Status hier in Gitea; **Prosa/Detail** im Repo unter "
|
||||
f"`docs/{'teilprojekte-archiv.md' if it['src']=='archiv' else 'teilprojekte.md'}` "
|
||||
f"(Abschnitt `{it['tp']}`).\n\n"
|
||||
f"- Status: {it['marker']} ({status_label})\n"
|
||||
+ (f"- Abschluss: {it['date']}\n" if it["date"] else ""))
|
||||
f"`docs/teilprojekte-archiv.md` (Abschnitt `{it['tp']}`).\n\n"
|
||||
f"- Status: ✅ ({status_label})\n"
|
||||
f"- Angelegt aus dem Archiv-Backfill (`bin/gitea-tp-sync.py`).\n")
|
||||
want_ids = [label_id[l] for l in labels if l in label_id]
|
||||
|
||||
cur = by_tp.get(it["tp"])
|
||||
if cur is None:
|
||||
print(f" [{'+' if COMMIT else 'DRY'}] NEW {title} {labels} state={state}")
|
||||
if COMMIT:
|
||||
sc, res = api("POST", f"/repos/{OWNER_REPO}/issues",
|
||||
{"title": title, "body": body, "labels": want_ids})
|
||||
if state == "closed" and isinstance(res, dict) and res.get("number"):
|
||||
api("PATCH", f"/repos/{OWNER_REPO}/issues/{res['number']}", {"state": "closed"})
|
||||
created += 1
|
||||
else:
|
||||
have = {l["name"] for l in cur.get("labels", [])}
|
||||
drift = (have != set(labels)) or (cur.get("state") != state)
|
||||
if drift:
|
||||
print(f" [{'~' if COMMIT else 'DRY'}] UPD #{cur['number']} {title} "
|
||||
f"{sorted(have)}->{labels} state {cur.get('state')}->{state}")
|
||||
if COMMIT:
|
||||
api("PATCH", f"/repos/{OWNER_REPO}/issues/{cur['number']}",
|
||||
{"labels": want_ids, "state": state})
|
||||
updated += 1
|
||||
if cur is not None:
|
||||
# 🔴 Existierendes Issue: NICHT anfassen. Weder Label noch State noch Body.
|
||||
# Status/Thema aendert nur der Mensch im Board (Gitea = SSoT seit TP36-Cutover).
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
# Fehlt das Issue, obwohl der Header schon einen Zeiger traegt? -> Zeiger ist stale.
|
||||
hint = f" ⚠ Datei-Header nennt bereits Gitea #{it['issue']} — Issue fehlt!" if it.get("issue") else ""
|
||||
print(f" [{'+' if COMMIT else 'DRY'}] NEW {title} {labels} state={state}{hint}")
|
||||
if COMMIT:
|
||||
sc, res = api("POST", f"/repos/{OWNER_REPO}/issues",
|
||||
{"title": title, "body": body, "labels": want_ids})
|
||||
if isinstance(res, dict) and res.get("number"):
|
||||
api("PATCH", f"/repos/{OWNER_REPO}/issues/{res['number']}", {"state": "closed"})
|
||||
print(f" -> #{res['number']} angelegt (closed). Zeiger in "
|
||||
f"docs/teilprojekte-archiv.md nachtragen: `· Gitea #{res['number']}`")
|
||||
else:
|
||||
print(f" !! Anlegen fehlgeschlagen: {res}")
|
||||
created += 1
|
||||
|
||||
print(f"\n{'Angelegt' if COMMIT else 'Wuerde anlegen'}: {created} · "
|
||||
f"{'Abgeglichen' if COMMIT else 'Wuerde abgleichen'}: {updated}")
|
||||
f"unveraendert gelassen (SSoT = Gitea): {skipped}")
|
||||
if not COMMIT:
|
||||
print("Dry-Run — nichts geschrieben. Mit --commit ausfuehren.")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user