#!/usr/bin/env python3
from __future__ import annotations
import argparse, json, re, subprocess, sys, tempfile
from collections import Counter
from pathlib import Path
from typing import Any, Dict, List, Set
import yaml
from jsonschema import Draft202012Validator, FormatChecker
from rdflib import Graph, Namespace, RDF, RDFS, OWL, URIRef

EXIT_OK=0; EXIT_DEFECT=4; EXIT_RUNTIME=2
GEARM=Namespace('https://specs.kivanura.org/gearm#')
META=Namespace('https://specs.kivanura.org/gearm/meta#')
GEARMI=Namespace('https://specs.kivanura.org/gearm/invariant/')
GEARMSH=Namespace('https://specs.kivanura.org/gearm/shacl#')
SH=Namespace('http://www.w3.org/ns/shacl#')
DCT=Namespace('http://purl.org/dc/terms/')
SKOS=Namespace('http://www.w3.org/2004/02/skos/core#')

class Reconciler:
    def __init__(self, root:Path):
        self.root=root
        self.bps=root.parents[3]
        self.spec=self.bps/'docs/tools/gearm/0.1/gearm-0.1-specification.md'
        self.results=[]
        self.details={}
        self.concepts=yaml.safe_load((root/'gearm-0.1.concept-registry.yaml').read_text())
        self.rels=yaml.safe_load((root/'gearm-0.1.relationship-registry.yaml').read_text())
        self.invs=yaml.safe_load((root/'gearm-0.1.invariant-catalogue.yaml').read_text())
        self.ctx=json.loads((root/'gearm-0.1.context.jsonld').read_text())
        self.schema=json.loads((root/'gearm-0.1.schema.json').read_text())
        self.profile=json.loads((root/'gearm-0.1.profile.schema.json').read_text())
        self.manifest=json.loads((root/'testcases/manifest.json').read_text())
        self.a4=Graph().parse(root/'gearm-0.1.ttl',format='turtle')
        self.a6=Graph().parse(root/'gearm-0.1.shacl.ttl',format='turtle')
        self.c_by_id={x['id']:x for x in self.concepts['concepts']}
        self.c_by_q={x['qname']:x for x in self.concepts['concepts']}
        self.c_by_iri={x['iri']:x for x in self.concepts['concepts']}
        self.r_by_id={x['id']:x for x in self.rels['relationships']}
        self.r_by_q={x['qname']:x for x in self.rels['relationships']}
        self.r_by_iri={x['iri']:x for x in self.rels['relationships']}
        self.i_by_id={x['id']:x for x in self.invs['invariants']}
    def add(self,id,category,ok,summary,detail=None,severity='error'):
        rec={'id':id,'category':category,'status':'PASS' if ok else ('WARN' if severity=='warning' else 'FAIL'),'summary':summary}
        if detail is not None: rec['detail']=detail
        self.results.append(rec)
    def run(self):
        self.core(); self.a1(); self.a2(); self.a3(); self.a4_checks(); self.a5(); self.a6_checks(); self.a7(); self.a8(); self.a9(); self.a10a11(); self.a12(); self.docs()
        return self.report()
    def core(self):
        files=['gearm-0.1.concept-registry.yaml','gearm-0.1.relationship-registry.yaml','gearm-0.1.invariant-catalogue.yaml','gearm-0.1.ttl','gearm-0.1.context.jsonld','gearm-0.1.shacl.ttl','gearm-0.1.schema.json','gearm-0.1.profile.schema.json']
        self.add('A13-001','package',all((self.root/f).exists() for f in files),'A1-A8 core artifact files are present')
        versions={self.concepts['registry']['specificationVersion'],self.rels['registry']['specificationVersion'],self.invs['registry']['specificationVersion'],self.schema['x-gearm-specificationVersion'],self.profile['properties']['gearmVersion'].get('const')}
        self.add('A13-002','package',versions=={'0.1'},'Core artifacts agree on GEARM specification version',sorted(map(str,versions)))
    def a1(self):
        cs=self.concepts['concepts']; st=self.concepts['statistics']
        ids=[x['id'] for x in cs]; iris=[x['iri'] for x in cs]; qs=[x['qname'] for x in cs]; locals=[x['localName'] for x in cs]
        self.add('A13-010','A1',len(cs)==176==st['conceptCount'],'A1 concept count matches declared statistics',len(cs))
        self.add('A13-011','A1',all(len(set(v))==len(v) for v in [ids,iris,qs,locals]),'A1 concept IDs, IRIs, QNames and local names are unique')
        self.add('A13-012','A1',all(x['iri']=='https://specs.kivanura.org/gearm#'+x['localName'] and x['qname']=='gearm:'+x['localName'] for x in cs),'A1 concept QName/IRI/local-name identities are internally coherent')
        fams={x['key'] for x in self.concepts['conceptFamilies']}
        domains={x['id'] for x in self.concepts['referenceModelDomains']}
        bad=[x['id'] for x in cs if x['family'] not in fams or (x.get('primaryReferenceDomain') is not None and x.get('primaryReferenceDomain') not in domains)]
        self.add('A13-013','A1',not bad,'A1 concept family and RM-domain references resolve',bad)
        ex=self.concepts['externalConceptReferences']; br=self.concepts['boundaryReferences']
        ok=len(ex)==self.concepts['statistics']['externalConceptReferenceCount'] and len(br)==self.concepts['statistics']['boundaryReferenceCount']
        self.add('A13-014','A1',ok,'A1 external and boundary reference counts reconcile')
    def a2(self):
        rs=self.rels['relationships']; st=self.rels['statistics']
        self.add('A13-020','A2',len(rs)==74==st['relationshipCount'],'A2 relationship count matches declared statistics',len(rs))
        self.add('A13-021','A2',len({x['id'] for x in rs})==len(rs) and len({x['iri'] for x in rs})==len(rs) and len({x['qname'] for x in rs})==len(rs),'A2 relationship IDs, IRIs and QNames are unique')
        self.add('A13-022','A2',all(x['iri']=='https://specs.kivanura.org/gearm#'+x['localName'] and x['qname']=='gearm:'+x['localName'] for x in rs),'A2 relationship QName/IRI/local-name identities are internally coherent')
        external_refs=self.concepts['externalConceptReferences']; boundary_refs=self.concepts['boundaryReferences']
        cf=set(self.c_by_id)|set(self.c_by_q)|set(self.c_by_iri)|{x['id'] for x in external_refs}|{x.get('targetQName') for x in external_refs if x.get('targetQName')}|{x['id'] for x in boundary_refs}|{x.get('qname') for x in boundary_refs if x.get('qname')}
        bad=[]
        for r in rs:
            for k in ('sourceConcepts','targetConcepts'):
                for ref in r.get(k,[]) or []:
                    if ref not in cf: bad.append((r['id'],k,ref))
            sc=r.get('semanticCounterpart')
            if sc and sc not in self.r_by_id and sc not in self.r_by_q and sc not in self.r_by_iri: bad.append((r['id'],'semanticCounterpart',sc))
        self.add('A13-023','A2',not bad,'A2 concept and semantic-counterpart references resolve against A1/A2',bad[:20])
        fams={x['key'] for x in self.rels['relationshipFamilies']}; badf=[(r['id'],f) for r in rs for f in r.get('families',[]) if f not in fams]
        self.add('A13-024','A2',not badf,'A2 relationship-family references resolve',badf)
    def a3(self):
        iv=self.invs['invariants']; st=self.invs['statistics']
        self.add('A13-030','A3',len(iv)==180==st['invariantCount']==st['uniqueInvariantIdCount'],'A3 invariant count and uniqueness statistics reconcile',len(iv))
        ids=[x['id'] for x in iv]; legacy=[x['legacyId'] for x in iv]
        self.add('A13-031','A3',len(set(ids))==180 and len(set(legacy))==180 and all(i['legacyId'].replace('M9-INV-','GEARM-INV-')==i['id'] for i in iv),'A3 canonical and legacy invariant IDs map one-to-one')
        external_refs=self.concepts['externalConceptReferences']; boundary_refs=self.concepts['boundaryReferences']
        cref=set(self.c_by_id)|set(self.c_by_q)|set(self.c_by_iri)|{x['id'] for x in external_refs}|{x.get('targetQName') for x in external_refs if x.get('targetQName')}|{x['id'] for x in boundary_refs}|{x.get('qname') for x in boundary_refs if x.get('qname')}; rref=set(self.r_by_id)|set(self.r_by_q)|set(self.r_by_iri)
        badc=[]; badr=[]
        for i in iv:
            for x in i.get('involvedConcepts',[]) or []:
                ref = x.get('id') or x.get('qname') or x.get('iri') if isinstance(x,dict) else x
                if ref not in cref: badc.append((i['id'],ref))
            for x in i.get('involvedRelationships',[]) or []:
                ref = x.get('id') or x.get('qname') or x.get('iri') if isinstance(x,dict) else x
                if ref not in rref: badr.append((i['id'],ref))
        self.add('A13-032','A3',not badc and not badr,'A3 concept and relationship links resolve against A1/A2',{'concept':badc[:10],'relationship':badr[:10]})
        cls=Counter(x['invariantClass'] for x in iv); val=Counter(x['validationClass'] for x in iv); req=Counter(x['requirementLevel'] for x in iv)
        ok=cls==Counter(st['classCounts']) and val==Counter(st['validationClassCounts']) and req==Counter(st['requirementLevelCounts'])
        self.add('A13-033','A3',ok,'A3 class, validation-class and requirement-level statistics reconcile',{'classes':dict(cls),'validation':dict(val),'requirements':dict(req)})
        # Source specification invariant ID parity; statement exactness is separately checked where safe.
        text=self.spec.read_text(encoding='utf-8')
        specids=re.findall(r'^\|\s*(GEARM-INV-\d{3})\s*\|',text,re.M)
        self.add('A13-034','A3',len(specids)==180 and set(specids)==set(ids),'Normative specification exposes exactly the A3 invariant ID set',{'spec':len(specids),'catalogue':len(ids)})
        rows=dict(re.findall(r'^\|\s*(GEARM-INV-\d{3})\s*\|\s*(.*?)\s*\|\s*$',text,re.M))
        mism=[(i['id'],rows.get(i['id']),i['statement']) for i in iv if rows.get(i['id'])!=i['statement']]
        self.add('A13-036','A3/specification',not mism and len(rows)==180,'A3 invariant statements exactly match the normative M9 table',mism[:10])
        # A3 status should no longer claim constraints are unimplemented after A6.
        stale=[x['id'] for x in iv if x.get('machineConstraintStatus')=='not-yet-implemented']
        self.add('A13-035','A3/A6',not stale,'A3 machine-constraint status is reconciled with established A6 coverage',{'staleCount':len(stale)},severity='error')
    def a4_checks(self):
        cs=self.concepts['concepts']; rs=self.rels['relationships']; iv=self.invs['invariants']
        missing=[x['id'] for x in cs if (URIRef(x['iri']),RDF.type,OWL.Class) not in self.a4]
        self.add('A13-040','A4',not missing,'A4 represents every A1 GEARM-owned concept as owl:Class',missing[:20])
        missing=[x['id'] for x in rs if (URIRef(x['iri']),RDF.type,RDF.Property) not in self.a4 and (URIRef(x['iri']),RDF.type,OWL.SymmetricProperty) not in self.a4]
        self.add('A13-041','A4',not missing,'A4 represents every A2 relationship as an RDF property',missing[:20])
        missing=[x['id'] for x in iv if (URIRef(str(GEARMI)+x['id']),RDF.type,META.SemanticInvariant) not in self.a4]
        self.add('A13-042','A4',not missing,'A4 represents every A3 invariant as a first-class RDF resource',missing[:20])
        missing=[]
        for c in cs:
            for parent in c.get('specializes',[]) or []:
                p=self.c_by_id.get(parent) or self.c_by_q.get(parent) or self.c_by_iri.get(parent)
                if p and (URIRef(c['iri']),RDFS.subClassOf,URIRef(p['iri'])) not in self.a4: missing.append((c['id'],p['id']))
        self.add('A13-043','A4',not missing,'A4 preserves every explicit A1 specialization as rdfs:subClassOf',missing)
        prohibited=[OWL.disjointWith,RDFS.domain,RDFS.range,OWL.inverseOf,OWL.equivalentClass]
        findings=[]
        for p in prohibited:
            for s,o in self.a4.subject_objects(p): findings.append((str(s),str(p),str(o)))
        for typ in [OWL.TransitiveProperty,OWL.FunctionalProperty,OWL.InverseFunctionalProperty]:
            for s in self.a4.subjects(RDF.type,typ): findings.append((str(s),'rdf:type',str(typ)))
        self.add('A13-044','A4',not findings,'A4 conservative-formalization guardrails remain intact',findings[:20])
        # A4 invariant statements/classes/levels must mirror A3.
        mism=[]
        for i in iv:
            n=URIRef(str(GEARMI)+i['id'])
            pairs=[(META.statement,i['statement']),(META.invariantClass,i['invariantClass']),(META.requirementLevel,i['requirementLevel']),(META.validationClass,i['validationClass']),(META.machineConstraintStatus,i['machineConstraintStatus'])]
            for p,exp in pairs:
                got=self.a4.value(n,p)
                if got is None or str(got)!=str(exp): mism.append((i['id'],str(p),str(exp),None if got is None else str(got)))
        self.add('A13-045','A3/A4',not mism,'A4 invariant metadata mirrors A3 catalogue metadata',mism[:20])
    def a5(self):
        c=self.ctx['@context']; bad=[]
        if '@vocab' in c: bad.append('@vocab')
        for x in self.concepts['concepts']:
            if c.get(x['localName'])!=x['iri']: bad.append(x['id'])
        self.add('A13-050','A5',not bad,'A5 has no default @vocab and maps every A1 concept to its canonical IRI',bad[:20])
        bad=[]
        for x in self.rels['relationships']:
            v=c.get(x['localName'])
            if not isinstance(v,dict) or v.get('@id')!=x['iri'] or v.get('@type')!='@id': bad.append(x['id'])
        self.add('A13-051','A5',not bad,'A5 maps every A2 relationship as an IRI-valued JSON-LD term',bad[:20])
        ns={'gearm':'https://specs.kivanura.org/gearm#','gearmmeta':'https://specs.kivanura.org/gearm/meta#','gearmi':'https://specs.kivanura.org/gearm/invariant/'}
        bad=[k for k,v in ns.items() if not isinstance(c.get(k),dict) or c[k].get('@id')!=v or c[k].get('@prefix') is not True]
        self.add('A13-052','A4/A5',not bad,'A5 namespace prefixes align with A4 namespaces',bad)
    def a6_checks(self):
        cover=list(self.a6.subjects(RDF.type,GEARMSH.InvariantCoverage)); shapes=list(self.a6.subjects(RDF.type,SH.NodeShape))
        self.add('A13-060','A6',len(cover)==180 and len(shapes)==85,'A6 coverage and executable-shape counts match established baseline',{'coverage':len(cover),'shapes':len(shapes)})
        invset=set(self.i_by_id); bad=[]; seen=[]
        for s in cover:
            inv=self.a6.value(s,GEARMSH.implementsInvariant)
            iid=str(inv).rstrip('/').split('/')[-1] if inv else None
            if iid not in invset: bad.append(iid)
            seen.append(iid)
        self.add('A13-061','A3/A6',not bad and set(seen)==invset,'A6 has exactly one coverage record for every A3 invariant',{'bad':bad[:10],'unique':len(set(seen))})
        status=Counter(str(self.a6.value(s,GEARMSH.coverageStatus)).split('#')[-1] for s in cover)
        self.add('A13-062','A6',status==Counter({'PartialExecutable':85,'HybridAssessmentRequired':35,'AssessmentOnly':60}),'A6 coverage-status counts reconcile',dict(status))
        bad=[]
        for sh in shapes:
            inv=self.a6.value(sh,GEARMSH.implementsInvariant); iid=str(inv).split('/')[-1] if inv else None
            if iid not in invset or self.i_by_id[iid]['validationClass']!='hybrid': bad.append(iid)
        self.add('A13-063','A3/A6',not bad,'Every executable A6 shape maps to an A3 Hybrid invariant',bad)
        # Reconcile A3 current machineConstraintStatus with A6 coverage.
        expected={}
        for s in cover:
            iid=str(self.a6.value(s,GEARMSH.implementsInvariant)).split('/')[-1]
            st=str(self.a6.value(s,GEARMSH.coverageStatus)).split('#')[-1]
            expected[iid]={'PartialExecutable':'partial-executable','HybridAssessmentRequired':'assessment-required','AssessmentOnly':'assessment-only'}[st]
        mismatch=[(iid,self.i_by_id[iid].get('machineConstraintStatus'),expected[iid]) for iid in invset if self.i_by_id[iid].get('machineConstraintStatus')!=expected[iid]]
        self.add('A13-064','A3/A6',not mismatch,'A3 machineConstraintStatus agrees with A6 coverage classification',{'mismatchCount':len(mismatch),'sample':mismatch[:10]})
    def a7(self):
        s=self.schema; cs=self.concepts['concepts']; rs=self.rels['relationships']; iv=self.invs['invariants']
        Draft202012Validator.check_schema(s)
        self.add('A13-070','A7',True,'A7 is valid Draft 2020-12 JSON Schema')
        tr=s['$defs']['TypeReference']['oneOf']; enums=set(v for arm in tr for v in arm.get('enum',[]))
        missing=[x['id'] for x in cs if not {x['localName'],x['qname'],x['iri']}.issubset(enums)]
        self.add('A13-071','A1/A7',not missing,'A7 TypeReference covers every A1 concept in local/QName/IRI forms',missing[:20])
        cr=s['$defs']['ConceptReference']['oneOf']; crvals=set(v for arm in cr for v in arm.get('enum',[]))|enums
        missing=[x['id'] for x in cs if x['id'] not in crvals]
        self.add('A13-072','A1/A7',not missing,'A7 ConceptReference covers every A1 registry ID',missing[:20])
        rvals=set(s['$defs']['RelationshipReference']['enum']); missing=[]
        for x in rs:
            for v in [x['id'],x['localName'],x['qname'],x['iri']]:
                if v not in rvals: missing.append((x['id'],v))
        self.add('A13-073','A2/A7',not missing,'A7 RelationshipReference covers every A2 relationship in four canonical forms',missing[:20])
        ivals=set(s['$defs']['InvariantReference']['enum']); self.add('A13-074','A3/A7',ivals==set(self.i_by_id),'A7 InvariantReference exactly matches A3 invariant identifiers',{'schema':len(ivals),'A3':len(self.i_by_id)})
    def a8(self):
        p=self.profile; Draft202012Validator.check_schema(p); self.add('A13-080','A8',True,'A8 is valid Draft 2020-12 JSON Schema')
        cs=self.concepts['concepts']; rs=self.rels['relationships']
        cvals=set(p['$defs']['CanonicalConceptReference']['enum']); missing=[]
        for x in cs:
            for v in [x['id'],x['localName'],x['qname'],x['iri']]:
                if v not in cvals: missing.append((x['id'],v))
        self.add('A13-081','A1/A8',not missing and len(cvals)==704,'A8 canonical concept references reconcile with A1',{'count':len(cvals),'missing':missing[:10]})
        rvals=set(p['$defs']['CanonicalRelationshipReference']['enum']); missing=[]
        for x in rs:
            for v in [x['id'],x['localName'],x['qname'],x['iri']]:
                if v not in rvals: missing.append((x['id'],v))
        self.add('A13-082','A2/A8',not missing and len(rvals)==296,'A8 canonical relationship references reconcile with A2',{'count':len(rvals),'missing':missing[:10]})
        ivals=set(p['$defs']['InvariantReference']['enum']); tight=set(p['$defs']['TightenableInvariantReference']['enum']); expected={x['id'] for x in self.invs['invariants'] if x['requirementLevel']!='SHALL'}
        self.add('A13-083','A3/A8',ivals==set(self.i_by_id) and tight==expected,'A8 invariant and tightenable-invariant sets derive correctly from A3',{'all':len(ivals),'tightenable':sorted(tight)})
        badns='https://specs.kivanura.org/gearm#' in json.dumps(p.get('$defs',{})) and False
        # Concrete protection check via schema instance.
        v=Draft202012Validator(p,format_checker=FormatChecker())
        candidate={'profileId':'urn:test:p','profileVersion':'1','profiles':{'specification':'GEARM','version':'0.1'},'authority':{'name':'x'},'scope':{'description':'x'},'extensionNamespaces':['https://specs.kivanura.org/gearm#']}
        self.add('A13-084','A8',not v.is_valid(candidate),'A8 rejects reuse of the canonical GEARM namespace for Profile extensions')
    def a9(self):
        exdir=self.root/'examples'; files=sorted(p for p in exdir.iterdir() if p.suffix in {'.json','.jsonld'})
        a7v=Draft202012Validator(self.schema,format_checker=FormatChecker()); a8v=Draft202012Validator(self.profile,format_checker=FormatChecker())
        bad=[]
        for p in files:
            doc=json.loads(p.read_text())
            v=a8v if 'profile' in p.name else a7v
            if not v.is_valid(doc): bad.append(p.name)
        self.add('A13-090','A9',len(files)==8 and not bad,'All eight A9 canonical examples validate against their applicable A7/A8 schema',{'files':len(files),'bad':bad})
    def a10a11(self):
        tcs=self.manifest['testcases']; ids=[x['id'] for x in tcs]
        self.add('A13-100','A10/A11',len(tcs)==46 and len(set(ids))==46,'A10/A11 manifest contains 46 unique testcase identities',len(tcs))
        invset=set(self.i_by_id); bad=[]; missing=[]
        for tc in tcs:
            for iid in tc.get('targetedInvariants',[]):
                if iid not in invset: bad.append((tc['id'],iid))
            rel=tc.get('file') or tc.get('artifact')
            if rel and not (self.root/'testcases'/rel).resolve().exists(): missing.append((tc['id'],rel))
        self.add('A13-101','A3/A10/A11',not bad,'All testcase invariant references resolve to A3',bad)
        self.add('A13-102','A10/A11',not missing,'All testcase files/artifacts referenced by manifest exist',missing)
        counts={'positive':sum(int(i.split('-')[-1])<100 for i in ids)}
        band=Counter('positive' if int(x['id'][-3:])<100 else 'structural-invalid' if int(x['id'][-3:])<200 else 'semantic-invalid' if int(x['id'][-3:])<300 else 'profile-invalid' for x in tcs)
        self.add('A13-103','A10/A11',band==Counter({'positive':14,'structural-invalid':10,'semantic-invalid':10,'profile-invalid':12}),'A10/A11 testcase band counts reconcile',dict(band))
    def a12(self):
        runner=self.root/'testcases/run_conformance.py'
        self.add('A13-110','A12',runner.exists(),'A12 runner is present')
        with tempfile.TemporaryDirectory() as d:
            rp=Path(d)/'report.json'
            proc=subprocess.run([sys.executable,str(runner),'--suite','--json-report',str(rp)],cwd=self.root,capture_output=True,text=True)
            if proc.returncode!=0 or not rp.exists():
                self.add('A13-111','A12',False,'A12 full suite executes successfully',{'exit':proc.returncode,'stderr':proc.stderr[-1000:],'stdout':proc.stdout[-1000:]})
            else:
                report=json.loads(rp.read_text())
                summary=report.get('summary',{})
                ok=(summary.get('testcases')==46 and summary.get('expectationsMatched')==46 and summary.get('expectationMismatches')==0 and summary.get('machineRegression')=='PASS')
                self.add('A13-111','A12',ok,'A12 full suite executes with no expectation mismatch',{'exit':proc.returncode,'summary':summary})
    def docs(self):
        text=self.spec.read_text(encoding='utf-8')
        names=['gearm-0.1.concept-registry.yaml','gearm-0.1.relationship-registry.yaml','gearm-0.1.invariant-catalogue.yaml','gearm-0.1.ttl','gearm-0.1.context.jsonld','gearm-0.1.shacl.ttl','gearm-0.1.schema.json','gearm-0.1.profile.schema.json','testcases/manifest.json','testcases/run_conformance.py','reconciliation/run_reconciliation.py','reconciliation/README.md']
        missing=[n for n in names if n not in text]
        self.add('A13-120','documentation',not missing,'Normative GEARM specification references all established A1-A13 primary artifacts',missing)
    def report(self):
        fails=sum(r['status']=='FAIL' for r in self.results); warns=sum(r['status']=='WARN' for r in self.results); passes=sum(r['status']=='PASS' for r in self.results)
        return {'specification':{'id':'GEARM','version':'0.1'},'artifactStage':'GEARM-A13','reconciliationVersion':'0.1.0-draft','purpose':'Cross-artifact reconciliation across GEARM-A1 through GEARM-A12.','normativeBoundary':'A13 checks consistency among machine artifacts and their normative source. A reconciliation PASS does not replace semantic assessment or establish complete GEARM conformance.','summary':{'checkCount':len(self.results),'passCount':passes,'warningCount':warns,'failCount':fails,'status':'PASS' if fails==0 else 'FAIL'},'checks':self.results}

def main():
    ap=argparse.ArgumentParser(); ap.add_argument('--json-report'); ap.add_argument('--quiet',action='store_true'); args=ap.parse_args()
    root=Path(__file__).resolve().parents[1]
    try: rep=Reconciler(root).run()
    except Exception as e:
        print(f'GEARM-A13 runtime error: {e}',file=sys.stderr); return EXIT_RUNTIME
    if args.json_report:
        Path(args.json_report).write_text(json.dumps(rep,indent=2,sort_keys=False)+'\n',encoding='utf-8')
    if not args.quiet:
        for c in rep['checks']: print(f"[{c['status']}] {c['id']} {c['summary']}")
        s=rep['summary']; print(f"\nGEARM-A13 reconciliation: {s['status']} — {s['passCount']} pass, {s['warningCount']} warn, {s['failCount']} fail / {s['checkCount']} checks")
    return EXIT_OK if rep['summary']['failCount']==0 else EXIT_DEFECT
if __name__=='__main__': raise SystemExit(main())
