# prevent/contract.py
#!/usr/bin/env python3
"""contract.py — the frozen Finding/Report JSON shape every detector emits (SARIF-aligned).
audience: AI coding agents first. The runner speaks ONLY this JSON; every Python detector imports emit()
to print it. This file is the SINGLE source of truth for the finding shape AND status derivation —
no-false-clean lives here: a non-empty `unresolved` ALWAYS degrades status, so a partial scan can never
serialize as a clean 'ok'. Do NOT change this shape lightly; it is the one-way-door contract."""
import json, sys

LEVELS = ("error", "warning", "note")  # SARIF levels: error=blockable(precise); warning=surfaced; note=info

def finding(rule_id, level, cls, message, file, line=0, symbol=""):
    if level not in LEVELS:  # raise, not assert — must survive `python3 -O` (asserts stripped)
        raise ValueError(f"bad level {level!r} (must be one of {LEVELS})")
    return {"ruleId": rule_id, "level": level, "class": cls,
            "message": message, "file": file, "line": line, "symbol": symbol}

def emit(detector, findings, scanned, unresolved=(), status=None):
    """Print the contract JSON to stdout. `status` auto-degrades to 'degraded' whenever `unresolved`
    is non-empty (no-false-clean) unless an explicit status (e.g. 'error') is passed by the caller."""
    if status is None:
        status = "degraded" if unresolved else "ok"
    json.dump({"detector": detector, "status": status, "findings": list(findings),
               "coverage": {"scanned": list(scanned), "unresolved": list(unresolved)}}, sys.stdout)
    sys.stdout.write("\n")

RUNGS = ("syntactic-local", "located-suggestion")  # auto-applyable vs agent/human-applied (ARCHITECTURE §gated HARDER)

def resolution(adapter, cls, rung, location, suggestion, patch=None, status=None, unresolved=()):
    """Build the validated Resolution dict a SolutionAdapter emits (mirrors finding(): returns a dict, never prints).
    no-false-clean + resolution-gated-HARDER live HERE:
      - rung must be a known RUNG (raise, not assert — survives `python3 -O`);
      - a non-empty `unresolved` auto-degrades status (a partial/uncertain fix can never serialize 'ok');
      - a `patch` is legal ONLY on rung=='syntactic-local' AND status=='ok'. A located-suggestion or a degraded
        resolution that carried a patch would be a silent rewrite of insecure code under uncertainty — worse than
        no fix. The emitter refuses it (defense in depth; the runner/ledger also gate auto-apply)."""
    if rung not in RUNGS:
        raise ValueError(f"bad rung {rung!r} (must be one of {RUNGS})")
    if status is None:
        status = "degraded" if unresolved else "ok"
    if patch is not None and (rung != "syntactic-local" or status != "ok"):
        raise ValueError("patch is legal ONLY on rung=='syntactic-local' AND status=='ok' "
                         "(auto-apply gated HARDER; semantic/uncertain fixes emit a located suggestion, never a patch)")
    return {"adapter": adapter, "class": cls, "rung": rung, "status": status,
            "location": location, "suggestion": suggestion, "patch": patch,
            "coverage": {"unresolved": list(unresolved)}}
