40-validation/validate-knowledge-baseline.py

#!/usr/bin/env python3
"""Validiert die SE-0022-Inventur- und Wissensfüllstandsbaseline."""
import argparse, csv, json
from pathlib import Path

EXPECTED_CATEGORIES = {"visions","meanings","rules","glossary","learning-nuggets","relationships","plateaus","capabilities","building-blocks","artifacts"}

def validate(legacy_root: Path, inventory: Path, dashboard: Path) -> dict:
    errors=[]
    with inventory.open(encoding="utf-8", newline="") as f:
        rows=list(csv.DictReader(f, delimiter="\t"))
    required={"path","id","artifact_type","status","version","title","erreichbar_ueber"}
    if not rows or not required.issubset(rows[0]): errors.append("Inventarspalten unvollständig")
    paths=[r.get("path","") for r in rows]; ids=[r.get("id","") for r in rows]
    missing=[p for p in paths if not (legacy_root/p).is_file()]
    if len(rows)!=307: errors.append(f"Quellenkarte enthält {len(rows)} statt 307 Einträge")
    if len(set(paths))!=len(paths): errors.append("Quellenkarte enthält doppelte Pfade")
    if not all(ids) or len(set(ids))!=len(ids): errors.append("Quellenkarte enthält leere oder doppelte IDs")
    if missing: errors.append(f"{len(missing)} Quellenpfade fehlen")
    data=json.loads(dashboard.read_text(encoding="utf-8")); categories=data.get("categories",[]); totals=data.get("totals",{})
    if {c.get("id") for c in categories}!=EXPECTED_CATEGORIES: errors.append("Dashboard-Kategorien widersprechen dem Vertrag")
    coverage=totals.get("coverage",{}); maturity=totals.get("maturity",{})
    if coverage.get("complete",0)+coverage.get("partial",0)+coverage.get("legacyOnly",0)!=totals.get("legacy"): errors.append("Coverage-Summe widerspricht Legacy-Nenner")
    if sum(maturity.values())!=totals.get("legacy"): errors.append("Reifegradsumme widerspricht Legacy-Nenner")
    for key in ("complete","partial","legacyOnly","evolved"):
        if sum(c["coverage"][key] for c in categories)!=coverage.get(key): errors.append(f"Coverage-Kategoriesumme {key} widerspricht Total")
    for key in ("found","understood","consolidated","anchored","notAssessed"):
        if sum(c["maturity"][key] for c in categories)!=maturity.get(key): errors.append(f"Reifegrad-Kategoriesumme {key} widerspricht Total")
    if totals.get("reviewed") is not None or totals.get("approved") is not None: errors.append("Unbelegte Review-/Freigabewerte müssen null sein")
    return {"schemaVersion":"1.0","workItem":"SE-0022","status":"PASSED" if not errors else "FAILED","sourceInventory":{"entries":len(rows),"uniquePaths":len(set(paths)),"uniqueIds":len(set(ids)),"missingPaths":missing},"coverage":coverage,"maturity":maturity,"unassignedLegacyUnits":coverage.get("legacyOnly"),"deviations":data.get("deviations",[]),"errors":errors}

def main():
    p=argparse.ArgumentParser(); p.add_argument("--legacy-root",type=Path,required=True); p.add_argument("--inventory",type=Path,required=True); p.add_argument("--dashboard",type=Path,required=True); p.add_argument("--output",type=Path)
    a=p.parse_args(); result=validate(a.legacy_root,a.inventory,a.dashboard); text=json.dumps(result,ensure_ascii=False,indent=2)+"\n"
    if a.output: a.output.write_text(text,encoding="utf-8")
    else: print(text,end="")
    raise SystemExit(0 if result["status"]=="PASSED" else 1)
if __name__=="__main__": main()