#!/usr/bin/env python3
"""GEARA 0.1 A13 cross-artifact reconciliation runner.

This runner reconciles the established A1-A12 machine stack against itself and
against the normative human-readable GEARA 0.1 specification. It does not
perform Architecture Assessment and does not establish complete GEARA
conformance.
"""
from __future__ import annotations

import argparse
import hashlib
import json
import re
import subprocess
import sys
import tempfile
from collections import Counter
from pathlib import Path
from typing import Any, Iterable

import yaml
from jsonschema import Draft202012Validator
from rdflib import Graph, Literal, Namespace, RDF, RDFS, OWL, URIRef

GEARA_NS = "https://specs.kivanura.org/geara#"
INV_NS = "https://specs.kivanura.org/geara/invariant/"
META_NS = "https://specs.kivanura.org/geara/meta#"
SH_NS = "http://www.w3.org/ns/shacl#"
SHX_NS = "https://specs.kivanura.org/geara/shacl#"

GEARA = Namespace(GEARA_NS)
GEARAI = Namespace(INV_NS)
META = Namespace(META_NS)
SH = Namespace(SH_NS)
SHX = Namespace(SHX_NS)
SKOS = Namespace("http://www.w3.org/2004/02/skos/core#")
DCTERMS = Namespace("http://purl.org/dc/terms/")

RUNNER_ID = "GEARA-0.1-A13-RECONCILIATION-RUNNER"
RUNNER_VERSION = "0.1.0-draft"


def load_json(path: Path) -> Any:
    return json.loads(path.read_text(encoding="utf-8"))


def load_yaml(path: Path) -> Any:
    return yaml.safe_load(path.read_text(encoding="utf-8"))


def sha256(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as f:
        for chunk in iter(lambda: f.read(1024 * 1024), b""):
            h.update(chunk)
    return h.hexdigest()


def stable_json_bytes(data: Any) -> bytes:
    return (json.dumps(data, indent=2, sort_keys=False, ensure_ascii=False) + "\n").encode("utf-8")


def list_duplicates(values: Iterable[Any]) -> list[Any]:
    c = Counter(values)
    return sorted(k for k, v in c.items() if v > 1)


def set_eq(a: Iterable[Any], b: Iterable[Any]) -> bool:
    return set(a) == set(b)


def normalize_bool_literal(v: Any) -> bool | None:
    if isinstance(v, bool):
        return v
    if isinstance(v, Literal):
        try:
            return bool(v.toPython())
        except Exception:
            return None
    return None


class Reconciler:
    def __init__(self, repo_root: Path):
        self.repo_root = repo_root.resolve()
        self.spec_dir = self.repo_root / "static/specs/geara/0.1"
        self.doc_path = self.repo_root / "docs/tools/geara/0.1/geara-0.1-specification.md"
        self.checks: list[dict[str, Any]] = []

        self.a1 = load_yaml(self.spec_dir / "geara-0.1.architecture-registry.yaml")
        self.a2 = load_yaml(self.spec_dir / "geara-0.1.relationship-registry.yaml")
        self.a3 = load_yaml(self.spec_dir / "geara-0.1.invariant-catalogue.yaml")
        self.a5 = load_json(self.spec_dir / "geara-0.1.context.jsonld")
        self.a7 = load_json(self.spec_dir / "geara-0.1.schema.json")
        self.a8 = load_json(self.spec_dir / "geara-0.1.profile.schema.json")
        self.a9 = load_json(self.spec_dir / "examples/geara-0.1.examples.json")
        self.manifest = load_json(self.spec_dir / "testcases/manifest.json")
        self.spec_text = self.doc_path.read_text(encoding="utf-8")

        self.a4 = Graph().parse(self.spec_dir / "geara-0.1.ttl", format="turtle")
        self.a6 = Graph().parse(self.spec_dir / "geara-0.1.shacl.ttl", format="turtle")

        self.a1_elements = self.a1["architectureElements"]
        self.a2_relationships = self.a2["relationships"]
        self.a3_invariants = self.a3["invariants"]
        self.a1_by_id = {e["id"]: e for e in self.a1_elements}
        self.a2_by_id = {r["id"]: r for r in self.a2_relationships}
        self.a3_by_id = {i["id"]: i for i in self.a3_invariants}

    def check(self, cid: str, group: str, title: str, passed: bool, details: Any = None) -> None:
        self.checks.append({
            "id": cid,
            "group": group,
            "title": title,
            "status": "PASS" if passed else "FAIL",
            "details": details,
        })

    def run(self) -> dict[str, Any]:
        self.check_a1_a2()
        self.check_a3()
        self.check_a4()
        self.check_a5()
        self.check_a6()
        self.check_a7_a8()
        self.check_a9_a11()
        self.check_a12()
        self.check_spec_and_inventory()

        failed = [c for c in self.checks if c["status"] != "PASS"]
        groups: dict[str, dict[str, int]] = {}
        for c in self.checks:
            g = groups.setdefault(c["group"], {"checks": 0, "passed": 0, "failed": 0})
            g["checks"] += 1
            if c["status"] == "PASS":
                g["passed"] += 1
            else:
                g["failed"] += 1

        canonical_paths = [
            "static/specs/geara/0.1/geara-0.1.architecture-registry.yaml",
            "static/specs/geara/0.1/geara-0.1.relationship-registry.yaml",
            "static/specs/geara/0.1/geara-0.1.invariant-catalogue.yaml",
            "static/specs/geara/0.1/geara-0.1.ttl",
            "static/specs/geara/0.1/geara-0.1.context.jsonld",
            "static/specs/geara/0.1/geara-0.1.shacl.ttl",
            "static/specs/geara/0.1/geara-0.1.schema.json",
            "static/specs/geara/0.1/geara-0.1.profile.schema.json",
            "static/specs/geara/0.1/examples/geara-0.1.examples.json",
            "static/specs/geara/0.1/testcases/manifest.json",
            "static/specs/geara/0.1/conformance/run-geara-conformance.py",
        ]
        inventory = []
        for rel in canonical_paths:
            p = self.repo_root / rel
            inventory.append({"path": rel, "sha256": sha256(p), "sizeBytes": p.stat().st_size})

        return {
            "reconciliation": {
                "id": RUNNER_ID,
                "version": RUNNER_VERSION,
                "artifactStage": "GEARA-A13",
                "specification": {"id": "GEARA", "version": "0.1"},
                "scope": "Cross-artifact reconciliation of the established GEARA A1-A12 machine stack and its normative specification integration.",
                "boundary": "A13 PASS demonstrates internal machine-artifact reconciliation. It does not establish complete GEARA architectural conformance and does not replace Architecture Assessment or Hybrid judgement.",
                "deterministic": True,
            },
            "summary": {
                "checks": len(self.checks),
                "passed": len(self.checks) - len(failed),
                "failed": len(failed),
                "allChecksPassed": not failed,
                "groups": groups,
                "architectureElementCount": len(self.a1_elements),
                "relationshipCount": len(self.a2_relationships),
                "invariantCount": len(self.a3_invariants),
                "rdfTripleCount": len(self.a4),
                "shaclTripleCount": len(self.a6),
                "a6NodeShapeCount": len(set(self.a6.subjects(RDF.type, SH.NodeShape))),
                "exampleCount": self.a9.get("exampleCount"),
                "testcaseCount": self.manifest.get("testcaseCount"),
            },
            "artifactInventory": inventory,
            "checks": self.checks,
        }

    def check_a1_a2(self) -> None:
        g = "A1-A2 identity and relationship reconciliation"
        elems = self.a1_elements
        rels = self.a2_relationships
        element_types = {x["key"] for x in self.a1["elementTypes"]}
        families = {x["key"] for x in self.a1["families"]}
        rel_families = {x["key"] for x in self.a2["relationshipFamilies"]}

        self.check("A13-001", g, "A1 declares 259 architecture elements", len(elems) == 259,
                   {"actual": len(elems), "expected": 259})
        self.check("A13-002", g, "A1 element IDs are unique", not list_duplicates(e["id"] for e in elems),
                   {"duplicates": list_duplicates(e["id"] for e in elems)})
        self.check("A13-003", g, "A1 local names, QNames and IRIs are unique",
                   not list_duplicates(e["localName"] for e in elems)
                   and not list_duplicates(e["qname"] for e in elems)
                   and not list_duplicates(e["iri"] for e in elems),
                   {"duplicateLocalNames": list_duplicates(e["localName"] for e in elems),
                    "duplicateQNames": list_duplicates(e["qname"] for e in elems),
                    "duplicateIris": list_duplicates(e["iri"] for e in elems)})
        bad_ns = [e["id"] for e in elems if e["qname"] != f"geara:{e['localName']}" or e["iri"] != GEARA_NS + e["localName"]]
        self.check("A13-004", g, "A1 QName and IRI identities align with the canonical namespace", not bad_ns,
                   {"mismatches": bad_ns})
        bad_type = [e["id"] for e in elems if e["elementType"] not in element_types]
        bad_fam = [e["id"] for e in elems if e["family"] not in families]
        self.check("A13-005", g, "A1 element-type references resolve", not bad_type, {"unresolved": bad_type})
        self.check("A13-006", g, "A1 family references resolve", not bad_fam, {"unresolved": bad_fam})
        stats_ok = (
            self.a1["statistics"]["architectureElementCount"] == len(elems)
            and self.a1["statistics"]["architectureElementTypeCount"] == len(self.a1["elementTypes"])
            and self.a1["statistics"]["familyCount"] == len(self.a1["families"])
            and self.a1["statistics"]["controlledVocabularyCount"] == len(self.a1["controlledVocabularies"])
            and self.a1["statistics"]["externalSemanticReferenceCount"] == len(self.a1["externalSemanticReferences"])
            and self.a1["statistics"]["externalArchitectureReferenceCount"] == len(self.a1["externalArchitectureReferences"])
        )
        self.check("A13-007", g, "A1 declared statistics reconcile with actual registry content", stats_ok)
        ext_refs = self.a1["externalSemanticReferences"] + self.a1["externalArchitectureReferences"]
        ext_bad = [x["id"] for x in ext_refs if str(x.get("iri", "")).startswith(GEARA_NS)]
        self.check("A13-008", g, "A1 external references remain reference-only and are not minted in the GEARA namespace",
                   not ext_bad and all(x.get("referenceOnly") is True for x in ext_refs),
                   {"namespaceViolations": ext_bad})

        self.check("A13-009", g, "A2 declares 84 canonical relationships", len(rels) == 84,
                   {"actual": len(rels), "expected": 84})
        self.check("A13-010", g, "A2 relationship IDs, local names, QNames and IRIs are unique",
                   not list_duplicates(r["id"] for r in rels)
                   and not list_duplicates(r["localName"] for r in rels)
                   and not list_duplicates(r["qname"] for r in rels)
                   and not list_duplicates(r["iri"] for r in rels))
        bad_rel_ns = [r["id"] for r in rels if r["qname"] != f"geara:{r['localName']}" or r["iri"] != GEARA_NS + r["localName"]]
        self.check("A13-011", g, "A2 QName and IRI identities align with the canonical namespace", not bad_rel_ns,
                   {"mismatches": bad_rel_ns})
        bad_rel_fam = [r["id"] for r in rels if any(f not in rel_families for f in r["families"])]
        self.check("A13-012", g, "A2 relationship-family references resolve", not bad_rel_fam,
                   {"unresolved": bad_rel_fam})
        element_names = {e["localName"] for e in elems}
        collision = sorted(element_names & {r["localName"] for r in rels})
        self.check("A13-013", g, "A2 relationship local names do not collide with A1 architecture element local names", not collision,
                   {"collisions": collision})
        inheritance = sorted(r["localName"] for r in rels if r.get("ontologyInheritance") is True)
        symmetric = sorted(r["localName"] for r in rels if r.get("symmetric") is True)
        self.check("A13-014", g, "A2 preserves specializes as the only ontology-inheritance relationship", inheritance == ["specializes"],
                   {"ontologyInheritance": inheritance})
        self.check("A13-015", g, "A2 preserves equivalentTo as the only explicitly symmetric relationship", symmetric == ["equivalentTo"],
                   {"symmetric": symmetric})
        stronger = [r["id"] for r in rels if r.get("transitive") or r.get("functional") or r.get("inverseFunctional")]
        self.check("A13-016", g, "A2 does not assert transitive, functional or inverse-functional strengthening", not stronger,
                   {"violations": stronger})
        transfer = [r["id"] for r in rels if r.get("automaticOwnershipTransfer") or r.get("automaticAuthorityTransfer")]
        self.check("A13-017", g, "A2 relationships do not automatically transfer ownership or authority", not transfer,
                   {"violations": transfer})
        a2_stats_ok = (
            self.a2["statistics"]["relationshipCount"] == len(rels)
            and self.a2["statistics"]["relationshipFamilyCount"] == len(self.a2["relationshipFamilies"])
            and self.a2["statistics"]["ontologyInheritanceRelationshipCount"] == len(inheritance)
            and self.a2["statistics"]["symmetricRelationshipCount"] == len(symmetric)
        )
        self.check("A13-018", g, "A2 declared statistics reconcile with actual relationship content", a2_stats_ok)

    def check_a3(self) -> None:
        g = "A3 invariant reconciliation"
        invs = self.a3_invariants
        groups = {x["id"]: x for x in self.a3["invariantGroups"]}
        ext_ids = {x["id"] for x in self.a1["externalSemanticReferences"] + self.a1["externalArchitectureReferences"]}

        self.check("A13-019", g, "A3 declares 123 architecture invariants", len(invs) == 123,
                   {"actual": len(invs), "expected": 123})
        self.check("A13-020", g, "A3 invariant IDs are unique", not list_duplicates(i["id"] for i in invs))
        unresolved_groups = [i["id"] for i in invs if i["group"] not in groups]
        self.check("A13-021", g, "A3 invariant-group references resolve", not unresolved_groups,
                   {"unresolved": unresolved_groups})
        group_count_actual = Counter(i["group"] for i in invs)
        group_count_bad = {gid: {"declared": rec["invariantCount"], "actual": group_count_actual[gid]}
                           for gid, rec in groups.items() if rec["invariantCount"] != group_count_actual[gid]}
        self.check("A13-022", g, "A3 invariant-group declared counts reconcile", not group_count_bad,
                   {"mismatches": group_count_bad})

        bad_a1 = []
        bad_a2 = []
        bad_ext = []
        for i in invs:
            for ref in i.get("involvedArchitectureElements", []) or []:
                rid = ref.get("id") if isinstance(ref, dict) else ref
                if rid not in self.a1_by_id:
                    bad_a1.append({"invariant": i["id"], "reference": rid})
            for ref in i.get("involvedRelationships", []) or []:
                rid = ref.get("id") if isinstance(ref, dict) else ref
                if rid not in self.a2_by_id:
                    bad_a2.append({"invariant": i["id"], "reference": rid})
            for ref in i.get("involvedExternalReferences", []) or []:
                rid = ref.get("id") if isinstance(ref, dict) else ref
                if rid not in ext_ids:
                    bad_ext.append({"invariant": i["id"], "reference": rid})
        self.check("A13-023", g, "A3 architecture-element references resolve to A1", not bad_a1, {"unresolved": bad_a1})
        self.check("A13-024", g, "A3 relationship references resolve to A2", not bad_a2, {"unresolved": bad_a2})
        self.check("A13-025", g, "A3 external references resolve to A1 reference-only registries", not bad_ext, {"unresolved": bad_ext})

        spec_invariants = dict(re.findall(r"^- \*\*(GEARA-INV-\d{3})\*\* — (.+)$", self.spec_text, flags=re.MULTILINE))
        a3_statements = {i["id"]: i["statement"] for i in invs}
        missing_spec = sorted(set(a3_statements) - set(spec_invariants))
        missing_a3 = sorted(set(spec_invariants) - set(a3_statements))
        statement_mismatch = []
        for iid in sorted(set(a3_statements) & set(spec_invariants)):
            if a3_statements[iid].strip() != spec_invariants[iid].strip():
                statement_mismatch.append(iid)
        self.check("A13-026", g, "A3 invariant identity set matches the normative section-14 invariant set",
                   len(spec_invariants) == 123 and not missing_spec and not missing_a3,
                   {"specCount": len(spec_invariants), "missingFromSpec": missing_spec, "missingFromA3": missing_a3})
        self.check("A13-027", g, "A3 invariant statements are verbatim with the normative specification", not statement_mismatch,
                   {"mismatches": statement_mismatch})

        status_counts = Counter(i["machineConstraintStatus"] for i in invs)
        self.check("A13-028", g, "A3 reconciled machine-constraint status is 21 partial-executable and 102 assessment-required",
                   status_counts == Counter({"assessment-required": 102, "partial-executable": 21}),
                   {"actual": dict(sorted(status_counts.items()))})
        self.check("A13-029", g, "A3 declared statistics reconcile with invariant content",
                   self.a3["statistics"]["invariantCount"] == len(invs)
                   and self.a3["statistics"]["uniqueInvariantIdCount"] == len(set(i["id"] for i in invs))
                   and self.a3["statistics"]["invariantGroupCount"] == len(groups))

    def check_a4(self) -> None:
        g = "A4 RDF/OWL reconciliation"
        graph = self.a4
        a1_iris = {URIRef(e["iri"]) for e in self.a1_elements}
        a2_iris = {URIRef(r["iri"]) for r in self.a2_relationships}
        inv_iris = {URIRef(INV_NS + i["id"]) for i in self.a3_invariants}

        self.check("A13-030", g, "A4 graph parses and retains the established 7,344 triples", len(graph) == 7344,
                   {"actual": len(graph), "expected": 7344})
        missing_classes = sorted(str(x) for x in a1_iris if (x, RDF.type, OWL.Class) not in graph)
        self.check("A13-031", g, "Every A1 architecture element is represented as owl:Class in A4", not missing_classes,
                   {"missing": missing_classes})
        missing_props = sorted(str(x) for x in a2_iris if (x, RDF.type, RDF.Property) not in graph)
        self.check("A13-032", g, "Every A2 relationship is represented as rdf:Property in A4", not missing_props,
                   {"missing": missing_props})
        a4_invs = set(graph.subjects(RDF.type, META.ArchitectureInvariant))
        self.check("A13-033", g, "A4 invariant-resource identity set equals A3", a4_invs == inv_iris,
                   {"a4Count": len(a4_invs), "a3Count": len(inv_iris)})
        stmt_bad = []
        status_bad = []
        for i in self.a3_invariants:
            s = URIRef(INV_NS + i["id"])
            if Literal(i["statement"]) not in set(graph.objects(s, META.statement)):
                stmt_bad.append(i["id"])
            if Literal(i["machineConstraintStatus"]) not in set(graph.objects(s, META.machineConstraintStatus)):
                status_bad.append(i["id"])
        self.check("A13-034", g, "A4 preserves all A3 invariant statements verbatim", not stmt_bad, {"mismatches": stmt_bad})
        self.check("A13-035", g, "A4 machine-constraint status mirrors the reconciled A3 metadata", not status_bad, {"mismatches": status_bad})

        sym_props = sorted(str(s) for s in graph.subjects(RDF.type, OWL.SymmetricProperty) if str(s).startswith(GEARA_NS))
        self.check("A13-036", g, "A4 symmetry formalization matches A2 exactly", sym_props == [GEARA_NS + "equivalentTo"],
                   {"symmetricProperties": sym_props})
        spec_subprop = (URIRef(GEARA_NS + "specializes"), RDFS.subPropertyOf, RDFS.subClassOf) in graph
        other_subclass_rel = sorted(str(s) for s in a2_iris if s != URIRef(GEARA_NS + "specializes") and (s, RDFS.subPropertyOf, RDFS.subClassOf) in graph)
        self.check("A13-037", g, "A4 preserves specializes as the sole relationship mapped toward rdfs:subClassOf",
                   spec_subprop and not other_subclass_rel,
                   {"specializesMappingPresent": spec_subprop, "unexpected": other_subclass_rel})

        forbidden_predicates = [RDFS.domain, RDFS.range, OWL.disjointWith, OWL.inverseOf, OWL.equivalentClass, OWL.equivalentProperty]
        forbidden_found = []
        for pred in forbidden_predicates:
            for s, o in graph.subject_objects(pred):
                forbidden_found.append({"predicate": str(pred), "subject": str(s), "object": str(o)})
        stronger_types = []
        for t in [OWL.TransitiveProperty, OWL.FunctionalProperty, OWL.InverseFunctionalProperty]:
            for s in graph.subjects(RDF.type, t):
                if str(s).startswith(GEARA_NS):
                    stronger_types.append({"type": str(t), "subject": str(s)})
        self.check("A13-038", g, "A4 contains no prohibited stronger OWL/RDFS axioms", not forbidden_found and not stronger_types,
                   {"forbiddenPredicates": forbidden_found, "strongerPropertyTypes": stronger_types})

        ext_ids = [x["id"] for x in self.a1["externalSemanticReferences"] + self.a1["externalArchitectureReferences"]]
        ext_class = []
        for xid in ext_ids:
            ref_iri = URIRef(f"https://specs.kivanura.org/geara/reference/{xid}")
            if (ref_iri, RDF.type, OWL.Class) in graph:
                ext_class.append(xid)
        self.check("A13-039", g, "A4 does not re-own A1 external references as owl:Class", not ext_class,
                   {"violations": ext_class})

    def check_a5(self) -> None:
        g = "A5 JSON-LD context reconciliation"
        ctx = self.a5["@context"]
        self.check("A13-040", g, "A5 declares JSON-LD 1.1 and no default @vocab",
                   ctx.get("@version") == 1.1 and "@vocab" not in ctx,
                   {"version": ctx.get("@version"), "hasDefaultVocab": "@vocab" in ctx})
        missing_a1 = []
        for e in self.a1_elements:
            if ctx.get(e["localName"]) != e["iri"]:
                missing_a1.append(e["id"])
        self.check("A13-041", g, "A5 maps every A1 local name to its exact canonical IRI", not missing_a1,
                   {"mismatches": missing_a1})
        missing_a2 = []
        for r in self.a2_relationships:
            val = ctx.get(r["localName"])
            if not isinstance(val, dict) or val.get("@id") != r["iri"] or val.get("@type") != "@id":
                missing_a2.append(r["id"])
        self.check("A13-042", g, "A5 maps every A2 relationship and preserves IRI-valued coercion", not missing_a2,
                   {"mismatches": missing_a2})
        ext_labels = {x["label"] for x in self.a1["externalSemanticReferences"] + self.a1["externalArchitectureReferences"]}
        ext_aliases = sorted(x for x in ext_labels if x in ctx)
        self.check("A13-043", g, "A5 does not mint unqualified aliases for externally governed reference labels", not ext_aliases,
                   {"aliases": ext_aliases})
        prefix_expect = {
            "geara": GEARA_NS,
            "gearameta": META_NS,
            "gearai": INV_NS,
            "rdf": str(RDF),
            "rdfs": str(RDFS),
            "owl": str(OWL),
            "skos": str(SKOS),
        }
        prefix_bad = []
        for p, iri in prefix_expect.items():
            v = ctx.get(p)
            actual = v.get("@id") if isinstance(v, dict) else v
            if actual != iri:
                prefix_bad.append({"prefix": p, "actual": actual, "expected": iri})
        self.check("A13-044", g, "A5 namespace prefixes reconcile with A4 namespaces", not prefix_bad,
                   {"mismatches": prefix_bad})

    def check_a6(self) -> None:
        g = "A6 SHACL reconciliation"
        graph = self.a6
        coverage = set(graph.subjects(RDF.type, SHX.InvariantCoverage))
        shapes = set(graph.subjects(RDF.type, SH.NodeShape))
        self.check("A13-045", g, "A6 retains one coverage resource for each of the 123 A3 invariants", len(coverage) == 123,
                   {"coverageResources": len(coverage)})
        coverage_inv = set(graph.objects(None, SHX.implementsInvariant))
        expected_inv = {URIRef(INV_NS + i["id"]) for i in self.a3_invariants}
        self.check("A13-046", g, "A6 covered invariant identity set equals A3", coverage_inv == expected_inv,
                   {"covered": len(coverage_inv), "expected": len(expected_inv)})
        self.check("A13-047", g, "A6 retains 24 executable NodeShapes", len(shapes) == 24,
                   {"actual": len(shapes), "expected": 24})
        statuses = Counter()
        assessment_flags_ok = True
        for c in coverage:
            vals = list(graph.objects(c, SHX.coverageStatus))
            if len(vals) == 1:
                statuses[str(vals[0]).split("#")[-1]] += 1
            flags = list(graph.objects(c, SHX.requiresArchitectureAssessment))
            if not flags or not all(normalize_bool_literal(v) is True for v in flags):
                assessment_flags_ok = False
        self.check("A13-048", g, "A6 coverage classification reconciles to 21 PartialExecutable, 82 HybridAssessmentRequired and 20 AssessmentOnly",
                   statuses == Counter({"PartialExecutable": 21, "HybridAssessmentRequired": 82, "AssessmentOnly": 20}),
                   {"actual": dict(sorted(statuses.items()))})
        self.check("A13-049", g, "Every A6 coverage record preserves the Architecture-Assessment requirement", assessment_flags_ok)
        shape_inv = set()
        for shape in shapes:
            shape_inv.update(graph.objects(shape, SHX.implementsInvariant))
        unresolved_shape_inv = sorted(str(x) for x in shape_inv if x not in expected_inv)
        self.check("A13-050", g, "A6 Shape invariant references resolve to A3", not unresolved_shape_inv,
                   {"unresolved": unresolved_shape_inv})
        partial_ids = {i["id"] for i in self.a3_invariants if i["machineConstraintStatus"] == "partial-executable"}
        shape_ids = {str(x).split("/")[-1] for x in shape_inv if str(x).startswith(INV_NS)}
        self.check("A13-051", g, "A3 partial-executable invariant set equals the A6 executable-Shape invariant set", partial_ids == shape_ids,
                   {"a3Count": len(partial_ids), "a6Count": len(shape_ids), "missingInA6": sorted(partial_ids-shape_ids), "unexpectedInA6": sorted(shape_ids-partial_ids)})
        forbidden = []
        for pred in [OWL.disjointWith, OWL.inverseOf, OWL.equivalentClass, OWL.equivalentProperty, RDFS.domain, RDFS.range]:
            for s, o in graph.subject_objects(pred):
                forbidden.append({"predicate": str(pred), "subject": str(s), "object": str(o)})
        self.check("A13-052", g, "A6 does not introduce prohibited OWL/RDFS semantic strengthening", not forbidden,
                   {"violations": forbidden})

    def check_a7_a8(self) -> None:
        g = "A7-A8 schema reconciliation"
        schema_errors = []
        for label, schema in [("A7", self.a7), ("A8", self.a8)]:
            try:
                Draft202012Validator.check_schema(schema)
            except Exception as e:
                schema_errors.append({"schema": label, "error": str(e)})
        self.check("A13-053", g, "A7 and A8 are valid Draft 2020-12 schemas", not schema_errors,
                   {"errors": schema_errors})

        expected_arch_refs = []
        for e in self.a1_elements:
            expected_arch_refs.extend([e["id"], e["localName"], e["qname"], e["iri"]])
        actual_arch_refs = self.a7["$defs"]["ArchitectureElementReference"]["enum"]
        self.check("A13-054", g, "A7 ArchitectureElementReference exactly covers all four A1 identity forms", actual_arch_refs == expected_arch_refs,
                   {"actualCount": len(actual_arch_refs), "expectedCount": len(expected_arch_refs)})

        expected_rel_refs = []
        for r in self.a2_relationships:
            expected_rel_refs.extend([r["id"], r["localName"], r["qname"], r["iri"]])
        actual_rel_refs = self.a7["$defs"]["RelationshipReference"]["enum"]
        self.check("A13-055", g, "A7 RelationshipReference exactly covers all four A2 identity forms", actual_rel_refs == expected_rel_refs,
                   {"actualCount": len(actual_rel_refs), "expectedCount": len(expected_rel_refs)})

        expected_inv_refs = []
        for i in self.a3_invariants:
            expected_inv_refs.extend([i["id"], f"gearai:{i['id']}", INV_NS + i["id"]])
        actual_inv_refs = self.a7["$defs"]["InvariantReference"]["enum"]
        self.check("A13-056", g, "A7 InvariantReference exactly covers all three A3 identity forms", actual_inv_refs == expected_inv_refs,
                   {"actualCount": len(actual_inv_refs), "expectedCount": len(expected_inv_refs)})

        capability_elems = [e for e in self.a1_elements if e["elementType"] in {"ArchitecturalCapability", "FoundationCapability"}]
        cap_expected = []
        for e in capability_elems:
            cap_expected.extend([e["id"], e["localName"], e["qname"], e["iri"]])
            if e.get("sourceIdentifier"):
                cap_expected.append(e["sourceIdentifier"])
        cap_actual = self.a8["$defs"]["CapabilityReference"]["enum"]
        self.check("A13-057", g, "A8 CapabilityReference reconciles with all A1 capability identities", cap_actual == cap_expected,
                   {"actualCount": len(cap_actual), "expectedCount": len(cap_expected)})

        abb_elems = [e for e in self.a1_elements if e["elementType"] == "ReferenceArchitectureBuildingBlock"]
        abb_expected = []
        for e in abb_elems:
            abb_expected.extend([e["id"], e["localName"], e["qname"], e["iri"]])
            if e.get("sourceIdentifier"):
                abb_expected.append(e["sourceIdentifier"])
        abb_actual = self.a8["$defs"]["ABBReference"]["enum"]
        self.check("A13-058", g, "A8 ABBReference reconciles with all A1 reference ABB identities and source shorthands", abb_actual == abb_expected,
                   {"actualCount": len(abb_actual), "expectedCount": len(abb_expected)})

        rr_elems = [e for e in self.a1_elements if e["elementType"] == "LogicalRegistryResponsibility"]
        rr_expected = []
        for e in rr_elems:
            rr_expected.extend([e["id"], e["localName"], e["qname"], e["iri"]])
            if e.get("sourceIdentifier"):
                rr_expected.append(e["sourceIdentifier"])
        rr_actual = self.a8["$defs"]["RegistryResponsibilityReference"]["enum"]
        self.check("A13-059", g, "A8 RegistryResponsibilityReference reconciles with A1 RR1-RR9 identities", rr_actual == rr_expected,
                   {"actualCount": len(rr_actual), "expectedCount": len(rr_expected)})

        con_elems = [e for e in self.a1_elements if e["elementType"] == "ArchitecturalContractFamily"]
        con_expected = []
        for e in con_elems:
            con_expected.extend([e["id"], e["localName"], e["qname"], e["iri"]])
            if e.get("sourceIdentifier"):
                con_expected.append(e["sourceIdentifier"])
        con_actual = self.a8["$defs"]["ContractFamilyReference"]["enum"]
        self.check("A13-060", g, "A8 ContractFamilyReference reconciles with A1 C1-C7 identities", con_actual == con_expected,
                   {"actualCount": len(con_actual), "expectedCount": len(con_expected)})

        should_inv = [i["id"] for i in self.a3_invariants if i["requirementLevel"] == "SHOULD"]
        expected_should_inv = []
        for iid in should_inv:
            expected_should_inv.extend([iid, f"gearai:{iid}", INV_NS + iid])
        actual_should_inv = self.a8["$defs"]["ShouldInvariantReference"]["enum"]
        self.check("A13-061", g, "A8 invariant-tightening set equals the seven A3 SHOULD invariants", actual_should_inv == expected_should_inv and len(should_inv) == 7,
                   {"shouldInvariantIds": should_inv, "referenceFormCount": len(actual_should_inv)})

        requirement_lines = re.findall(r"^- \*\*(GEARA-[A-Z]+-\d{3})\*\* — (.+)$", self.spec_text, flags=re.MULTILINE)
        req_map = {rid: stmt for rid, stmt in requirement_lines if not rid.startswith("GEARA-INV-")}
        should_requirements = [rid for rid, stmt in req_map.items() if re.search(r"\bSHOULD\b", stmt)]
        actual_should_req = self.a8["$defs"]["ShouldRequirementReference"]["enum"]
        self.check("A13-062", g, "A8 requirement-tightening set equals the 32 stable Appendix-H SHOULD requirements",
                   len(req_map) == 75 and len(should_requirements) == 32 and actual_should_req == should_requirements,
                   {"stableRequirementCount": len(req_map), "shouldRequirementCount": len(should_requirements), "schemaCount": len(actual_should_req)})
        top_props = set(self.a8.get("properties", {}))
        prohibited = sorted(top_props & {"deviations", "waivers", "exceptions"})
        self.check("A13-063", g, "A8 exposes no Profile-level deviation, waiver or exception mechanism", not prohibited,
                   {"prohibitedProperties": prohibited})

    def check_a9_a11(self) -> None:
        g = "A9-A11 examples and testcase oracle reconciliation"
        examples = self.a9["examples"]
        self.check("A13-064", g, "A9 example index declares 13 examples and every indexed file exists",
                   self.a9.get("exampleCount") == 13 and len(examples) == 13 and all((self.spec_dir / "examples" / e["file"]).is_file() for e in examples),
                   {"declared": self.a9.get("exampleCount"), "indexed": len(examples)})
        files = [e["file"] for e in examples]
        cp_prefix_ok = all(any(f.startswith(f"{n:02d}-") for f in files) for n in range(1, 11))
        self.check("A13-065", g, "A9 retains one indexed canonical example for each CP1-CP10 pattern position", cp_prefix_ok)

        tcs = self.manifest["testcases"]
        ids = [tc["id"] for tc in tcs]
        self.check("A13-066", g, "A10/A11 manifest declares 70 unique testcase identities", len(tcs) == 70 and len(set(ids)) == 70 and self.manifest.get("testcaseCount") == 70,
                   {"actual": len(tcs), "unique": len(set(ids)), "declared": self.manifest.get("testcaseCount")})
        band_counts = Counter(tc["band"] for tc in tcs)
        expected_bands = Counter({"positive": 21, "structural-invalid": 12, "architecture-semantic-invalid": 23, "profile-invalid": 14})
        self.check("A13-067", g, "A10/A11 testcase band counts reconcile to 21/12/23/14", band_counts == expected_bands,
                   {"actual": dict(sorted(band_counts.items()))})
        missing_fixture = [tc["id"] for tc in tcs if not (self.spec_dir / "testcases" / tc["fixture"]).is_file()]
        self.check("A13-068", g, "Every manifest testcase resolves to a packaged fixture", not missing_fixture,
                   {"missing": missing_fixture})
        bad_inv = []
        inv_ids = set(self.a3_by_id)
        for tc in tcs:
            for iid in tc.get("applicableInvariants", []) or []:
                if iid not in inv_ids:
                    bad_inv.append({"testcase": tc["id"], "invariant": iid})
            for iid in tc.get("expected", {}).get("expectedViolationInvariants", []) or []:
                if iid not in inv_ids:
                    bad_inv.append({"testcase": tc["id"], "invariant": iid})
        self.check("A13-069", g, "All testcase invariant references resolve to A3", not bad_inv, {"unresolved": bad_inv})
        manifest_shapes = set()
        for tc in tcs:
            manifest_shapes.update(tc.get("expected", {}).get("expectedViolationShapes", []) or [])
        actual_shapes = {str(s) for s in self.a6.subjects(RDF.type, SH.NodeShape)}
        self.check("A13-070", g, "Semantic-negative oracle covers the complete current A6 NodeShape set", manifest_shapes == actual_shapes,
                   {"manifestShapeCount": len(manifest_shapes), "a6ShapeCount": len(actual_shapes), "missing": sorted(actual_shapes-manifest_shapes), "unexpected": sorted(manifest_shapes-actual_shapes)})
        positive_source_pairs_ok = True
        mismatches = []
        for n in range(1, 14):
            tc = next((x for x in tcs if x["id"] == f"GEARA-TC-{n:03d}"), None)
            ex = examples[n-1] if n <= len(examples) else None
            if not tc or not ex:
                positive_source_pairs_ok = False
                mismatches.append(f"GEARA-TC-{n:03d}")
                continue
            fixture = (self.spec_dir / "testcases" / tc["fixture"]).read_text(encoding="utf-8")
            example = (self.spec_dir / "examples" / ex["file"]).read_text(encoding="utf-8")
            if ex["file"].endswith(".jsonld"):
                # Only the relative @context path differs because testcase fixtures are one directory deeper.
                fixture_norm = fixture.replace('"../../geara-0.1.context.jsonld"', '"../geara-0.1.context.jsonld"')
                if fixture_norm != example:
                    positive_source_pairs_ok = False
                    mismatches.append(tc["id"])
            elif fixture != example:
                positive_source_pairs_ok = False
                mismatches.append(tc["id"])
        self.check("A13-071", g, "A10 TC001-TC013 remain deterministic copies of A9 examples modulo relative JSON-LD context path", positive_source_pairs_ok,
                   {"mismatches": mismatches})

    def check_a12(self) -> None:
        g = "A12 runner reconciliation"
        runner = self.spec_dir / "conformance/run-geara-conformance.py"
        self.check("A13-072", g, "A12 runner and requirements declaration are packaged",
                   runner.is_file() and (self.spec_dir / "conformance/requirements.txt").is_file())
        compile_proc = subprocess.run([sys.executable, "-m", "py_compile", str(runner)], capture_output=True, text=True)
        self.check("A13-073", g, "A12 runner compiles successfully", compile_proc.returncode == 0,
                   {"returnCode": compile_proc.returncode, "stderr": compile_proc.stderr.strip()})

        reports = []
        run_errors = []
        with tempfile.TemporaryDirectory(prefix="geara-a13-") as td:
            for idx in (1, 2):
                out = Path(td) / f"run-{idx}.json"
                p = subprocess.run([sys.executable, str(runner), "--json-report", str(out)], cwd=self.repo_root,
                                   capture_output=True, text=True)
                if p.returncode != 0 or not out.exists():
                    run_errors.append({"run": idx, "returnCode": p.returncode, "stderr": p.stderr.strip(), "stdoutTail": p.stdout[-1000:]})
                else:
                    reports.append(load_json(out))
        self.check("A13-074", g, "A12 complete-suite execution succeeds twice", not run_errors and len(reports) == 2,
                   {"errors": run_errors})
        deterministic = len(reports) == 2 and stable_json_bytes(reports[0]) == stable_json_bytes(reports[1])
        self.check("A13-075", g, "A12 complete-suite JSON report is deterministic across repeated executions", deterministic)
        if reports:
            summary = reports[0]["summary"]
            all_match = summary.get("testcasesExecuted") == 70 and summary.get("testcasesMatched") == 70 and summary.get("testcasesMismatched") == 0 and summary.get("allExpectedOutcomesMatched") is True
            layers = summary.get("layers", {})
            layer_counts_ok = (
                layers.get("A7_JSON_SCHEMA", {}).get("executed") == 55
                and layers.get("A5_JSONLD_EXPANSION", {}).get("executed") == 34
                and layers.get("A6_SHACL", {}).get("executed") == 34
                and layers.get("A8_PROFILE_SCHEMA", {}).get("executed") == 15
            )
            shape_ok = summary.get("a6ExecutableShapeCount") == 24
        else:
            all_match = layer_counts_ok = shape_ok = False
            summary = {}
        self.check("A13-076", g, "A12 matches all 70 manifest-declared testcase outcomes", all_match, {"summary": summary})
        self.check("A13-077", g, "A12 validator execution counts reconcile with the manifest", layer_counts_ok,
                   {"layers": summary.get("layers", {}) if isinstance(summary, dict) else {}})
        self.check("A13-078", g, "A12 executes against all 24 current A6 executable Shapes", shape_ok,
                   {"actual": summary.get("a6ExecutableShapeCount") if isinstance(summary, dict) else None})

    def check_spec_and_inventory(self) -> None:
        g = "Normative specification and artifact inventory reconciliation"
        # These checks assume the A13 specification integration has been applied before the runner executes.
        required_downloads = [
            "geara-0.1.architecture-registry.yaml",
            "geara-0.1.relationship-registry.yaml",
            "geara-0.1.invariant-catalogue.yaml",
            "geara-0.1.ttl",
            "geara-0.1.context.jsonld",
            "geara-0.1.shacl.ttl",
            "geara-0.1.schema.json",
            "geara-0.1.profile.schema.json",
            "examples/geara-0.1.examples.json",
            "testcases/manifest.json",
            "conformance/run-geara-conformance.py",
            "reconciliation/geara-0.1.reconciliation-report.json",
            "reconciliation/run-geara-reconciliation.py",
        ]
        missing_links = [x for x in required_downloads if x not in self.spec_text]
        self.check("A13-079", g, "Human-readable specification references the established A1-A13 machine entry points", not missing_links,
                   {"missing": missing_links})
        programme = [
            "A1   Canonical Architecture Registry", "A2   Architecture Relationship Registry", "A3   Architecture Invariant Catalogue",
            "A4   RDF / OWL Representation", "A5   JSON-LD Context", "A6   SHACL Constraint Layer",
            "A7   Structural JSON / JSON-LD Schema", "A8   GEARA Profile Schema", "A9   Canonical Example Set",
            "A10  Positive Test Cases", "A11  Negative Test Cases", "A12  Automated Conformance Runner",
            "A13  Cross-Artifact Reconciliation", "A14  Regression Suite", "A15  Release Closure",
        ]
        self.check("A13-080", g, "Human-readable specification preserves the agreed fifteen-slice executable programme", all(x in self.spec_text for x in programme))
        self.check("A13-081", g, "Specification explicitly preserves the machine-validation versus Architecture-Assessment boundary",
                   "Passing all machine-executable checks SHALL NOT by itself establish full GEARA conformance" in self.spec_text
                   and "Architecture Assessment" in self.spec_text)
        self.check("A13-082", g, "Specification marks A14 and A15 as the remaining executable slices after A13",
                   "A14 and A15 remain" in self.spec_text or "A14 and A15 remain intentionally deferred" in self.spec_text)

        required_files = [
            self.spec_dir / "geara-0.1.architecture-registry.yaml",
            self.spec_dir / "geara-0.1.relationship-registry.yaml",
            self.spec_dir / "geara-0.1.invariant-catalogue.yaml",
            self.spec_dir / "geara-0.1.ttl",
            self.spec_dir / "geara-0.1.context.jsonld",
            self.spec_dir / "geara-0.1.shacl.ttl",
            self.spec_dir / "geara-0.1.schema.json",
            self.spec_dir / "geara-0.1.profile.schema.json",
            self.spec_dir / "examples/geara-0.1.examples.json",
            self.spec_dir / "testcases/manifest.json",
            self.spec_dir / "conformance/run-geara-conformance.py",
        ]
        self.check("A13-083", g, "All canonical A1-A12 machine entry-point artifacts are present", all(p.is_file() for p in required_files),
                   {"missing": [str(p.relative_to(self.repo_root)) for p in required_files if not p.is_file()]})
        # Ensure the source stack reports the intended stage/version boundaries rather than being silently restaged as A13.
        stage_expect = {
            "A1": self.a1["registry"].get("artifactStage"),
            "A2": self.a2["registry"].get("artifactStage"),
            "A3": self.a3["registry"].get("artifactStage"),
            "A5": self.a5.get("x-geara-artifactStage"),
            "A7": self.a7.get("x-geara-artifactStage"),
        }
        self.check("A13-084", g, "Established source artifacts retain their original artifact-stage identities",
                   stage_expect == {"A1": "GEARA-A1", "A2": "GEARA-A2", "A3": "GEARA-A3", "A5": "GEARA-A5", "A7": "GEARA-A7"},
                   {"actual": stage_expect})


def infer_repo_root(script_path: Path) -> Path:
    spec_dir = script_path.resolve().parent.parent
    # .../<repo>/static/specs/geara/0.1 -> repo is parents[3]
    return spec_dir.parents[3]


def main() -> int:
    parser = argparse.ArgumentParser(description="GEARA 0.1 A13 cross-artifact reconciliation runner")
    parser.add_argument("--repo-root", type=Path, default=None, help="Repository/package root containing docs/ and static/")
    parser.add_argument("--json-report", type=Path, default=None, help="Write deterministic reconciliation JSON report")
    args = parser.parse_args()

    repo_root = args.repo_root.resolve() if args.repo_root else infer_repo_root(Path(__file__))
    try:
        report = Reconciler(repo_root).run()
    except Exception as exc:
        print(f"GEARA A13 reconciliation setup/error: {exc}", file=sys.stderr)
        return 2

    for c in report["checks"]:
        print(f"{c['id']} {c['status']} - {c['title']}")
    s = report["summary"]
    print(f"GEARA A13 reconciliation: {s['passed']}/{s['checks']} PASS; {s['failed']} FAIL")

    if args.json_report:
        args.json_report.parent.mkdir(parents=True, exist_ok=True)
        args.json_report.write_bytes(stable_json_bytes(report))

    return 0 if s["allChecksPassed"] else 1


if __name__ == "__main__":
    raise SystemExit(main())
