Merge commit 'cfaf67010017eab368216aded483a64126dbcb2e' as 'bahn/wissensdatenbank'

This commit is contained in:
2026-06-30 21:19:25 +02:00
4724 changed files with 667022 additions and 0 deletions
@@ -0,0 +1,101 @@
"""EINMALIGES Bootstrap-Skript: erzeugt config/tools.yaml aus der Support-Seite.
Findet alle "Tool <Name>"-Unterseiten von Seite 428909879 und schreibt pro Tool
einen Katalogeintrag (Name OHNE "Tool "-Praefix). Tool-weiter Scope = intern
(restriktiv). Danach wird config/tools.yaml MANUELL gepflegt.
Aufruf:
CONFLUENCE_URL=... CONFLUENCE_TOKEN=... python scripts/bootstrap_tools.py
"""
from __future__ import annotations
import os
import re
from atlassian import Confluence
SUPPORT_PAGE = "428909879"
BASE = "https://arija-confluence.jaas.service.deutschebahn.com/spaces/BES/pages"
HEADER = """\
# =============================================================================
# TOOL-KATALOG der Wissensdatenbank
# Initial generiert aus 'Zentraler 1st Level Support' (428909879).
# AB JETZT MANUELL PFLEGEN. Allgemeines Wissen steht in config/general.yaml.
#
# AUFBAU PRO TOOL:
# - id / name / domain
# scope: intern # tool-weit: intern | extern | allgemein | mixed
# owners: ["a@db.de"] # intern Verantwortliche
# contact: "team@db.de" # herausgebbarer Kontakt (Default einfachbahn@deutschebahn.com)
# sources: # 1..n Quellen, beliebig mischbar
# - url: "..."
# strategy: confluence_tree # confluence_page|confluence_tree|confluence_faq|
# # crawler|sitemap|gitlab_md|file|pdf
# # scope/Optionen pro Quelle optional ueberschreibbar
#
# SCOPE:
# intern -> alles intern (Default, restriktiv; z.B. CRM Salesforce)
# extern -> alles extern
# allgemein -> gilt fuer intern UND extern
# mixed -> Tool hat Quellen mit unterschiedlichem Scope (KEINE Trennung pro Seite;
# im Zweifel zwei Quellen). Source-scope kann "intern,extern" sein.
#
# confluence_tree-Tiefe: options.max_depth (-1=alle Unterseiten, 0=nur Seite)
# =============================================================================
tools:
"""
def slug(s: str) -> str:
s = re.sub(r"^tool\s+", "", s.strip(), flags=re.I)
s = re.sub(r"[^a-z0-9]+", "-", s.lower())
return s.strip("-")[:40]
def clean_name(s: str) -> str:
return re.sub(r"^tool\s+", "", s.strip(), flags=re.I).strip()
def walk(client, pid, out=None):
out = out if out is not None else []
for ch in client.get_child_pages(pid) or []:
out.append((ch.get("id"), ch.get("title")))
walk(client, ch.get("id"), out)
return out
def main() -> None:
client = Confluence(
url=os.environ["CONFLUENCE_URL"], token=os.environ["CONFLUENCE_TOKEN"], cloud=False
)
tools = [(pid, t.strip()) for pid, t in walk(client, SUPPORT_PAGE)
if t.strip().lower().startswith("tool")]
lines = [HEADER.rstrip("\n")]
seen = set()
for pid, title in tools:
sid = slug(title)
if sid in seen:
sid = f"{sid}-{pid}"
seen.add(sid)
url = f"{BASE}/{pid}/" + re.sub(r"[^A-Za-z0-9]+", "+", title).strip("+")
lines += [
f" - id: {sid}",
f' name: "{clean_name(title)}"',
f" domain: {sid}",
" scope: intern",
" sources:",
f' - url: "{url}"',
" strategy: confluence_tree",
f' tags: ["{sid}"]',
]
with open("config/tools.yaml", "w", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n")
print(f"config/tools.yaml geschrieben: {len(tools)} Tools (alle scope: intern)")
if __name__ == "__main__":
main()
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Halb-/Vollautomatik: GitLab-Issue "Neues Wissen" -> tools.yaml-Eintrag -> MR (mit Vorschau).
# Voraussetzungen: glab (GitLab CLI) eingeloggt, python-venv aktiv, CONFLUENCE_* gesetzt.
#
# Aufruf: scripts/issue_to_mr.sh <ISSUE_ID>
# Wiring-Optionen: manuell durch Reviewer ODER GitLab-Webhook (Issue-Event) ODER
# scheduled Job, der offene Issues mit Label "neues-wissen" abarbeitet.
set -euo pipefail
ISSUE_ID="${1:?Usage: issue_to_mr.sh <ISSUE_ID>}"
BRANCH="wissen/issue-${ISSUE_ID}"
echo "1) Issue ${ISSUE_ID} lesen und Eintrag erzeugen ..."
glab issue view "${ISSUE_ID}" --output json | python -c 'import sys,json;print(json.load(sys.stdin).get("description",""))' \
| python scripts/issue_to_source.py --append
echo "2) Branch + Commit ..."
git checkout -B "${BRANCH}"
git add config/tools.yaml
git commit -m "feat(wissen): neue Quelle aus Issue #${ISSUE_ID}"
echo "3) Vorschau erzeugen (nur die neue Quelle) ..."
# Tool-Slug = letzter hinzugefuegter id-Eintrag
TOOL=$(grep -E '^\s*- id:' config/tools.yaml | tail -1 | sed 's/.*id:\s*//')
python -m src.main --config config/tools.yaml --data preview --only "${TOOL}" || true
echo "4) MR anlegen (Vorschau-Hinweis im Body) ..."
git push -u origin "${BRANCH}"
glab mr create --fill --yes \
--description "Automatisch aus Issue #${ISSUE_ID}. Vorschau-Markdown siehe CI-Job 'preview' (PREVIEW_ONLY=${TOOL}). Bitte pruefen und mergen = Freigabe." \
--label "neues-wissen" || echo "MR-Anlage manuell pruefen (glab nicht verfuegbar?)"
echo "Fertig. Reviewer prueft Vorschau im MR und merged (= Freigabe)."
@@ -0,0 +1,149 @@
"""Wandelt ein ausgefuelltes 'Neues Wissen'-Issue in einen tools.yaml-Eintrag.
Kern der Automatisierung (Idee): Issue -> yaml-Eintrag -> MR (mit Vorschau).
Aufruf:
# Issue-Text aus Datei oder STDIN:
python scripts/issue_to_source.py < issue.md
glab issue view 42 | python scripts/issue_to_source.py
# direkt an config/tools.yaml anhaengen:
python scripts/issue_to_source.py --append < issue.md
Erwartet die Abschnitte der Vorlage (.gitlab/issue_templates/Neues_Wissen.md):
Tool/Domaene, Link(s), Verarbeitung (Strategie), Sichtbarkeit (Scope),
Ansprechpartner, Optionen, Hinweise.
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from src.strategy_detect import detect_strategy, sniff_strategy # noqa: E402
FIELD_MAP = {
"tool": ["welches tool", "tool", "domaene", "domäne"],
"sources": ["quellen", "quelle", "link"],
"owners": ["verantwortlich", "owners"],
"contact": ["kontakt", "contact"],
"options": ["optionen"],
}
def _slug(s: str) -> str:
s = re.sub(r"[^a-z0-9]+", "-", s.lower())
return s.strip("-")[:40] or "neues-wissen"
def parse_issue(text: str) -> dict:
"""Zerlegt den Issue-Text in Felder (anhand der ###-Ueberschriften).
Quellen-Zeilen haben das Format: URL | Strategie | Scope
(Strategie/Scope optional). Fuehrendes "- " (Markdown-Liste) wird entfernt.
"""
sections: dict[str, str] = {}
cur = None
buf: list[str] = []
for line in text.splitlines():
m = re.match(r"^#{2,4}\s+(.*)", line.strip())
if m:
if cur:
sections[cur] = "\n".join(buf).strip()
cur = m.group(1).strip().lower()
buf = []
elif cur:
if line.strip().startswith("<!--") or line.strip().startswith("/label"):
continue
buf.append(line)
if cur:
sections[cur] = "\n".join(buf).strip()
def find(key: str) -> str:
for head, val in sections.items():
if any(alias in head for alias in FIELD_MAP[key]):
return val.strip()
return ""
sources = []
for raw in find("sources").splitlines():
ln = raw.strip().lstrip("-").strip()
if "http" not in ln:
continue
parts = [p.strip() for p in ln.split("|")]
url = parts[0].strip()
if not url.startswith("http"):
continue
strat = parts[1].strip() if len(parts) > 1 and parts[1].strip() else ""
scope = parts[2].strip() if len(parts) > 2 and parts[2].strip() else ""
sources.append({"url": url, "strategy": strat, "scope": scope})
owners = [o.strip() for o in re.split(r"[,\n]", find("owners")) if o.strip()]
contact = find("contact").splitlines()[0].strip() if find("contact") else ""
return {
"tool": find("tool").splitlines()[0].strip() if find("tool") else "",
"sources": sources,
"owners": owners,
"contact": contact,
}
def build_entry(data: dict, sniff: bool = False) -> str:
from src.strategy_detect import KNOWN
tool = data["tool"] or "neues-tool"
sid = _slug(tool)
owners = ", ".join(f'"{o}"' for o in data["owners"])
contact = data.get("contact", "").strip()
detect = sniff_strategy if sniff else detect_strategy
lines = [
f" - id: {sid}",
f' name: "{tool}"',
f" domain: {sid}",
" scope: mixed",
f" owners: [{owners}]",
]
if contact:
lines.append(f' contact: "{contact}"')
lines.append(" sources:")
unknown = []
for src in data["sources"] or [{"url": "<URL_FEHLT>", "strategy": "", "scope": ""}]:
url = src["url"]
strat = src["strategy"] if src["strategy"] in KNOWN else (detect(url) or "UNBEKANNT")
scope = src["scope"] or "intern"
if strat == "UNBEKANNT":
unknown.append(url)
lines.append(f" # !! Keine passende Strategie erkannt -> neue Strategie pruefen: {url}")
lines += [
f' - url: "{url}"',
f" strategy: {strat}",
f' scope: "{scope}"',
f' tags: ["{sid}"]',
]
out = "\n".join(lines) + "\n"
if unknown:
out += "\n# WARNUNG: " + str(len(unknown)) + " URL(s) ohne passende Strategie -> ggf. neue Strategie bauen.\n"
return out
def main() -> None:
parser = argparse.ArgumentParser(description="Issue -> tools.yaml-Eintrag")
parser.add_argument("--append", action="store_true", help="an config/tools.yaml anhaengen")
parser.add_argument("--sniff", action="store_true", help="Strategie per Netzwerk verfeinern (HEAD/GET)")
parser.add_argument("--config", default="config/tools.yaml")
args = parser.parse_args()
text = sys.stdin.read()
entry = build_entry(parse_issue(text), sniff=args.sniff)
if args.append:
with open(args.config, "a", encoding="utf-8") as f:
f.write("\n" + entry)
print(f"angehaengt an {args.config}:\n\n{entry}")
else:
print(entry)
if __name__ == "__main__":
main()