40-validation/validate-semantic-ids.py
#!/usr/bin/env python3
import argparse,json,re
from pathlib import Path
ID=re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]*$")
def validate(inputs):
units=[];errors=[]
for repo,path in inputs:
try:d=json.loads(path.read_text(encoding="utf-8"))
except Exception as e:errors.append({"code":"INVALID_MANIFEST","location":f"{repo}:{path}","message":str(e)});continue
for i,u in enumerate(d.get("semanticUnits",[])):
loc=f"{repo}:{path}#semanticUnits[{i}]";cid=u.get("id");aliases=u.get("legacyIds",[])
if not isinstance(cid,str) or not ID.fullmatch(cid):errors.append({"code":"INVALID_ID","id":cid,"location":loc})
for a in aliases:
if not isinstance(a,str) or not ID.fullmatch(a):errors.append({"code":"INVALID_ALIAS","id":a,"location":loc})
units.append((cid,set(aliases),loc))
canon={}
for cid,aliases,loc in units:
canon.setdefault(cid,[]).append(loc)
for cid,locs in canon.items():
if len(locs)>1:errors.append({"code":"DUPLICATE_CANONICAL_ID","id":cid,"locations":locs})
amap={}
for cid,aliases,loc in units:
for a in aliases:amap.setdefault(a,[]).append((cid,loc))
for a,owners in amap.items():
distinct={x[0] for x in owners}
if len(distinct)>1:errors.append({"code":"ALIAS_COLLISION","id":a,"locations":[x[1] for x in owners]})
if a in canon and any(cid!=a for cid,_ in owners):errors.append({"code":"ALIAS_SHADOWS_CANONICAL_ID","id":a,"locations":canon[a]+[x[1] for x in owners]})
return {"schemaVersion":"1.0","workItem":"SE-0024","status":"PASSED" if not errors else "FAILED","manifests":len(inputs),"semanticUnits":len(units),"canonicalIds":len(canon),"aliases":len(amap),"errors":errors}
def main():
p=argparse.ArgumentParser();p.add_argument("--input",action="append",required=True,help="repository=path");p.add_argument("--output",type=Path);a=p.parse_args();items=[]
for x in a.input:
repo,sep,path=x.partition("=")
if not sep:p.error("--input benötigt repository=path")
items.append((repo,Path(path)))
r=validate(items);text=json.dumps(r,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 r["status"]=="PASSED" else 1)
if __name__=="__main__":main()