40-validation/validate-learning-nuggets.py

#!/usr/bin/env python3
import argparse,json,re
from pathlib import Path
FIELDS=("id","artifact_guid","artifact_type","status","version","owner","language")
HEADINGS=("Thema","Das Problem","Anschauliches Bild","Grenzen","Weiterführende Artefakte")
REF=re.compile(r"\[[^\]]+\]\(([^)]+)\)")
def frontmatter(text):
 if not text.startswith("---"):return {}
 parts=text.split("---",2);result={}
 if len(parts)<3:return result
 for line in parts[1].splitlines():
  key,sep,value=line.partition(":")
  if sep:result[key.strip()]=value.strip().strip('"')
 return result
def validate(root):
 findings=[];records=[]
 for path in sorted(root.glob("NUG-*.md")):
  text=path.read_text(encoding="utf-8");meta=frontmatter(text);heads=set(re.findall(r"^##\s+(.+?)\s*$",text,re.M));missing=[f for f in FIELDS if not meta.get(f)];missing_heads=[h for h in HEADINGS if h not in heads];meaning=[h for h in heads if h.startswith("Bedeutung für")];refs=REF.findall(text)
  if missing:findings.append({"code":"MISSING_FIELDS","file":path.name,"fields":missing})
  if meta.get("artifact_type")!="Learning Nugget":findings.append({"code":"INVALID_ARTIFACT_TYPE","file":path.name})
  if missing_heads or not meaning:findings.append({"code":"MISSING_SECTIONS","file":path.name,"sections":missing_heads+([] if meaning else ["Bedeutung für …"])})
  if not refs:findings.append({"code":"MISSING_SOURCE_REFERENCES","file":path.name})
  records.append({"file":path.name,"id":meta.get("id"),"references":len(refs)})
 return {"schemaVersion":"1.0","workItem":"SE-0029","status":"FAILED" if findings else "PASSED","nuggets":len(records),"sourceReferences":sum(r["references"] for r in records),"contentTruthAssessed":False,"records":records,"findings":findings}
def main():
 p=argparse.ArgumentParser();p.add_argument("root",type=Path);p.add_argument("--output",type=Path);a=p.parse_args();r=validate(a.root);text=json.dumps(r,ensure_ascii=False,indent=2)+"\n"
 if a.output:a.output.parent.mkdir(parents=True,exist_ok=True);a.output.write_text(text)
 else:print(text,end="")
 raise SystemExit(0 if r["status"]=="PASSED" else 1)
if __name__=="__main__":main()