#!/usr/bin/env python3
from __future__ import annotations

import argparse
import fnmatch
import hashlib
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
import time
import zipfile
from collections import Counter, defaultdict
from pathlib import Path, PurePosixPath
from typing import Any, Iterable

ROOT = Path(__file__).resolve().parents[1]
LEDGER = ROOT / "ci" / "themefactory-tests.jsonl"
POLICY = ROOT / "ci" / "policy.yaml"
SCHEMA = ROOT / "ci" / "schemas" / "theme-test.schema.json"
WORKFLOW = ROOT / ".github" / "workflows" / "themefactory-verify.yml"
VALID_STATUSES = {"todo", "in_progress", "complete", "deferred"}
VALID_ARCH = {"gutenberg-hybrid", "block-fse", "classic", "elementor"}
VALID_WORKFLOW = {"new-theme", "mockup-to-theme", "source-preserving-conversion", "enhancement"}
VALID_RUNNERS = {"static", "runtime"}
COST_WEIGHT = {"low": 5, "medium": 15, "high": 45}
META_IDS = {f"TF-META-{i:03d}" for i in range(1, 15)}

class VerificationError(RuntimeError):
    pass


def fail(message: str) -> None:
    raise VerificationError(message)


def load_jsonish(path: Path) -> Any:
    try:
        return json.loads(path.read_text())
    except FileNotFoundError as exc:
        raise VerificationError(f"missing file: {path}") from exc
    except json.JSONDecodeError as exc:
        raise VerificationError(f"{path}: invalid JSON-compatible YAML: {exc}") from exc


def sha256_file(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 tree_digest(root: Path, ignore: Iterable[str] = ()) -> str:
    h = hashlib.sha256()
    ignored = tuple(ignore)
    for path in sorted((p for p in root.rglob("*") if p.is_file()), key=lambda p: p.as_posix().casefold()):
        rel = path.relative_to(root).as_posix()
        if any(fnmatch.fnmatch(rel, pat) for pat in ignored):
            continue
        h.update(rel.encode() + b"\0")
        h.update(hashlib.sha256(path.read_bytes()).digest())
    return h.hexdigest()


def load_ledger() -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    try:
        lines = LEDGER.read_text().splitlines()
    except FileNotFoundError as exc:
        raise VerificationError(f"missing ledger: {LEDGER}") from exc
    for n, line in enumerate(lines, 1):
        if not line.strip():
            continue
        try:
            row = json.loads(line)
        except json.JSONDecodeError as exc:
            raise VerificationError(f"ledger line {n}: {exc}") from exc
        if not isinstance(row, dict):
            fail(f"ledger line {n}: expected object")
        rows.append(row)
    return rows


def implementation_file(value: str) -> Path | None:
    if not value:
        return None
    file_part = value.split(":", 1)[0]
    path = ROOT / file_part
    return path


def check_ledger(*, require_complete: bool = False) -> dict[str, Any]:
    rows = load_ledger()
    ids = [r.get("id") for r in rows]
    duplicates = sorted(k for k, v in Counter(ids).items() if v > 1)
    if duplicates:
        fail(f"duplicate ledger IDs: {duplicates}")
    bad = []
    for row in rows:
        missing = [k for k in ("id", "title", "phase", "runner", "required", "blocking", "status", "implementation") if k not in row]
        if missing:
            bad.append(f"{row.get('id','?')}: missing {missing}")
            continue
        if row["status"] not in VALID_STATUSES:
            bad.append(f"{row['id']}: invalid status {row['status']}")
        if row["runner"] not in VALID_RUNNERS:
            bad.append(f"{row['id']}: invalid runner {row['runner']}")
        if row["status"] == "complete":
            impl = implementation_file(str(row["implementation"]))
            if impl is None or not impl.exists():
                bad.append(f"{row['id']}: complete but implementation missing: {row['implementation']}")
        if require_complete and row["required"] and row["status"] != "complete":
            bad.append(f"{row['id']}: required test is not complete")
    if bad:
        fail("ledger invalid:\n" + "\n".join(bad))
    return {"tests": len(rows), "status": dict(Counter(r["status"] for r in rows))}


def discover_themes() -> list[str]:
    base = ROOT / "themes"
    return sorted([p.name for p in base.iterdir() if p.is_dir() and not p.name.startswith(".")], key=str.casefold)


def contract_path(theme: str) -> Path:
    return ROOT / "themes" / theme / "theme-test.yaml"


def validate_contract(theme: str, contract: dict[str, Any]) -> None:
    if contract.get("schema") != 1:
        fail(f"{theme}: contract schema must be 1")
    if contract.get("architecture") not in VALID_ARCH:
        fail(f"{theme}: invalid architecture {contract.get('architecture')!r}")
    if contract.get("workflow") not in VALID_WORKFLOW:
        fail(f"{theme}: invalid workflow {contract.get('workflow')!r}")
    if not isinstance(contract.get("features"), dict):
        fail(f"{theme}: features must be an object")
    runtime = contract.get("runtime")
    if not isinstance(runtime, dict) or "routes" not in runtime:
        fail(f"{theme}: runtime.routes is required")
    routes = runtime.get("routes")
    if not (routes == "auto" or isinstance(routes, list)):
        fail(f"{theme}: runtime.routes must be 'auto' or a list")
    if isinstance(routes, list) and not routes:
        fail(f"{theme}: runtime.routes list cannot be empty")
    reference = contract.get("reference")
    if contract.get("workflow") in {"source-preserving-conversion", "mockup-to-theme"}:
        if not reference:
            fail(f"{theme}: {contract['workflow']} requires reference")
        ref = ROOT / "reference" / str(reference)
        if not ref.exists():
            fail(f"{theme}: reference does not exist: {ref}")
    viewports = runtime.get("viewports")
    if viewports is not None and (not isinstance(viewports, list) or not all(isinstance(x, int) and 240 <= x <= 5120 for x in viewports)):
        fail(f"{theme}: invalid runtime.viewports")
    if contract.get("features", {}).get("demo_import"):
        demo = contract.get("demo_import")
        if not isinstance(demo, dict):
            fail(f"{theme}: demo_import feature requires demo_import contract")
        selector = demo.get("button_selector")
        if not isinstance(selector, str) or not selector.strip():
            fail(f"{theme}: demo_import.button_selector is required to avoid ambiguous admin submit controls")


def load_contract(theme: str) -> dict[str, Any]:
    path = contract_path(theme)
    value = load_jsonish(path)
    if not isinstance(value, dict):
        fail(f"{theme}: contract must be object")
    validate_contract(theme, value)
    return value


def check_contracts() -> dict[str, Any]:
    themes = discover_themes()
    contracted: list[str] = []
    for theme in themes:
        load_contract(theme)
        contracted.append(theme)
    return {"themes": contracted, "count": len(contracted)}


def profile_tokens(contract: dict[str, Any]) -> set[str]:
    tokens = {"core", str(contract["architecture"]), str(contract["workflow"])}
    for name, enabled in contract.get("features", {}).items():
        if enabled:
            tokens.add(name.replace("_", "-"))
            tokens.add(name)
    return tokens


def applicable(row: dict[str, Any], contract: dict[str, Any], mode: str) -> bool:
    if row.get("phase") == "scheduled" and mode != "scheduled":
        return False
    if row.get("phase") != "scheduled" and mode == "scheduled" and not row.get("required", True):
        # scheduled mode can still include core required checks, but not advisory duplicates
        pass
    workflows = row.get("workflows") or []
    if workflows and contract["workflow"] not in workflows:
        return False
    features = row.get("features") or []
    cfeatures = contract.get("features", {})
    if features and not all(bool(cfeatures.get(f, cfeatures.get(f.replace("-", "_"), False))) for f in features):
        return False
    profiles = row.get("profiles") or []
    if profiles and "core" not in profiles:
        tokens = profile_tokens(contract)
        if not any(p in tokens or p.replace("-", "_") in tokens for p in profiles):
            return False
    return True


def plan(theme: str, mode: str = "pr") -> dict[str, Any]:
    contract = load_contract(theme)
    rows = load_ledger()
    selected = [r for r in rows if applicable(r, contract, mode)]
    ids = [r["id"] for r in selected]
    if len(ids) != len(set(ids)):
        fail(f"{theme}: planner emitted duplicate test IDs")
    phases: dict[str, list[str]] = defaultdict(list)
    for row in selected:
        phases[row["phase"]].append(row["id"])
    result = {
        "schema": 1,
        "theme": theme,
        "mode": mode,
        "architecture": contract["architecture"],
        "workflow": contract["workflow"],
        "profiles": sorted(profile_tokens(contract)),
        "tests": ids,
        "phases": {k: v for k, v in sorted(phases.items())},
        "expected": len(ids),
        "tree": tree_digest(ROOT / "themes" / theme),
        "contract_sha256": sha256_file(contract_path(theme)),
        "ledger_sha256": sha256_file(LEDGER),
    }
    return result


def build_shards(theme: str, mode: str = "pr", max_shards: int | None = None) -> list[dict[str, Any]]:
    p = plan(theme, mode)
    rows_by_id = {r["id"]: r for r in load_ledger()}
    runtime_ids = [tid for tid in p["tests"] if rows_by_id[tid]["runner"] == "runtime" and rows_by_id[tid]["phase"] in {"runtime", "release", "scheduled"}]
    groups: dict[str, list[str]] = defaultdict(list)
    for tid in runtime_ids:
        row = rows_by_id[tid]
        # keep semantically related checks in one fixture/shard; this reduces repeated WP state setup.
        key = str(row.get("domain") or tid.split("-")[1])
        groups[key].append(tid)
    weighted = []
    for key, test_ids in groups.items():
        weight = sum(COST_WEIGHT.get(str(rows_by_id[x].get("cost", "low")), 5) for x in test_ids)
        weighted.append((weight, key, sorted(test_ids)))
    weighted.sort(key=lambda x: (-x[0], x[1]))
    if not weighted:
        return []
    if max_shards is None:
        max_shards = int(load_jsonish(POLICY).get("runtime", {}).get("max_browser_shards", 6))
    count = max(1, min(max_shards, len(weighted)))
    bins = [{"weight": 0, "groups": [], "tests": []} for _ in range(count)]
    for weight, key, test_ids in weighted:
        target = min(range(count), key=lambda i: (bins[i]["weight"], i))
        bins[target]["weight"] += weight
        bins[target]["groups"].append(key)
        bins[target]["tests"].extend(test_ids)
    result = []
    for i, entry in enumerate(bins):
        if not entry["tests"]:
            continue
        result.append({"shard_id": f"s{i+1:02d}", "theme": theme, "groups": entry["groups"], "tests": entry["tests"], "weight": entry["weight"]})
    claimed = [x for shard in result for x in shard["tests"]]
    if len(claimed) != len(set(claimed)) or set(claimed) != set(runtime_ids):
        fail(f"{theme}: invalid shard union")
    return result


def run(cmd: list[str], *, cwd: Path | None = None, input_bytes: bytes | None = None) -> subprocess.CompletedProcess[bytes]:
    return subprocess.run(cmd, cwd=cwd or ROOT, input=input_bytes, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False)


def theme_root(theme: str) -> Path:
    path = ROOT / "themes" / theme
    if not path.is_dir():
        fail(f"unknown theme: {theme}")
    return path


def iter_files(base: Path, suffix: str | tuple[str, ...]) -> list[Path]:
    return sorted([p for p in base.rglob("*") if p.is_file() and p.suffix.lower() in ((suffix,) if isinstance(suffix, str) else suffix)], key=lambda p: p.as_posix().casefold())


def assertion(condition: bool, message: str) -> None:
    if not condition:
        fail(message)


def static_required_files(theme: str, _: dict[str, Any]) -> None:
    base = theme_root(theme)
    for name in ("style.css", "index.php"):
        assertion((base / name).is_file(), f"{theme}: missing {name}")


def static_style_header(theme: str, _: dict[str, Any]) -> None:
    text = (theme_root(theme) / "style.css").read_text(errors="replace")[:8192]
    for key in ("Theme Name", "Version", "Requires at least", "Requires PHP"):
        assertion(re.search(rf"(?mi)^\s*{re.escape(key)}\s*:\s*\S.+$", text) is not None, f"{theme}: style.css missing {key}")


def static_php_syntax(theme: str, _: dict[str, Any]) -> None:
    for path in iter_files(theme_root(theme), ".php"):
        cp = run(["php", "-l", str(path)])
        assertion(cp.returncode == 0, f"{theme}: PHP syntax failed {path.relative_to(ROOT)}\n{cp.stdout.decode(errors='replace')}")


def static_js_syntax(theme: str, _: dict[str, Any]) -> None:
    for path in iter_files(theme_root(theme), (".js", ".mjs", ".cjs")):
        cp = run(["node", "--check", str(path)])
        assertion(cp.returncode == 0, f"{theme}: JS syntax failed {path.relative_to(ROOT)}\n{cp.stdout.decode(errors='replace')}")


def static_json(theme: str, _: dict[str, Any]) -> None:
    for path in iter_files(theme_root(theme), ".json"):
        try:
            json.loads(path.read_text())
        except json.JSONDecodeError as exc:
            fail(f"{theme}: invalid JSON {path.relative_to(ROOT)}: {exc}")


def static_theme_json(theme: str, _: dict[str, Any]) -> None:
    path = theme_root(theme) / "theme.json"
    assertion(path.is_file(), f"{theme}: missing theme.json")
    data = load_jsonish(path)
    assertion(isinstance(data, dict), f"{theme}: theme.json root must be object")
    assertion(data.get("version") in {2, 3}, f"{theme}: unsupported theme.json version {data.get('version')}")
    for key in ("settings", "styles"):
        if key in data:
            assertion(isinstance(data[key], dict), f"{theme}: theme.json {key} must be object")


def static_block_metadata(theme: str, _: dict[str, Any]) -> None:
    base = theme_root(theme)
    block_jsons = [p for p in base.rglob("block.json") if p.is_file()]
    names: set[str] = set()
    for path in block_jsons:
        data = load_jsonish(path)
        name = data.get("name") if isinstance(data, dict) else None
        assertion(isinstance(name, str) and "/" in name, f"{theme}: block.json missing namespaced name: {path}")
        assertion(name not in names, f"{theme}: duplicate block name {name}")
        names.add(name)
    # PHP/JS registered blocks may exist without block.json; contract can permit that, but if block.json exists it must be sound.


def static_symlinks(theme: str, _: dict[str, Any]) -> None:
    links = [p for p in theme_root(theme).rglob("*") if p.is_symlink()]
    assertion(not links, f"{theme}: symlinks not allowed: {[str(p.relative_to(ROOT)) for p in links]}")


def combined_text(theme: str) -> str:
    chunks = []
    for p in theme_root(theme).rglob("*"):
        if p.is_file() and p.suffix.lower() in {".php", ".js", ".json", ".css", ".html", ".txt", ".md"}:
            try:
                chunks.append(p.read_text(errors="replace"))
            except OSError:
                pass
    return "\n".join(chunks)


def static_secrets(theme: str, _: dict[str, Any]) -> None:
    text = combined_text(theme)
    patterns = [r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----", r"gh[pousr]_[A-Za-z0-9]{20,}", r"sk-[A-Za-z0-9]{20,}"]
    hits = [pat for pat in patterns if re.search(pat, text)]
    assertion(not hits, f"{theme}: possible committed secret/private key markers: {hits}")


def static_local_paths(theme: str, _: dict[str, Any]) -> None:
    text = combined_text(theme)
    forbidden = ["/home/user/", "/home/oai/", "/mnt/data/", "debian1", "debian2", "debian3"]
    hits = [x for x in forbidden if x in text]
    assertion(not hits, f"{theme}: shipped source contains local/buildbox paths: {hits}")


def static_legacy_runtime(theme: str, contract: dict[str, Any]) -> None:
    if contract.get("allow_legacy_runtime"):
        return
    bad = []
    for p in theme_root(theme).rglob("*"):
        rel = p.relative_to(theme_root(theme)).as_posix().lower()
        if p.is_file() and ("phpmailer" in rel or rel.endswith("sendmail.php") or rel.endswith("mailer.php")):
            bad.append(rel)
    assertion(not bad, f"{theme}: bundled legacy mail runtime: {bad}")


def static_i18n(theme: str, _: dict[str, Any]) -> None:
    style = (theme_root(theme) / "style.css").read_text(errors="replace")
    m = re.search(r"(?mi)^\s*Text Domain\s*:\s*([^\s]+)", style)
    domains = set(re.findall(r"(?:__|_e|esc_html__|esc_html_e|esc_attr__|esc_attr_e)\s*\([^,]+,\s*['\"]([^'\"]+)['\"]", combined_text(theme)))
    if domains:
        assertion(m is not None, f"{theme}: translation calls exist but Text Domain header missing")
        declared = m.group(1).strip() if m else ""
        assertion(domains <= {declared}, f"{theme}: translation domains {sorted(domains)} do not match {declared}")


def static_debug_output(theme: str, _: dict[str, Any]) -> None:
    hits=[]
    for p in iter_files(theme_root(theme), ".php"):
        text=p.read_text(errors="replace")
        if re.search(r"\b(var_dump|print_r)\s*\(", text) or re.search(r"\bdd\s*\(", text):
            hits.append(p.relative_to(ROOT).as_posix())
    assertion(not hits, f"{theme}: debug output calls in shipped PHP: {hits}")


def static_dependency_contract(theme: str, contract: dict[str, Any]) -> None:
    plugins = contract.get("plugins", [])
    assertion(isinstance(plugins, list), f"{theme}: plugins must be list")
    for plugin in plugins:
        assertion(isinstance(plugin, dict), f"{theme}: plugin dependency must be object")
        for key in ("slug", "version", "required", "purpose"):
            assertion(key in plugin, f"{theme}: plugin dependency missing {key}")


def source_manifest(theme: str) -> Path | None:
    candidates = list(theme_root(theme).glob("*SOURCE*PRESERVATION*MANIFEST*.json")) + list(theme_root(theme).glob("verification/*source*manifest*.json"))
    return candidates[0] if candidates else None


def static_source_manifest(theme: str, contract: dict[str, Any]) -> None:
    path = source_manifest(theme)
    assertion(path is not None and path.is_file(), f"{theme}: source-preservation manifest missing")
    data = load_jsonish(path)
    assertion(isinstance(data, dict), f"{theme}: source manifest must be object")
    assertion(contract.get("reference"), f"{theme}: source conversion contract has no reference")


def static_visual_baseline(theme: str, contract: dict[str, Any]) -> None:
    reference = contract.get("reference")
    if contract["workflow"] in {"source-preserving-conversion", "mockup-to-theme"}:
        assertion(reference and (ROOT / "reference" / str(reference)).exists(), f"{theme}: visual authority reference missing")
    elif contract["workflow"] == "new-theme":
        baseline = theme_root(theme) / "verification" / "visual-baseline.json"
        assertion(baseline.exists(), f"{theme}: new theme requires approved visual-baseline.json")


def static_wp_security_patterns(theme: str, _: dict[str, Any]) -> None:
    text = combined_text(theme)
    assertion(not re.search(r"\beval\s*\(|\bassert\s*\(\s*\$_", text), f"{theme}: dangerous eval/assert pattern")
    # State-changing admin hooks should show both capability and nonce checks in the same theme.
    if re.search(r"admin_post_|wp_ajax_", text):
        assertion(re.search(r"current_user_can|user_can", text) is not None, f"{theme}: admin/AJAX handler without capability check evidence")
        assertion(re.search(r"check_admin_referer|check_ajax_referer|wp_verify_nonce", text) is not None, f"{theme}: admin/AJAX handler without nonce check evidence")


def static_editable_coverage(theme: str, contract: dict[str, Any]) -> None:
    required = contract.get("editable_fields", {})
    if not required:
        return
    text = combined_text(theme)
    missing=[]
    for component, fields in required.items():
        for field in fields:
            if str(field) not in text:
                missing.append(f"{component}.{field}")
    assertion(not missing, f"{theme}: editable field identities absent from implementation: {missing}")


def static_custom_block_justification(theme: str, contract: dict[str, Any]) -> None:
    just = contract.get("custom_block_justifications", {})
    if contract.get("features", {}).get("custom_blocks"):
        assertion(isinstance(just, dict) and just, f"{theme}: custom blocks require custom_block_justifications")
        for name, reason in just.items():
            assertion(isinstance(reason, str) and len(reason.strip()) >= 12, f"{theme}: weak custom block justification for {name}")


def static_source_deviations(theme: str, contract: dict[str, Any]) -> None:
    deviations = contract.get("source_deviations", [])
    assertion(isinstance(deviations, list), f"{theme}: source_deviations must be list")
    for item in deviations:
        assertion(isinstance(item, dict), f"{theme}: source deviation must be object")
        assertion(item.get("selector") not in {"*", "html", "body"}, f"{theme}: wildcard/whole-page source deviation forbidden")
        assertion(all(item.get(k) for k in ("selector", "reason", "type")), f"{theme}: source deviation needs selector/reason/type")


def static_classic_hierarchy(theme: str, contract: dict[str, Any]) -> None:
    base=theme_root(theme)
    assertion((base/"index.php").exists(), f"{theme}: classic fallback index.php missing")
    for name in contract.get("classic_templates", []):
        assertion((base/name).is_file(), f"{theme}: declared classic template missing: {name}")


def static_elementor_contract(theme: str, contract: dict[str, Any]) -> None:
    assertion(contract.get("architecture") == "elementor", f"{theme}: Elementor contract on non-Elementor architecture")
    assertion(any(p.get("slug") == "elementor" for p in contract.get("plugins", [])), f"{theme}: Elementor architecture must declare elementor plugin")


def static_woo_overrides(theme: str, contract: dict[str, Any]) -> None:
    base=theme_root(theme)/"woocommerce"
    if not base.exists():
        return
    for p in iter_files(base, ".php"):
        cp=run(["php","-l",str(p)])
        assertion(cp.returncode==0, f"{theme}: invalid WooCommerce override {p}")


def static_fse_markup(theme: str, _: dict[str, Any]) -> None:
    base=theme_root(theme)
    templates=list((base/"templates").glob("*.html")) if (base/"templates").is_dir() else []
    assertion(templates, f"{theme}: block-fse requires templates/*.html")
    for p in templates + (list((base/"parts").glob("*.html")) if (base/"parts").is_dir() else []):
        text=p.read_text(errors="replace")
        assertion(text.count("<!-- wp:") <= text.count("<!-- /wp:") + text.count(" /-->"), f"{theme}: likely unbalanced block markup in {p.relative_to(ROOT)}")


def check_deterministic_zip(theme: str, _: dict[str, Any]) -> None:
    base=theme_root(theme)
    script=ROOT/"tools"/"theme_factory.py"
    with tempfile.TemporaryDirectory() as td:
        container=Path(td)/"container"; container.mkdir(); shutil.copytree(base,container/theme)
        a=Path(td)/"a.zip"; b=Path(td)/"b.zip"
        for dest in (a,b):
            cp=run([sys.executable,str(script),"pack",str(container),str(dest)])
            assertion(cp.returncode==0, f"{theme}: deterministic pack failed: {cp.stdout.decode(errors='replace')}")
        assertion(sha256_file(a)==sha256_file(b), f"{theme}: repeated pack is not deterministic")


def zip_tree(path: Path) -> dict[str, str]:
    result={}
    with zipfile.ZipFile(path) as z:
        for info in z.infolist():
            if info.is_dir(): continue
            parts=PurePosixPath(info.filename).parts
            rel="/".join(parts[1:]) if len(parts)>1 else parts[0]
            result[rel]=hashlib.sha256(z.read(info)).hexdigest()
    return result


def non_shipping_theme_files() -> set[str]:
    policy = load_jsonish(ROOT / "ci" / "policy.yaml")
    values = policy.get("non_shipping_theme_files", []) if isinstance(policy, dict) else []
    assertion(isinstance(values, list) and all(isinstance(x, str) and x and not x.startswith("/") and ".." not in PurePosixPath(x).parts for x in values), "invalid non_shipping_theme_files policy")
    return {PurePosixPath(x).as_posix() for x in values}


def source_tree(base: Path) -> dict[str,str]:
    excluded = non_shipping_theme_files()
    result={}
    for p in base.rglob("*"):
        if not p.is_file():
            continue
        rel=p.relative_to(base).as_posix()
        if rel in excluded:
            continue
        result[rel]=sha256_file(p)
    return result


def check_tree_equivalence(theme: str, _: dict[str, Any]) -> None:
    dist=ROOT/"dist"/f"{theme}.zip"
    assertion(dist.is_file(), f"{theme}: dist ZIP missing")
    z=zip_tree(dist); s=source_tree(theme_root(theme))
    assertion(z==s, f"{theme}: dist ZIP tree differs from canonical theme tree: missing={sorted(set(s)-set(z))[:20]} extra={sorted(set(z)-set(s))[:20]} changed={sorted(k for k in set(s)&set(z) if s[k]!=z[k])[:20]}")


def check_dist_current(theme: str, contract: dict[str, Any]) -> None:
    check_tree_equivalence(theme, contract)


def check_screenshot(theme: str, contract: dict[str, Any]) -> None:
    path=theme_root(theme)/"screenshot.png"
    assertion(path.is_file(), f"{theme}: screenshot.png missing")
    # PNG IHDR width/height without Pillow dependency.
    data=path.read_bytes()[:24]
    assertion(data[:8]==b"\x89PNG\r\n\x1a\n" and len(data)>=24, f"{theme}: screenshot.png is not PNG")
    width=int.from_bytes(data[16:20],"big"); height=int.from_bytes(data[20:24],"big")
    expected=contract.get("screenshot", {}).get("dimensions", [1200,900])
    assertion([width,height]==expected, f"{theme}: screenshot dimensions {width}x{height}, expected {expected[0]}x{expected[1]}")


def check_package_hygiene(theme: str, _: dict[str, Any]) -> None:
    forbidden={"node_modules",".git",".pytest_cache","tmp","cache","tests","__pycache__"}
    bad=[]
    for p in theme_root(theme).rglob("*"):
        rel=p.relative_to(theme_root(theme))
        if any(part in forbidden for part in rel.parts) or (p.is_file() and p.suffix in {".map", ".log"}):
            bad.append(rel.as_posix())
    assertion(not bad, f"{theme}: forbidden package artifacts: {bad[:50]}")


def check_archive_limits(theme: str, _: dict[str, Any]) -> None:
    dist=ROOT/"dist"/f"{theme}.zip"
    cp=run([sys.executable,str(ROOT/"tools"/"theme_factory.py"),"validate",str(dist)])
    assertion(cp.returncode==0, f"{theme}: archive validation failed: {cp.stdout.decode(errors='replace')}")


def check_visual_baseline_change(theme: str, contract: dict[str, Any]) -> None:
    if contract["workflow"] != "new-theme":
        return
    baseline=theme_root(theme)/"verification"/"visual-baseline.json"
    data=load_jsonish(baseline)
    assertion(isinstance(data,dict) and data.get("approved_by") and data.get("approved_sha256"), f"{theme}: visual baseline requires approval metadata")


STATIC_HANDLERS = {
    "TF-STATIC-001": static_required_files,
    "TF-STATIC-002": static_style_header,
    "TF-STATIC-003": static_php_syntax,
    "TF-STATIC-004": static_js_syntax,
    "TF-STATIC-005": static_json,
    "TF-STATIC-006": static_theme_json,
    "TF-STATIC-007": static_block_metadata,
    "TF-STATIC-008": static_symlinks,
    "TF-STATIC-009": static_secrets,
    "TF-STATIC-010": static_local_paths,
    "TF-STATIC-011": static_legacy_runtime,
    "TF-STATIC-012": static_i18n,
    "TF-STATIC-013": static_debug_output,
    "TF-STATIC-014": static_dependency_contract,
    "TF-STATIC-015": static_source_manifest,
    "TF-STATIC-016": static_visual_baseline,
    "TF-STATIC-017": static_wp_security_patterns,
    "TF-EDITABILITY-002": static_editable_coverage,
    "TF-GB-005": static_custom_block_justification,
    "TF-CONV-005": static_source_deviations,
    "TF-VIS-001": check_visual_baseline_change,
    "TF-FSE-001": static_fse_markup,
    "TF-CLASSIC-001": static_classic_hierarchy,
    "TF-EL-007": static_elementor_contract,
    "TF-WOO-008": static_woo_overrides,
    "TF-PKG-009": check_deterministic_zip,
    "TF-PKG-010": check_tree_equivalence,
    "TF-PKG-011": check_dist_current,
    "TF-PKG-012": check_screenshot,
    "TF-PKG-013": check_package_hygiene,
    "TF-PKG-014": check_archive_limits,
    "TF-REL-002": lambda theme, contract: check_workflow_contract(),
}


def implementation_coverage() -> dict[str, Any]:
    """Prove every ledger row resolves to an executable implementation authority."""
    import importlib.util

    spec = importlib.util.spec_from_file_location("themefactory_runtime_registry", ROOT / "tests" / "runtime" / "runtime_harness.py")
    assertion(spec is not None and spec.loader is not None, "runtime harness cannot be imported")
    runtime = importlib.util.module_from_spec(spec)
    sys.modules[spec.name] = runtime
    spec.loader.exec_module(runtime)
    runtime_registry = set(runtime.TESTS)
    repo_registry = set(repo_scope_ids(include_aggregate=False))
    aggregate_registry = {"TF-META-009", "TF-REL-001", "TF-REL-003", "TF-REL-004"}
    package_existing = {f"TF-PKG-{i:03d}" for i in range(1, 9)}
    holes: list[str] = []
    authorities: Counter[str] = Counter()
    for row in load_ledger():
        tid = row["id"]
        runner = row["runner"]
        phase = row["phase"]
        scope = row.get("scope", "theme")
        if runner == "runtime":
            ok = tid in runtime_registry
            authority = "runtime"
        elif phase == "aggregate":
            ok = tid in aggregate_registry
            authority = "aggregate"
        elif scope == "repo" and phase in {"static", "release"}:
            ok = tid in repo_registry
            authority = "repo-static"
        elif scope == "theme" and phase in {"static", "release"}:
            ok = tid in STATIC_HANDLERS or tid in package_existing
            authority = "theme-static"
        else:
            ok = False
            authority = "unclassified"
        if not ok:
            holes.append(f"{tid}: no executable authority ({runner}/{phase}/{scope})")
        else:
            authorities[authority] += 1
        impl = implementation_file(str(row.get("implementation", "")))
        if impl is None or not impl.exists():
            holes.append(f"{tid}: implementation path missing: {row.get('implementation')}")
    if holes:
        fail("ledger implementation coverage incomplete:\n" + "\n".join(holes))
    return {"tests": len(load_ledger()), "authorities": dict(authorities), "holes": []}


def runtime_environment_contract() -> dict[str, str]:
    policy = load_jsonish(ROOT / "ci" / "policy.yaml")
    runtime = policy.get("runtime", {}) if isinstance(policy, dict) else {}
    keys = ["wordpress_image", "wp_cli_image", "database_image", "selenium_image", "reference_image"]
    env: dict[str, str] = {}
    for key in keys:
        ref = str(runtime.get(key, ""))
        assertion("@sha256:" in ref and len(ref.rsplit("@sha256:", 1)[1]) == 64, f"runtime image must be digest pinned: {key}={ref}")
        env[key] = ref
    return env


def check_workflow_contract() -> None:
    text = WORKFLOW.read_text()
    for host in ("debian1", "debian2", "debian3"):
        assertion(host not in text, f"workflow pins forbidden hostname {host}")
    assertion("actions/cache@" not in text, "actions/cache must not wrap ARC local storage")
    assertion("continue-on-error: true" not in text, "required workflow must not continue-on-error")
    assertion("cancel-in-progress: true" in text, "workflow must cancel superseded PR candidates")
    assertion("themefactory-ci" in text and "themefactory-runtime" in text, "workflow missing ARC scale-set labels")
    assertion("actions/upload-artifact@" not in text and "actions/download-artifact@" not in text, "blocking CI must not depend on GitHub artifact quota")
    assertion("head.repo.full_name == github.repository" in text, "workflow missing fork PR guard")
    assertion("needs: [plan" in text or "needs:\n      - plan" in text, "workflow runtime/aggregate must depend on plan")
    for job in ("plan", "global-static", "runtime-engine-selftest", "theme-verify", "full-gate"):
        count=len(re.findall(rf"^  {re.escape(job)}:\s*$", text, flags=re.M))
        assertion(count==1, f"workflow job key must appear exactly once: {job} count={count}")
    assertion(text.count("python3 tools/theme_ci.py global-static --out") == 1, "workflow duplicates repository static execution")
    assertion(text.count("python3 tests/runtime/guard_selftest.py") == 1, "workflow duplicates runtime guard execution")
    runtime_arc = (ROOT / "ci" / "arc" / "themefactory-runtime.values.yaml").read_text()
    assertion("ephemeral-storage: 8Gi" in runtime_arc and "ephemeral-storage: 20Gi" in runtime_arc, "runtime DIND ephemeral-storage request/limit missing")
    assertion("ephemeral-storage: 2Gi" in runtime_arc and runtime_arc.count("ephemeral-storage: 8Gi") >= 2, "runtime runner ephemeral-storage request/limit missing")
    safe_merge = ROOT / "tools" / "theme_safe_merge.py"
    assertion(safe_merge.is_file() and (ROOT / "tools" / "theme-safe-merge").is_file(), "fail-closed safe-merge authority missing")
    merge_text = safe_merge.read_text()
    assertion('REQUIRED_CHECK = "Full gate"' in merge_text, "safe-merge required check drifted")
    assertion('REQUIRED_BASE = "main"' in merge_text, "safe-merge base branch drifted")
    assertion('"--match-head-commit"' in merge_text, "safe-merge lacks head-race protection")
    assertion('"github-actions"' in merge_text, "safe-merge does not bind provider identity")


def check_repo(require_complete: bool = False) -> dict[str, Any]:
    summary = check_ledger(require_complete=require_complete)
    check_contracts()
    environment = runtime_environment_contract()
    coverage = implementation_coverage()
    if WORKFLOW.exists():
        check_workflow_contract()
    elif require_complete:
        fail("workflow missing")
    # Every implementation path of a complete row must exist; every required row must be selected by at least one synthetic contract.
    rows=load_ledger()
    contracts=[load_contract(t) for t in discover_themes()]
    unreachable=[]
    for row in rows:
        if not row.get("required", True): continue
        if row["id"] in META_IDS: continue
        if not any(applicable(row,c,"pr") or applicable(row,c,"scheduled") for c in contracts):
            # Future profile tests are allowed if a profile contract exists under ci/profiles.
            profiles=row.get("profiles") or []
            features=row.get("features") or []
            declared_profiles=[p for p in profiles if p!="core"] + [f.replace("_","-") for f in features]
            if not any((ROOT/"ci"/"profiles"/f"{p}.yaml").exists() for p in declared_profiles):
                unreachable.append(row["id"])
    if unreachable:
        fail(f"required tests unreachable from contracts/profiles: {unreachable}")
    return {**summary, "themes": discover_themes(), "runtime_environment": environment, "implementation_coverage": coverage}


def static_receipt(theme: str, mode: str = "pr", output: Path | None = None) -> dict[str, Any]:
    started=time.time(); p=plan(theme,mode); rows={r["id"]:r for r in load_ledger()}; results=[]
    selected=[tid for tid in p["tests"] if rows[tid]["runner"]=="static" and rows[tid]["phase"] in {"static","release"} and rows[tid].get("scope","theme")=="theme"]
    # Repo-wide meta checks are executed once per theme receipt but are cheap and deterministic; CI normally runs static only for changed themes.
    for tid in selected:
        t0=time.time(); status="passed"; error=None
        try:
            if tid in META_IDS:
                if tid in {"TF-META-001"}: load_contract(theme)
                elif tid in {"TF-META-002"}: check_ledger()
                elif tid in {"TF-META-003"}: plan(theme,mode)
                elif tid in {"TF-META-004","TF-META-013"}: check_contracts()
                elif tid in {"TF-META-005","TF-META-006","TF-META-007","TF-META-010","TF-META-011","TF-META-012"}: check_workflow_contract()
                elif tid=="TF-META-008": build_shards(theme,mode)
                elif tid in {"TF-META-009","TF-META-014"}: pass
            elif tid.startswith("TF-PKG-00") and tid in {f"TF-PKG-{i:03d}" for i in range(1,9)}:
                # Existing unit tests own these; static receipt trusts only the pytest result injected by CI, not a repository claim.
                pass
            else:
                handler=STATIC_HANDLERS.get(tid)
                if handler is None:
                    fail(f"no static handler for {tid}")
                handler(theme, load_contract(theme))
        except Exception as exc:
            status="failed"; error=str(exc)
        results.append({"id":tid,"status":status,"duration_ms":round((time.time()-t0)*1000,2),"error":error})
    receipt={
        "schema":1,"theme":theme,"runner":"static","shard_id":"static","tree":p["tree"],"contract_sha256":p["contract_sha256"],
        "ledger_sha256":p["ledger_sha256"],"expected_ids":selected,"executed_ids":[r["id"] for r in results],"results":results,
        "started_at":started,"duration_ms":round((time.time()-started)*1000,2),"status":"passed" if all(r["status"]=="passed" for r in results) else "failed"
    }
    if output:
        output.parent.mkdir(parents=True,exist_ok=True); output.write_text(json.dumps(receipt,indent=2)+"\n")
    if receipt["status"]!="passed":
        failures=[r for r in results if r["status"]!="passed"]
        fail("static verification failed:\n"+"\n".join(f"{r['id']}: {r['error']}" for r in failures))
    return receipt


def repo_scope_ids(*, include_aggregate: bool = False) -> list[str]:
    rows=load_ledger()
    phases={"static","release","aggregate"} if include_aggregate else {"static","release"}
    return [r["id"] for r in rows if r.get("scope","theme")=="repo" and r["phase"] in phases]


def changed_themes(base: str, head: str = "HEAD") -> dict[str, Any]:
    cp=subprocess.run(["git","diff","--name-only",f"{base}...{head}"],cwd=ROOT,text=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE,check=False)
    if cp.returncode:
        fail(f"git diff failed: {cp.stderr}")
    themes=set(); files=[x for x in cp.stdout.splitlines() if x.strip()]
    known=set(discover_themes())
    infra=False
    for name in files:
        parts=PurePosixPath(name).parts
        if len(parts)>=2 and parts[0]=="themes" and parts[1] in known:
            # First-engine bootstrap may add only the contract; do not pretend unchanged theme source was modified.
            if len(parts)==3 and parts[2]=="theme-test.yaml":
                continue
            themes.add(parts[1])
        elif len(parts)==2 and parts[0]=="dist" and parts[1].endswith(".zip") and parts[1][:-4] in known:
            themes.add(parts[1][:-4])
        elif parts and parts[0] in {"ci","tests","tools",".github"}:
            infra=True
    return {"themes":sorted(themes,key=str.casefold),"files":files,"infra_changed":infra}


def ci_plan(base: str, head: str = "HEAD", mode: str = "pr", all_themes: bool = False) -> dict[str, Any]:
    change=changed_themes(base,head) if not all_themes else {"themes":discover_themes(),"files":[],"infra_changed":True}
    theme_plans=[]; static_matrix=[]; runtime_matrix=[]
    for theme in change["themes"]:
        p=plan(theme,mode)
        theme_plans.append(p)
        static_matrix.append({"theme":theme})
        for shard in build_shards(theme,mode):
            runtime_matrix.append({"theme":theme,"shard_id":shard["shard_id"],"tests":json.dumps(shard["tests"],separators=(",",":")),"tree":p["tree"]})
    runtime_infra = any(
      name == "tests/runtime/runtime_harness.py" or name == "tests/runtime/guard_selftest.py" or name.startswith("ci/runtime/") or name == "ci/policy.yaml"
      for name in change.get("files",[])
    )
    return {
      "schema":1,"base":base,"head":head,"mode":mode,"changes":change,"runtime_infra_changed":runtime_infra,
      "themes":change["themes"],"theme_plans":theme_plans,
      "static_matrix":static_matrix,"runtime_matrix":runtime_matrix,"aggregate_matrix":[{"theme":t} for t in change["themes"]],"theme_matrix":[{"theme":t} for t in change["themes"]],
      "run_runtime":bool(runtime_matrix),"run_theme_static":bool(static_matrix),
      "global_ids":repo_scope_ids(include_aggregate=False),
    }


def global_static_receipt(output: Path) -> dict[str, Any]:
    started=time.time(); rows={r["id"]:r for r in load_ledger()}; ids=repo_scope_ids(include_aggregate=False); results=[]
    pytest_cache: tuple[bool,str] | None=None
    def unit_suite() -> tuple[bool,str]:
        nonlocal pytest_cache
        if pytest_cache is None:
            cp=subprocess.run([sys.executable,"-m","pytest","-q","tests/test_theme_factory.py","tests/verification"],cwd=ROOT,text=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,check=False)
            pytest_cache=(cp.returncode==0,cp.stdout[-12000:])
        return pytest_cache
    for tid in ids:
        t0=time.time(); status="passed"; error=None; detail={}
        try:
            if tid.startswith("TF-PKG-00") and int(tid.split("-")[-1])<=8:
                ok,log=unit_suite(); detail={"pytest_tail":log}; assertion(ok,"package/unit verification suite failed")
            elif tid=="TF-META-001": detail=check_contracts()
            elif tid=="TF-META-002": detail=check_ledger()
            elif tid=="TF-META-003": detail={t:plan(t)["expected"] for t in discover_themes()}
            elif tid=="TF-META-004": detail=check_repo()
            elif tid in {"TF-META-005","TF-META-006","TF-META-007","TF-META-010","TF-META-011","TF-META-012","TF-REL-002"}: check_workflow_contract()
            elif tid=="TF-META-008": detail={t:len(build_shards(t)) for t in discover_themes()}
            elif tid=="TF-META-013": detail=check_contracts()
            elif tid=="TF-META-014":
                bad=[r["id"] for r in rows.values() if r.get("proof_required") and r.get("status")=="complete" and not r.get("red_proof")]
                assertion(not bad,f"completed proof-required tests missing RED proof: {bad}")
            else: fail(f"unimplemented repo-scope test {tid}")
        except Exception as exc:
            status="failed"; error=str(exc)
        results.append({"id":tid,"status":status,"duration_ms":round((time.time()-t0)*1000,2),"detail":detail,"error":error})
    receipt={"schema":1,"theme":"*","scope":"repo","runner":"static","shard_id":"repo-static","expected_ids":ids,"executed_ids":[r["id"] for r in results],"results":results,"duration_ms":round((time.time()-started)*1000,2),"status":"passed" if all(r["status"]=="passed" for r in results) else "failed"}
    output.parent.mkdir(parents=True,exist_ok=True); output.write_text(json.dumps(receipt,indent=2)+"\n")
    if receipt["status"]!="passed": fail("repo static verification failed: "+", ".join(r["id"] for r in results if r["status"]!="passed"))
    return receipt


def aggregate(theme: str, plan_path: Path, receipts_dir: Path, output: Path) -> dict[str, Any]:
    p=load_jsonish(plan_path); assertion(p.get("theme")==theme,"aggregate theme mismatch")
    rows={r["id"]:r for r in load_ledger()}
    theme_ids={tid for tid in p["tests"] if rows[tid].get("scope","theme")!="repo"}
    aggregate_ids={tid for tid in theme_ids if rows[tid]["phase"]=="aggregate"}
    expected=theme_ids-aggregate_ids
    receipt_paths=sorted(receipts_dir.rglob("*.json")); assertion(receipt_paths,"no receipts found")
    receipts=[]
    for path in receipt_paths:
        try: r=load_jsonish(path)
        except VerificationError: continue
        if not isinstance(r,dict) or "expected_ids" not in r or "results" not in r: continue
        if r.get("theme") not in {theme,"*"}: continue
        receipts.append(r)
    assertion(receipts,"no applicable receipts found")
    observed={}; shards=[]; artifact={}; environments=[]
    for receipt in receipts:
        if receipt.get("theme")==theme:
            assertion(receipt.get("tree") in {None,"",p.get("tree")},f"receipt tree mismatch: {receipt.get('shard_id')}")
        sid=receipt.get("shard_id"); assertion(sid and sid not in shards,f"duplicate/missing shard id: {sid}"); shards.append(sid)
        expected_ids=list(receipt.get("expected_ids",[])); executed_ids=list(receipt.get("executed_ids",[])); assertion(len(executed_ids)==len(set(executed_ids)),f"receipt {sid} duplicate IDs"); assertion(set(executed_ids)==set(expected_ids),f"receipt {sid} expected/executed mismatch")
        raw=list(receipt.get("results",[])); assertion({x.get('id') for x in raw}==set(executed_ids),f"receipt {sid} raw IDs mismatch")
        for result in raw:
            tid=result["id"]
            if tid not in expected: continue
            assertion(tid not in observed,f"test claimed by multiple shards: {tid}"); observed[tid]=result
        if receipt.get("artifact",{}).get("sha256"): artifact=receipt["artifact"]
        if receipt.get("theme")==theme and receipt.get("environment"):
            environments.append(receipt["environment"])
    missing=sorted(expected-set(observed)); extra=sorted(set(observed)-expected); assertion(not extra,f"unexpected observed IDs: {extra}")
    environment={}
    if environments:
        canon={json.dumps(x,sort_keys=True,separators=(",",":")) for x in environments}
        assertion(len(canon)==1,"runtime receipt environment mismatch across shards")
        environment=environments[0]
    failed=sorted(t for t,r in observed.items() if r.get("status")!="passed")
    aggregate_results=[]
    for tid in sorted(aggregate_ids):
        status="passed"; error=None
        try:
            if tid in {"TF-META-009","TF-REL-003","TF-REL-004"}: assertion(not missing and not failed,f"required evidence incomplete/failed missing={missing} failed={failed}")
            elif tid=="TF-REL-001":
                assertion(p.get("tree") and len(p["tree"])==64,"tree identity missing")
                assertion(p.get("contract_sha256") and p.get("ledger_sha256"),"contract/check identity missing")
                assertion(bool(artifact),"artifact digest missing from runtime evidence")
                assertion(bool(environment),"runtime environment identity missing from runtime evidence")
                assertion(environment.get("images")==runtime_environment_contract(),"runtime environment does not match pinned policy")
            else: fail(f"unknown aggregate test {tid}")
        except Exception as exc: status="failed"; error=str(exc)
        aggregate_results.append({"id":tid,"status":status,"error":error})
    aggregate_failed=[r["id"] for r in aggregate_results if r["status"]!="passed"]
    all_failed=failed+aggregate_failed
    result={"schema":1,"theme":theme,"tree":p["tree"],"contract_sha256":p.get("contract_sha256"),"ledger_sha256":p.get("ledger_sha256"),"profiles":p.get("profiles",[]),"expected":len(theme_ids),"executed":len(observed)+len(aggregate_results),"repo_scope_excluded":len(set(p["tests"])-theme_ids),"notStarted":len(missing),"passed":len(theme_ids)-len(all_failed)-len(missing),"failed":len(all_failed),"failed_ids":all_failed,"missing_ids":missing,"shards":sorted(shards),"artifact":artifact,"environment":environment,"aggregate_results":aggregate_results,"result":"PASS" if not all_failed and not missing else "FAIL"}
    output.parent.mkdir(parents=True,exist_ok=True); output.write_text(json.dumps(result,indent=2)+"\n")
    if result["result"]!="PASS": fail(f"aggregate failed: failed={all_failed}, missing={missing}")
    return result

def verify_theme(theme: str, mode: str = "pr", workdir: Path | None = None, engine: str | None = None) -> dict[str, Any]:
    """Run the exact selected per-theme gate locally or inside one ARC runtime job."""
    engine = engine or os.environ.get("THEMEFACTORY_CONTAINER_ENGINE", "docker")
    base = workdir or (ROOT / ".theme-ci" / theme)
    base = base.resolve()
    if base.exists():
        shutil.rmtree(base)
    receipts = base / "receipts"
    receipts.mkdir(parents=True, exist_ok=True)
    plan_path = base / "plan.json"
    shards_path = base / "shards.json"
    final_path = base / "final.json"
    p = plan(theme, mode)
    emit_json(p, plan_path)
    static_receipt(theme, mode, receipts / f"{theme}-static.json")
    shards = build_shards(theme, mode)
    emit_json(shards, shards_path)
    failed_processes: list[str] = []
    harness = ROOT / "tests" / "runtime" / "runtime_harness.py"
    for shard in shards:
        sid = shard["shard_id"]
        out = receipts / f"{theme}-{sid}.json"
        env = os.environ.copy()
        env["THEMEFACTORY_SHARD_ID"] = sid
        env["THEMEFACTORY_TREE"] = p["tree"]
        env["THEMEFACTORY_CONTAINER_ENGINE"] = engine
        cp = subprocess.run(
            [sys.executable, str(harness), "--theme", theme, "--tests", json.dumps(shard["tests"], separators=(",", ":")), "--out", str(out), "--engine", engine],
            cwd=ROOT, env=env, check=False
        )
        if cp.returncode != 0:
            failed_processes.append(sid)
    try:
        result = aggregate(theme, plan_path, receipts, final_path)
    except VerificationError as exc:
        if failed_processes:
            raise VerificationError(f"{exc}; runtime shard process failures={failed_processes}") from exc
        raise
    if failed_processes:
        fail(f"runtime shard process failures despite passing aggregate: {failed_processes}")
    return result


def emit_json(value: Any, path: Path | None = None) -> None:
    text=json.dumps(value,indent=2,sort_keys=True)+"\n"
    if path:
        path.parent.mkdir(parents=True,exist_ok=True); path.write_text(text)
    else:
        sys.stdout.write(text)


def main() -> int:
    parser=argparse.ArgumentParser(description="ThemeFactory verification planner and static/aggregate authority")
    sub=parser.add_subparsers(dest="cmd",required=True)
    p=sub.add_parser("check-ledger"); p.add_argument("--require-complete",action="store_true")
    sub.add_parser("check-contracts")
    p=sub.add_parser("check-repo"); p.add_argument("--require-complete",action="store_true")
    p=sub.add_parser("plan"); p.add_argument("theme"); p.add_argument("--mode",choices=["pr","release","scheduled"],default="pr"); p.add_argument("--out",type=Path)
    p=sub.add_parser("shards"); p.add_argument("theme"); p.add_argument("--mode",choices=["pr","release","scheduled"],default="pr"); p.add_argument("--max-shards",type=int); p.add_argument("--out",type=Path)
    p=sub.add_parser("static"); p.add_argument("theme"); p.add_argument("--mode",choices=["pr","release","scheduled"],default="pr"); p.add_argument("--out",type=Path,required=True)
    p=sub.add_parser("changed-themes"); p.add_argument("--base",required=True); p.add_argument("--head",default="HEAD")
    p=sub.add_parser("ci-plan"); p.add_argument("--base",required=True); p.add_argument("--head",default="HEAD"); p.add_argument("--mode",choices=["pr","release","scheduled"],default="pr"); p.add_argument("--all-themes",action="store_true"); p.add_argument("--out",type=Path)
    p=sub.add_parser("global-static"); p.add_argument("--out",type=Path,required=True)
    p=sub.add_parser("aggregate"); p.add_argument("theme"); p.add_argument("--plan",type=Path,required=True); p.add_argument("--receipts",type=Path,required=True); p.add_argument("--out",type=Path,required=True)
    p=sub.add_parser("verify"); p.add_argument("theme"); p.add_argument("--mode",choices=["pr","release","scheduled"],default="pr"); p.add_argument("--workdir",type=Path); p.add_argument("--engine")
    args=parser.parse_args()
    if args.cmd=="check-ledger": emit_json(check_ledger(require_complete=args.require_complete))
    elif args.cmd=="check-contracts": emit_json(check_contracts())
    elif args.cmd=="check-repo": emit_json(check_repo(require_complete=args.require_complete))
    elif args.cmd=="plan": emit_json(plan(args.theme,args.mode),args.out)
    elif args.cmd=="shards": emit_json(build_shards(args.theme,args.mode,args.max_shards),args.out)
    elif args.cmd=="static": emit_json(static_receipt(args.theme,args.mode,args.out))
    elif args.cmd=="changed-themes": emit_json(changed_themes(args.base,args.head))
    elif args.cmd=="ci-plan": emit_json(ci_plan(args.base,args.head,args.mode,args.all_themes),args.out)
    elif args.cmd=="global-static": emit_json(global_static_receipt(args.out))
    elif args.cmd=="aggregate": emit_json(aggregate(args.theme,args.plan,args.receipts,args.out))
    elif args.cmd=="verify": emit_json(verify_theme(args.theme,args.mode,args.workdir,args.engine))
    return 0

if __name__=="__main__":
    try:
        raise SystemExit(main())
    except VerificationError as exc:
        print(f"theme-ci: {exc}",file=sys.stderr)
        raise SystemExit(1)
