Merge commit 'cfaf67010017eab368216aded483a64126dbcb2e' as 'bahn/wissensdatenbank'
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
"""Review-Gate: schreibt Dokumente je nach Status in processed/ oder staging/.
|
||||
|
||||
Dedup (nur approved): Dieselbe Quelle-Seite (per page_identity, i.d.R. Confluence-Page-ID)
|
||||
darf je <scope>/<domaene> nur EIN Dokument im Feed erzeugen - auch wenn mehrere Quellen
|
||||
sie liefern (z.B. ein breiter confluence_tree UND eine dedizierte confluence_faq-Quelle).
|
||||
Bei Konkurrenz gewinnt die SPEZIFISCHERE Aufbereitung (FAQ > Seite/PDF), sonst die zuerst
|
||||
verarbeitete Quelle. So verschwinden die frueheren Hash-Suffix-Duplikate.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from ..model import ComponentType, Document, ReviewStatus, page_identity
|
||||
|
||||
# Aufbereitungs-Prioritaet bei gleicher Seite: FAQ (Q/A-Bloecke) schlaegt die generische
|
||||
# Seiten-/PDF-Darstellung (z.B. dieselbe FAQ-Seite als rohe Tabelle aus einem Tree).
|
||||
_PRIORITY = {ComponentType.FAQ: 2, ComponentType.PAGE: 1, ComponentType.PDF: 1}
|
||||
|
||||
|
||||
class ReviewGate:
|
||||
"""Schreibt Dokumente je nach Status:
|
||||
- approved -> `output_dir/processed/<scope>/<domaene>[/<tool>]/<slug>.md`
|
||||
- pending -> `staging_dir/pending/<domaene>/<slug>.md`
|
||||
Der Audit-Report (`review_report.json`) liegt im `staging_dir` (intern).
|
||||
|
||||
Konstruktor unterstuetzt zwei Aufrufformen:
|
||||
ReviewGate(output_dir="output", staging_dir="staging") # explizit
|
||||
ReviewGate(base) # Test-Convenience:
|
||||
-> output_dir = base/'output', staging_dir = base/'staging'
|
||||
"""
|
||||
|
||||
def __init__(self, output_dir: str | Path, staging_dir: str | Path | None = None):
|
||||
if staging_dir is None:
|
||||
# Test-Convenience: nur ein Pfad uebergeben -> staging unter <output>/staging
|
||||
# (kompatibel mit altem Layout, in dem alles flach unter einem tmp-Verzeichnis lag)
|
||||
self.output_dir = Path(output_dir)
|
||||
self.staging_dir = self.output_dir / "staging"
|
||||
else:
|
||||
self.output_dir = Path(output_dir)
|
||||
self.staging_dir = Path(staging_dir)
|
||||
self.processed = self.output_dir / "processed"
|
||||
self.staging = self.staging_dir # staging ist jetzt TOP-LEVEL (kein /staging-Suffix)
|
||||
self.report: list[dict] = []
|
||||
self._written: set[str] = set()
|
||||
self._approved: dict[tuple[str, str, str], dict] = {}
|
||||
|
||||
def _report_entry(self, doc: Document, path: Path) -> dict:
|
||||
# Pfad relativ zum jeweiligen Root abbilden, mit Prefix damit Konsumenten den
|
||||
# Bereich erkennen ("output/..." vs "staging/..."). staging zuerst pruefen, damit
|
||||
# auch ein verschachteltes Layout (staging unter output, z.B. in Tests) korrekt
|
||||
# als "staging/..." erkannt wird statt faelschlich als "output/staging/...".
|
||||
try:
|
||||
rel = "staging/" + str(path.relative_to(self.staging_dir))
|
||||
except ValueError:
|
||||
try:
|
||||
rel = "output/" + str(path.relative_to(self.output_dir))
|
||||
except ValueError:
|
||||
rel = str(path)
|
||||
return {
|
||||
"domain": doc.domain,
|
||||
"tool": doc.tool,
|
||||
"scope": doc.scope.value,
|
||||
"title": doc.title,
|
||||
"part": doc.part,
|
||||
"url": doc.url,
|
||||
"status": doc.review_status.value,
|
||||
"notes": doc.review_notes,
|
||||
"file": rel,
|
||||
}
|
||||
|
||||
def write(self, doc: Document) -> Path:
|
||||
if doc.review_status == ReviewStatus.APPROVED:
|
||||
# Struktur: <scope>/<domaene>[/<tool>] -> Chatbot kann in extern/ ueber
|
||||
# alle Domaenen/Tools suchen (extern/infraportal, extern/pathos, ...).
|
||||
base = self.processed / doc.scope.value / doc.domain
|
||||
if doc.tool and doc.tool not in ("allgemein", doc.domain):
|
||||
base = base / doc.tool
|
||||
|
||||
# Dedup: gleiche Seite je scope/domaene nur einmal.
|
||||
key = (doc.scope.value, doc.domain, page_identity(doc.url))
|
||||
prio = _PRIORITY.get(doc.component_type, 1)
|
||||
if page_identity(doc.url):
|
||||
existing = self._approved.get(key)
|
||||
if existing is not None:
|
||||
prev_path = Path(existing["path"])
|
||||
if prio > existing["priority"]:
|
||||
# spezifischere Aufbereitung gewinnt -> bestehende Datei ersetzen
|
||||
prev_path.write_text(doc.to_markdown(), encoding="utf-8")
|
||||
existing["priority"] = prio
|
||||
self.report[existing["idx"]] = self._report_entry(doc, prev_path)
|
||||
# gleich/niedriger -> Duplikat verwerfen (kein zweites File)
|
||||
return prev_path
|
||||
|
||||
target = base
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
path = target / f"{doc.slug()}.md"
|
||||
# Slug-Kollision NUR zwischen verschiedenen Seiten (gleiche Seite ist oben
|
||||
# bereits dedupliziert) -> per content_hash disambiguieren.
|
||||
if str(path) in self._written:
|
||||
path = target / f"{doc.slug()}-{doc.content_hash}.md"
|
||||
self._written.add(str(path))
|
||||
path.write_text(doc.to_markdown(), encoding="utf-8")
|
||||
self.report.append(self._report_entry(doc, path))
|
||||
if page_identity(doc.url):
|
||||
self._approved[key] = {"path": str(path), "priority": prio, "idx": len(self.report) - 1}
|
||||
return path
|
||||
|
||||
# pending -> staging (kein Dedup; Transparenz)
|
||||
target = self.staging / doc.review_status.value / doc.domain
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
path = target / f"{doc.slug()}.md"
|
||||
if str(path) in self._written:
|
||||
path = target / f"{doc.slug()}-{doc.content_hash}.md"
|
||||
self._written.add(str(path))
|
||||
path.write_text(doc.to_markdown(), encoding="utf-8")
|
||||
self.report.append(self._report_entry(doc, path))
|
||||
return path
|
||||
|
||||
def write_report(self) -> Path:
|
||||
# Audit-Log gehoert in den internen Bereich (staging), nicht in den output-Feed.
|
||||
self.staging_dir.mkdir(parents=True, exist_ok=True)
|
||||
path = self.staging_dir / "review_report.json"
|
||||
path.write_text(json.dumps(self.report, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
return path
|
||||
|
||||
def summary(self) -> dict[str, int]:
|
||||
out: dict[str, int] = {}
|
||||
for r in self.report:
|
||||
out[r["status"]] = out.get(r["status"], 0) + 1
|
||||
return out
|
||||
Reference in New Issue
Block a user