40-validation/validate-glossary-references.py

#!/usr/bin/env python3
"""Prüft explizite Glossaridentitäten und semantische ART-DEF-Referenzen."""
import argparse,json,re
from pathlib import Path
ID=re.compile(r"ART-DEF-[0-9]{4}")
SEMANTIC=re.compile(r"\[\[(ART-DEF-[0-9]{4})\]\]")
PATH_LINK=re.compile(r"\[[^\]]+\]\(([^)]*ART-DEF-[0-9]{4}[^)]*\.md(?:#[^)]*)?)\)",re.I)

def frontmatter(text):
 if not text.startswith("---"):return {}
 parts=text.split("---",2)
 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(entries,roots):
 findings=[];catalog={};files=sorted(entries.glob("ART-DEF-*.md"))
 for path in files:
  data=frontmatter(path.read_text(encoding="utf-8"));identity=data.get("id")
  if not identity or not ID.fullmatch(identity):findings.append({"code":"INVALID_GLOSSARY_ID","location":str(path),"id":identity});continue
  if identity in catalog:findings.append({"code":"DUPLICATE_GLOSSARY_ID","id":identity,"locations":[catalog[identity],str(path)]})
  catalog[identity]=str(path)
 references=[];path_links=[];navigation_links=[]
 for repository,root in roots:
  for path in sorted(root.rglob("*.md")):
   text=path.read_text(encoding="utf-8");location=f"{repository}:{path.relative_to(root).as_posix()}"
   for identity in SEMANTIC.findall(text):
    references.append({"id":identity,"location":location})
    if identity not in catalog:findings.append({"code":"MISSING_GLOSSARY_ENTRY","id":identity,"location":location})
   for target in PATH_LINK.findall(text):
    record={"target":target,"location":location}
    if path.name=="README.md" and path.parent.resolve()==entries.resolve():navigation_links.append(record)
    else:path_links.append(record);findings.append({"code":"NON_CANONICAL_GLOSSARY_LINK",**record})
 return {"schemaVersion":"1.0","workItem":"SE-0027","status":"FAILED" if findings else "PASSED","entries":len(catalog),"explicitReferences":len(references),"pathLinks":len(path_links),"navigationPathLinks":len(navigation_links),"wordUsageEvaluated":False,"catalog":sorted(catalog),"references":references,"navigationLinks":navigation_links,"findings":findings}

def main():
 p=argparse.ArgumentParser();p.add_argument("--entries",type=Path,required=True);p.add_argument("--references",action="append",default=[],help="repository=path");p.add_argument("--output",type=Path);a=p.parse_args();roots=[]
 for value in a.references:
  repo,sep,path=value.partition("=")
  if not sep:p.error("--references benötigt repository=path")
  roots.append((repo,Path(path).resolve()))
 result=validate(a.entries.resolve(),roots);text=json.dumps(result,ensure_ascii=False,indent=2)+"\n"
 if a.output:a.output.parent.mkdir(parents=True,exist_ok=True);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()