#!/usr/bin/env python3
"""Merge the per-deliverable sanity results and the lint result into a verdict.

Usage:
  sanity-merge <manifest.json> <sanity bead> <dev bead> <sprint>
    --reviewer sanity-llm|sanity-jev --started-at <epoch> --completed-at <epoch> < results.json
    --reviewer sanity-selected --started-at <epoch> --completed-at <epoch> --llm-vars <file>
    --jev-vars <file> --selection <file>

`manifest.json` is what `sanity-split` printed. stdin is a JSON array holding
the unchanged reply of every child of one reviewer (LLM or JEV), one per
deliverable: the parsed envelope, or the fenced JSON reply text as a string. Run once per reviewer against the same manifest/lint. New manifests
require explicit reviewer and its dispatch start and completion times; old manifests retain the
legacy invocation contract. New reviewer reports include shared run_id, commit,
reviewer, UTC timing and raw reviewer_results. Cannot-run emits CANNOT_RUN JSON
with null findings and the original error (exit 1/3); exit 4 has no report yet.
On success stdout is the vars object for `dev-sanity-complete.md.j2`
(generated_at, task_id, checked_bead, sprint, branch, commit, verdict,
findings_count, findings, findings_md, lint_md). `findings` preserves the
structured, per-finding evidence used to create child fix beads after FAIL.

For `--reviewer sanity-jev` every reply is screened first: a success without a valid receipt from the Jev client
(`data.jev`, verified with TYPESAFE_API_KEY by `--client`, default .claude/skills/atm-bd-orchestration/scripts/jev_client.py; a split
request's reply carries every part with its own receipt), whose findings contradict its result (the parts' common
choice, or dev-sanity's triaged `data.jev_final` when they disagree) or that reuses another deliverable's receipt,
and a SANITY.JEV_* failure whose message is not the client's own text, becomes a SANITY.RESULT_INVALID failure for
its deliverable; `rejected_results` keeps the replaced reply and reason. The selected merge rejects such a reply outright.

Before any verdict the worktree named in the manifest must still be at the
pinned SHA, on the pinned branch, with a clean tree, so lint and the checkers
saw the same tree.

Exit codes (the message is printed on stderr):
  0  usable verdict printed
  1  malformed or mismatched result set; the message names the deliverable
     (or the result index when the result carries no deliverable number)
  3  stdout is `<code> recoverable|fatal <deliverable>`; deliverable 0 is the
     task itself: SANITY.COMMIT_MISMATCH / SANITY.TARGET_UNREADABLE (worktree
     moved or unreadable) or SANITY.LINT_UNAVAILABLE (lint timed out,
     was cancelled or could not run). A number >= 1 is one checker's own
     complete failure envelope, echoed
  4  lint still running (no exit file yet)
"""
import argparse
from datetime import datetime, timezone
import hashlib
import importlib.util
import json
import math
import os
from pathlib import Path
import re
import subprocess
import sys

sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "atm-beads" / "scripts"))
from sanity_shared import valid_elapsed_s  # noqa: E402

# Tool lock and log files a clean worktree may hold; the same as `IGNORED_STATUS_PATHS` in assignment-gates.py.
IGNORED_STATUS_PATHS = (".beads.gate.lock", ".sc-compose/")
# The Jev transport, the sibling jev_client.py; it alone writes and verifies receipts.
CLIENT = Path(__file__).resolve().parent / "jev_client.py"

CHECKER_KIND = "skipped"
RUSTC = re.compile(r"^\s*-->\s+(?P<file>[^:\s]+):(?P<line>\d+):\d+", re.M)
RUSTFMT = re.compile(r"^Diff in (?P<file>[^:\s]+)(?: at line |:)(?P<line>\d+)", re.M)
FENCED_REPLY = re.compile(r"\s*```(?:json)?[ \t]*\n(.*?)\n```\s*", re.S)


class Reject(Exception):
    def __init__(self, message, code=1):
        super().__init__(message)
        self.code = code


def load_manifest(path):
    try:
        manifest = json.loads(Path(path).read_text())
    except (OSError, ValueError) as exc:
        raise Reject(f"cannot read manifest: {exc}")
    for key in ("sha", "deliverables_total", "branch", "lint"):
        if key not in manifest:
            raise Reject(f"manifest lacks {key}")
    if not isinstance(manifest["lint"], dict):
        raise Reject("manifest has no lint entry; sanity-split ran with --split-only")
    return manifest


def read_results():
    try:
        results = json.load(sys.stdin)
    except ValueError as exc:
        raise Reject(f"stdin is not JSON: {exc}")
    if not isinstance(results, list):
        raise Reject("stdin must be a JSON array of results")
    return [parse_reply(result, index) for index, result in enumerate(results)]


def parse_reply(reply, index):
    """A fenced JSON reply string becomes its envelope; any other value is kept as is."""
    if not isinstance(reply, str):
        return reply
    match = FENCED_REPLY.fullmatch(reply)
    try:
        return json.loads(match.group(1) if match else reply)
    except ValueError as exc:
        raise Reject(f"result {index}: reply is not fenced JSON: {exc}")


def failure_envelope(result, index, total):
    """A complete failure envelope, or Reject (exit 1) when `success` is false but the envelope is not."""
    error = result.get("error")
    if result.get("data") is not None or not isinstance(error, dict):
        raise Reject(f"result {index}: failure envelope must have data null and an error object")
    number = error.get("deliverable")
    if (not isinstance(error.get("code"), str) or not error["code"] or not isinstance(error.get("message"), str)
            or not isinstance(error.get("recoverable"), bool) or not isinstance(error.get("suggested_action"), str)):
        raise Reject(f"result {index}: failure envelope must have code, message, recoverable, suggested_action")
    if not isinstance(number, int) or isinstance(number, bool) or not 1 <= number <= total:
        raise Reject(f"result {index}: failure envelope must name its deliverable (1..{total}), got {number!r}")
    return error


def validate(result, index, manifest, sanity_bead, dev_bead):
    total = manifest["deliverables_total"]
    where = f"result {index}"
    if not isinstance(result.get("data"), dict):
        raise Reject(f"{where}: a success envelope needs a data object")
    data = result["data"]
    number = data.get("deliverable")
    if not isinstance(number, int) or isinstance(number, bool) or not 1 <= number <= total:
        raise Reject(f"{where}: deliverable must be an integer in 1..{total}, got {number!r}")
    where = f"deliverable {number}"
    if result.get("error") is not None:
        raise Reject(f"{where}: a success envelope must have error null")
    if data.get("sanity_bead") != sanity_bead:
        raise Reject(f"{where}: sanity_bead {data.get('sanity_bead')!r} is not {sanity_bead}")
    if data.get("dev_bead") != dev_bead:
        raise Reject(f"{where}: dev_bead {data.get('dev_bead')!r} is not {dev_bead}")
    if data.get("commit_checked") != manifest["sha"]:
        raise Reject(f"{where}: commit_checked {data.get('commit_checked')!r} is not {manifest['sha']}")
    findings = data.get("findings")
    if not isinstance(findings, list):
        raise Reject(f"{where}: findings must be a list")
    if len(findings) > 1:
        raise Reject(f"{where}: at most one finding is allowed")
    for finding in findings:
        if (not isinstance(finding, dict) or finding.get("kind") != CHECKER_KIND
                or not isinstance(finding.get("file"), str) or not finding["file"]
                or not isinstance(finding.get("line"), int) or isinstance(finding.get("line"), bool)
                or finding["line"] < 1 or not isinstance(finding.get("issue"), str) or not finding["issue"]):
            raise Reject(f"{where}: finding must be {{kind: skipped, file, line >= 1, issue}}: {finding!r}")
        dependencies = finding.get("depends_on", [])
        if not isinstance(dependencies, list):
            raise Reject(f"{where}: depends_on must be a list")
        for dependency in dependencies:
            if (not isinstance(dependency, dict)
                    or not isinstance(dependency.get("deliverable"), int)
                    or isinstance(dependency["deliverable"], bool)
                    or not 1 <= dependency["deliverable"] <= total
                    or not isinstance(dependency.get("file"), str) or not dependency["file"]
                    or not isinstance(dependency.get("line"), int)
                    or isinstance(dependency["line"], bool) or dependency["line"] < 1):
                raise Reject(f"{where}: depends_on entries must be {{deliverable: 1..{total}, file, line >= 1}}: {dependency!r}")
    return number, findings


def reply_status(result, index, manifest, sanity_bead, dev_bead):
    """Validate one unchanged reply and return its deliverable and coarse status."""
    if (not isinstance(result, dict) or set(result) not in ({"success", "data", "error"}, {"success", "data", "error", "elapsed_s"})
            or not isinstance(result.get("success"), bool)):
        raise Reject(f"result {index}: not a success or failure envelope")
    if "elapsed_s" in result and not valid_elapsed_s(result["elapsed_s"]):
        raise Reject(f"result {index}: elapsed_s must be a finite nonnegative number")
    if result["success"] is False:
        error = failure_envelope(result, index, manifest["deliverables_total"])
        return error["deliverable"], "cannot_run"
    number, findings = validate(result, index, manifest, sanity_bead, dev_bead)
    return number, "undone" if findings else "done"


def canonical_sha256(value):
    return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":")).encode()).hexdigest()


def load_reviewer_vars(path, reviewer, manifest):
    try:
        value = json.loads(Path(path).read_text())
    except (OSError, ValueError) as exc:
        raise Reject(f"cannot read reviewer vars {path}: {exc}")
    if not isinstance(value, dict):
        raise Reject(f"reviewer vars {path} must be a JSON object")
    if value.get("run_id") != manifest.get("run_id"):
        raise Reject(f"reviewer vars {path} run_id does not match manifest")
    if value.get("reviewer") != reviewer:
        raise Reject(f"reviewer vars {path} reviewer is not {reviewer}")
    results = value.get("reviewer_results")
    if not isinstance(results, list):
        raise Reject(f"reviewer vars {path} lacks reviewer_results array")
    digest = canonical_sha256(results)
    if value.get("reviewer_results_sha256") != digest:
        raise Reject(f"reviewer vars {path} reviewer_results_sha256 does not match reviewer_results")
    return results, digest


def load_client(path):
    """The Jev client module, or the reason it cannot be loaded."""
    try:
        spec = importlib.util.spec_from_file_location("jev_client", path)
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
        return module, None
    except (OSError, ImportError, AttributeError, SyntaxError) as exc:
        return None, f"cannot load the Jev client {path}: {exc}"


def jev_reply_problem(result, client, seen):
    """Why a sanity-jev reply cannot stand, or None. A success needs the client's receipt in `data.jev` (verified with
    TYPESAFE_API_KEY), or for a split request every part in order with its own verified receipt; findings that agree
    with the result (no: exactly one; yes: none), which is the parts' common choice or, when they disagree, the
    `data.jev_final` dev-sanity recorded after triage citing every part; and requests no other deliverable's receipt
    used. A SANITY.JEV_* failure needs the client's own message verbatim.
    Anything not an envelope is left to the envelope checks."""
    if not isinstance(result, dict) or not isinstance(result.get("success"), bool):
        return None
    client, load_error = client
    if result["success"] is False:
        error = result.get("error")
        code = error.get("code") if isinstance(error, dict) else None
        if not isinstance(code, str) or not code.startswith("SANITY.JEV_"):
            return None
        if load_error:
            return load_error
        return None if client.is_client_message(error.get("message")) else \
            f"Jev failure message is not the client's own text: {error.get('message')!r}"
    data = result.get("data")
    if not isinstance(data, dict):
        return None
    if load_error:
        return load_error
    key = os.environ.get("TYPESAFE_API_KEY", "").strip()
    if not key:
        return "TYPESAFE_API_KEY is not set, so the Jev receipt cannot be verified"
    jev, final = data.get("jev"), data.get("jev_final")
    if isinstance(jev, list):  # a split request: every part's result, in order, each with its own receipt
        receipts = []
        for i, part in enumerate(jev, 1):
            label = f"{i} of {len(jev)}"
            if not isinstance(part, dict) or part.get("part") != label:
                return f"Jev part {label} is missing or mislabelled: a split reply returns every part in order"
            receipt = part.get("receipt")
            if not client.verify_receipt(receipt, key):
                return f"no valid Jev receipt on part {label}: the reply was not written from a jev_client.py call"
            response = part.get("response")
            answer = response.get("answers", {}).get("written") if isinstance(response, dict) else None
            if not isinstance(answer, dict) or (answer.get("choice"), answer.get("probabilities")) != (
                    receipt["choice"], receipt["probabilities"]):
                return f"Jev part {label}'s response does not match its receipt"
            receipts.append(receipt)
        if not receipts:
            return "no valid Jev receipt in data.jev: the part list is empty"
    else:
        if not client.verify_receipt(jev, key):
            return "no valid Jev receipt in data.jev: the reply was not written from a jev_client.py call"
        receipts = [jev]
    for receipt in receipts:
        if receipt["question"] != "written" or receipt["choice"] not in ("yes", "no"):
            return f"Jev receipt answers {receipt['question']!r} with {receipt['choice']!r}, not written: yes or no"
    choices = {receipt["choice"] for receipt in receipts}
    if len(choices) == 1:
        (choice,) = choices
        if final is not None and (not isinstance(final, dict) or final.get("choice") != choice):
            return f"data.jev_final contradicts every Jev part's choice {choice!r}"
    else:
        if not (isinstance(final, dict) and final.get("choice") in ("yes", "no")
                and isinstance(final.get("reason"), str) and final["reason"].strip()
                and final.get("parts") == [part["part"] for part in jev]):
            return ("Jev parts disagree and data.jev_final does not record dev-sanity's triaged result "
                    "({choice, reason, parts} citing every part)")
        choice = final["choice"]
    findings = data.get("findings")
    if not isinstance(findings, list) or len(findings) != (1 if choice == "no" else 0):
        return f"Jev choice {choice!r} contradicts the reply's findings"
    if any(receipt["request_sha256"] in seen for receipt in receipts):
        return "Jev receipt reused from another deliverable's request"
    seen.update(receipt["request_sha256"] for receipt in receipts)
    return None


def screen_jev_results(results, client):
    """Replace each sanity-jev reply that cannot stand with a coordinator-origin SANITY.RESULT_INVALID failure for its
    deliverable, so selection takes the LLM reply; the replaced replies are kept, with the reason, in `rejected`."""
    seen, screened, rejected = set(), [], []
    for result in results:
        reason = jev_reply_problem(result, client, seen)
        source = (result.get("data") if result.get("success") else result.get("error")) if reason else None
        number = source.get("deliverable") if isinstance(source, dict) else None
        if reason is None or not isinstance(number, int) or isinstance(number, bool):
            screened.append(result)
            continue
        screened.append({"success": False, "data": None, "error": {
            "code": "SANITY.RESULT_INVALID", "message": reason, "recoverable": True,
            "suggested_action": "rerun the sc-sanity-jev child; selection takes the LLM reply meanwhile",
            "deliverable": number}})
        rejected.append({"deliverable": number, "reason": reason, "reply": result})
    return screened, rejected


def context_path_exists(manifest, path):
    """Require re-run context to be a file in the pinned manifest tree; a `path:start-end` entry's lines must lie in it."""
    worktree = manifest.get("worktree_path")
    if not isinstance(worktree, str) or not worktree:
        return False
    lines = re.fullmatch(r"(?P<path>.+):(?P<start>[0-9]+)-(?P<end>[0-9]+)", path)
    target = f"{manifest['sha']}:{lines['path'] if lines else path}"
    def git(*args):
        return subprocess.run(["git", "-C", worktree, *args], capture_output=True, text=True).stdout
    if git("cat-file", "-t", target).strip() != "blob":
        return False
    return not lines or 1 <= int(lines["start"]) <= int(lines["end"]) <= len(git("show", target).splitlines())


def selected_results(args, manifest, sanity_bead, dev_bead):
    """Choose whole validated replies; selection metadata is never a reply edit."""
    llm, llm_sha256 = load_reviewer_vars(args.llm_vars, "sanity-llm", manifest)
    jev, jev_sha256 = load_reviewer_vars(args.jev_vars, "sanity-jev", manifest)
    try:
        selection = json.loads(Path(args.selection).read_text())
    except (OSError, ValueError) as exc:
        raise Reject(f"cannot read selection: {exc}")
    if not isinstance(selection, list):
        raise Reject("selection must be a JSON array")
    args.parsed_selection = selection

    def index_replies(rows, name):
        indexed = {}
        for index, reply in enumerate(rows):
            number, status = reply_status(reply, index, manifest, sanity_bead, dev_bead)
            if number in indexed:
                raise Reject(f"deliverable {number}: more than one {name} result")
            indexed[number] = (reply, status)
        for number in range(1, manifest["deliverables_total"] + 1):
            if number not in indexed:
                raise Reject(f"deliverable {number}: no {name} result")
        return indexed

    client, jev_seen = load_client(getattr(args, "client", None) or CLIENT), set()
    for row in jev:
        if (reason := jev_reply_problem(row, client, jev_seen)):
            raise Reject(f"sanity-jev reply: {reason}")
    llm_by, jev_by = index_replies(llm, "llm"), index_replies(jev, "jev")
    selected, seen, sources = [], set(), {}
    required = {"deliverable", "llm", "jev", "selected", "reason", "rerun", "llm_sha256", "jev_sha256"}
    for entry in selection:
        if not isinstance(entry, dict) or set(entry) != required:
            raise Reject("selection entry has incorrect fields")
        number = entry.get("deliverable")
        if not isinstance(number, int) or isinstance(number, bool) or not 1 <= number <= manifest["deliverables_total"] or number in seen:
            raise Reject(f"deliverable {number!r}: selection must appear once")
        seen.add(number)
        if entry["llm_sha256"] != llm_sha256 or entry["jev_sha256"] != jev_sha256:
            raise Reject(f"deliverable {number}: selection hashes do not match reviewer results")
        if entry["llm"] != llm_by[number][1] or entry["jev"] != jev_by[number][1]:
            raise Reject(f"deliverable {number}: selection status does not match reply")
        if entry["selected"] not in {"llm", "jev", "rerun"} or not isinstance(entry["reason"], str):
            raise Reject(f"deliverable {number}: invalid selection or reason")
        if not entry["reason"] and (entry["llm"] != entry["jev"] or entry["selected"] == "rerun"):
            raise Reject(f"deliverable {number}: reason required for disagreement or rerun")
        rerun = entry["rerun"]
        if (entry["selected"] == "rerun") != (rerun is not None):
            raise Reject(f"deliverable {number}: rerun must match selected")
        if rerun is not None:
            if not isinstance(rerun, dict) or set(rerun) != {"reviewer", "context", "reply"}:
                raise Reject(f"deliverable {number}: invalid rerun")
            if rerun["reviewer"] not in {"sanity-llm", "sanity-jev"} or not isinstance(rerun["context"], list) or not rerun["context"] or not all(isinstance(path, str) and path and not Path(path).is_absolute() for path in rerun["context"]):
                raise Reject(f"deliverable {number}: rerun requires reviewer and nonempty repo-relative context")
            for path in rerun["context"]:
                if not context_path_exists(manifest, path):
                    raise Reject(f"deliverable {number}: rerun context path is not a file at manifest sha: {path}")
            reply = parse_reply(rerun["reply"], number)
            if rerun["reviewer"] == "sanity-jev" and (reason := jev_reply_problem(reply, client, set(jev_seen))):
                raise Reject(f"deliverable {number}: rerun reply: {reason}")
            reply_number, status = reply_status(reply, number, manifest, sanity_bead, dev_bead)
            if reply_number != number:
                raise Reject(f"deliverable {number}: rerun reply names another deliverable")
            source = rerun["reviewer"]
        elif entry["selected"] == "llm":
            reply, status, source = *llm_by[number], "sanity-llm"
        else:
            reply, status, source = *jev_by[number], "sanity-jev"
        if rerun is None and status == "cannot_run" and "cannot_run" != (llm_by if entry["selected"] == "jev" else jev_by)[number][1]:
            raise Reject(f"deliverable {number}: selection must take the reviewer with a valid reply")
        if rerun is not None and status == "cannot_run" and "cannot_run" != entry["llm"] or \
                rerun is not None and status == "cannot_run" and "cannot_run" != entry["jev"]:
            raise Reject(f"deliverable {number}: a failed rerun cannot replace a valid original reply")
        sources[number] = source
        selected.append(reply)
    missing = [number for number in range(1, manifest["deliverables_total"] + 1) if number not in seen]
    if missing:
        raise Reject(f"deliverable {missing[0]}: missing selection")
    return selected, selection, sources


class TaskFailure(Exception):
    """A task-level failure: printed as `<code> fatal 0`, exit 3."""


def verify_worktree(manifest):
    worktree = manifest.get("worktree_path") or ""
    if not Path(worktree).is_dir():
        raise TaskFailure("SANITY.TARGET_UNREADABLE fatal 0", f"worktree {worktree!r} is not a directory")
    def git(*argv):
        run = subprocess.run(["git", "-C", worktree, *argv], capture_output=True, text=True)
        if run.returncode != 0:
            raise TaskFailure("SANITY.TARGET_UNREADABLE fatal 0", f"git {' '.join(argv)} failed in {worktree}")
        return run.stdout.strip()
    head, branch, status = git("rev-parse", "HEAD"), git("rev-parse", "--abbrev-ref", "HEAD"), git("status", "--porcelain")
    if head != manifest["sha"]:
        raise TaskFailure("SANITY.COMMIT_MISMATCH fatal 0", f"HEAD is {head}, not {manifest['sha']}")
    if branch != manifest["branch"]:
        raise TaskFailure("SANITY.COMMIT_MISMATCH fatal 0", f"checked-out branch is {branch}, not {manifest['branch']}")
    if any(line and not any(path in line for path in IGNORED_STATUS_PATHS) for line in status.splitlines()):
        raise TaskFailure("SANITY.COMMIT_MISMATCH fatal 0", "worktree has uncommitted or untracked changes")


def lint_result(manifest):
    lint = manifest["lint"]
    exit_file = Path(lint["exit_file"])
    if not exit_file.exists():
        raise Reject("lint still running", code=4)
    state = exit_file.read_text().strip()
    if state in ("timeout", "cancelled", "error"):
        raise TaskFailure("SANITY.LINT_UNAVAILABLE fatal 0", f"lint {state}; see {lint['log']}")
    try:
        exit_code = int(state)
    except ValueError:
        raise Reject(f"lint exit file {exit_file} is not an integer or timeout/cancelled/error")
    try:
        log = Path(lint["log"]).read_text(errors="replace")
    except OSError:
        log = ""
    findings = []
    if exit_code != 0:
        worktree = (manifest.get("worktree_path") or "").rstrip("/") + "/"
        seen = set()
        for match in list(RUSTC.finditer(log)) + list(RUSTFMT.finditer(log)):
            path = match.group("file")
            if worktree != "/" and path.startswith(worktree):
                path = path[len(worktree):]
            key = (path, int(match.group("line")))
            if key not in seen:
                seen.add(key)
                findings.append({"kind": "lint", "file": path, "line": key[1],
                                 "issue": f"`{lint['command']}` reported a diagnostic here"})
        if not findings:
            findings.append({"kind": "lint", "file": "(lint)", "line": 0,
                             "issue": f"`{lint['command']}` exited {exit_code} without locatable diagnostics; see {lint['log']}"})
    return exit_code, findings, log


def fmt(finding):
    return f"- `{finding['file']}:{finding['line']}` {finding['kind']}: {finding['issue']}"


def deliverable_text(manifest, number):
    for assignment in manifest.get("assignments", []):
        if assignment.get("number") == number:
            deliverable = assignment.get("assignment", {}).get("deliverable", {})
            text = deliverable.get("text")
            if isinstance(text, str) and text:
                return text
    return f"Deliverable {number}"


def main(argv):
    parser = argparse.ArgumentParser(description=__doc__)
    for name in ("manifest_path", "sanity_bead", "dev_bead", "sprint"):
        parser.add_argument(name)
    parser.add_argument("--reviewer", choices=("sanity-llm", "sanity-jev", "sanity-selected"))
    parser.add_argument("--started-at", type=float)
    parser.add_argument("--completed-at", type=float)
    parser.add_argument("--llm-vars")
    parser.add_argument("--jev-vars")
    parser.add_argument("--selection")
    parser.add_argument("--client", help="the Jev client that verifies sanity-jev receipts (default .claude/skills/atm-bd-orchestration/scripts/jev_client.py)")
    try:
        args = parser.parse_args(argv[1:])
    except SystemExit:
        return 1
    manifest_path, sanity_bead, dev_bead, sprint = args.manifest_path, args.sanity_bead, args.dev_bead, args.sprint
    manifest = None
    results, selection, selection_sources, rejected = [], None, {}, []

    def metadata():
        if not args.reviewer:
            return {}
        completed = datetime.fromtimestamp(args.completed_at, timezone.utc)
        return {"run_id": manifest["run_id"], "reviewer": args.reviewer,
                "operational_reviewer": manifest["operational_reviewer"],
                "started_at": datetime.fromtimestamp(args.started_at, timezone.utc).isoformat().replace("+00:00", "Z"),
                "completed_at": completed.isoformat().replace("+00:00", "Z"),
                "duration_seconds": round(args.completed_at - args.started_at),
                "reviewer_results": results,
                "reviewer_results_sha256": canonical_sha256(results),
                "rejected_results": rejected,
                "selection": selection}

    def unavailable(code, message, legacy_status=None):
        if args.reviewer and manifest and manifest.get("run_id") and args.started_at is not None and args.completed_at is not None:
            cannot_run_selection = selection
            if args.reviewer == "sanity-selected" and cannot_run_selection is None:
                cannot_run_selection = getattr(args, "parsed_selection", [])
            payload = {**metadata(), "task_id": sanity_bead, "checked_bead": dev_bead,
                       "sprint": sprint, "branch": manifest["branch"], "commit": manifest["sha"],
                       "verdict": "CANNOT_RUN", "findings_count": None,
                       "error": {"code": code, "message": message}}
            if args.reviewer == "sanity-selected":
                payload["selection"] = cannot_run_selection
            json.dump(payload, sys.stdout, indent=2)
            print()
        else:
            print(legacy_status or code)

    try:
        manifest = load_manifest(manifest_path)
        if manifest.get("run_id") and not args.reviewer:
            raise Reject("a run manifest requires --reviewer, --started-at, and --completed-at")
        if args.reviewer and (args.reviewer not in manifest.get("reviewers", []) or args.started_at is None or args.completed_at is None):
            print("reviewer must be selected in manifest and --started-at/--completed-at are required", file=sys.stderr)
            return 1
        if args.reviewer:
            if not math.isfinite(args.started_at) or not math.isfinite(args.completed_at):
                print("--started-at and --completed-at must be finite epoch values", file=sys.stderr)
                return 1
            if args.completed_at < args.started_at:
                print("--completed-at must not precede --started-at", file=sys.stderr)
                return 1
            if args.completed_at > datetime.now(timezone.utc).timestamp() + 5:
                print("--completed-at must not be more than five seconds in the future", file=sys.stderr)
                return 1
        selected_mode = args.reviewer == "sanity-selected"
        if selected_mode:
            if not all((args.llm_vars, args.jev_vars, args.selection)):
                raise Reject("sanity-selected requires --llm-vars, --jev-vars, and --selection")
            results, selection, selection_sources = selected_results(args, manifest, sanity_bead, dev_bead)
        else:
            if any((args.llm_vars, args.jev_vars, args.selection)):
                raise Reject("selection inputs are only valid for sanity-selected")
            results = read_results()
            if args.reviewer == "sanity-jev":
                results, rejected = screen_jev_results(results, load_client(args.client or CLIENT))
        by_number = {}
        for index, result in enumerate(results):
            if (not isinstance(result, dict) or not isinstance(result.get("success"), bool)
                    or any(key not in result for key in ("success", "data", "error"))):
                raise Reject(f"result {index}: not a success or failure envelope "
                             "(success must be true or false; success, data and error must all be present)")
            if "elapsed_s" in result and not valid_elapsed_s(result["elapsed_s"]):
                raise Reject(f"result {index}: elapsed_s must be a finite nonnegative number")
            if result["success"] is False:
                envelope = failure_envelope(result, index, manifest["deliverables_total"])
                unavailable(envelope["code"], envelope["message"],
                            f"{envelope['code']} {'recoverable' if envelope['recoverable'] else 'fatal'} {envelope['deliverable']}")
                print(f"deliverable {envelope['deliverable']}: {envelope['message']}", file=sys.stderr)
                return 3
            number, findings = validate(result, index, manifest, sanity_bead, dev_bead)
            if number in by_number:
                raise Reject(f"deliverable {number}: more than one result")
            by_number[number] = findings
        missing = [n for n in range(1, manifest["deliverables_total"] + 1) if n not in by_number]
        if missing:
            raise Reject(f"deliverable {missing[0]}: no result" + (f" (also {missing[1:]})" if missing[1:] else ""))
        lint_exit, lint_findings, _ = lint_result(manifest)
        verify_worktree(manifest)
    except Reject as exc:
        print(str(exc), file=sys.stderr)
        if exc.code != 4 and args.reviewer:
            unavailable("SANITY.RESULT_INVALID", str(exc))
        return exc.code
    except TaskFailure as exc:
        unavailable(exc.args[0].split()[0], exc.args[1], exc.args[0])
        print(exc.args[1], file=sys.stderr)
        return 3

    report_findings = by_number
    blocks = []
    finding_records = []
    for number in sorted(report_findings):
        findings = report_findings[number]
        blocks.append(
            f"D{number}: done" if not findings
            else f"D{number}:\n" + "\n".join(fmt(f) for f in findings)
        )
        for index, finding in enumerate(findings, 1):
            finding_records.append({
                "finding_ref": f"D{number}-F{index}", "deliverable": number,
                "kind": finding["kind"], "file": finding["file"],
                "line": finding["line"], "issue": finding["issue"],
                "depends_on": finding.get("depends_on", []),
                "deliverable_text": deliverable_text(manifest, number),
            })
            if selection is not None:
                finding_records[-1]["reviewer"] = "sc-" + selection_sources[number]
    for index, finding in enumerate(lint_findings, 1):
        finding_records.append({
            "finding_ref": f"L-F{index}", "deliverable": 0,
            "kind": finding["kind"], "file": finding["file"],
            "line": finding["line"], "issue": finding["issue"],
            "depends_on": [],
        })
    count = len(finding_records)
    lint_md = f"`{manifest['lint']['command']}` exit {lint_exit}"
    if lint_findings:
        lint_md += "\n" + "\n".join(fmt(f) for f in lint_findings)
    json.dump({
        **metadata(),
        "generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
        "task_id": sanity_bead, "checked_bead": dev_bead, "sprint": sprint,
        "branch": manifest["branch"], "commit": manifest["sha"],
        "verdict": "PASS" if all(not findings for findings in report_findings.values()) and lint_exit == 0 else "FAIL",
        "findings_count": count, "findings": finding_records, "selection": selection,
        "findings_md": "\n".join(blocks), "lint_md": lint_md,
    }, sys.stdout, indent=2)
    print()
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))
