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()