                os.unlink(tmp_path)
            except Exception:
                pass
        txt = (p.stdout or "").strip()
        if not txt:
            err = f"no_output rc={p.returncode} stderr={(p.stderr or '').strip()}"
            return (False, err)
        try:
            obj = json.loads(txt)
        except Exception:
            err = f"bad_json rc={p.returncode} stderr={(p.stderr or '').strip()} out={txt}"
            return (False, err)
        if bool(obj.get("ok")):
            return True, obj.get("out")
        return (False, str(obj.get('err')))
    except subprocess.TimeoutExpired:
        return False, "timeout"
    except Exception as e:
        return False, str(e)

def _values_equal(a: Any, b: Any) -> bool:
    # Numeric tolerance for floats
    try:
        import numbers as _nums  # type: ignore
        if isinstance(a, _nums.Number) and isinstance(b, _nums.Number):
            # Use isclose for floats; ints will compare exactly with isclose as well
            return math.isclose(float(a), float(b), rel_tol=1e-9, abs_tol=1e-9)
    except Exception:
        pass
    # Sequences
    if isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)):
        if len(a) != len(b):
            return False
        return all(_values_equal(x, y) for x, y in zip(a, b))
    # Dicts
    if isinstance(a, dict) and isinstance(b, dict):
        if set(a.keys()) != set(b.keys()):
            return False
        return all(_values_equal(a[k], b[k]) for k in a.keys())
    return a == b

def run_and_test(code: str, spec: Dict[str, Any], ex: Any | None = None) -> Tuple[bool, Dict[str, Any]]:
    g: Dict[str, Any] = _safe_builtins()
    loc: Dict[str, Any] = {}
    try:
        # Normalize incoming code: accept either full def solve(...) or body-only
        txt = code.strip()
        # Strip accidental code fences via shared helper
        blk = extract_first_fenced_block(txt)
        if blk is not None:
            txt = blk.strip()
        # Per-test timeout using tiny subprocess per test case
        try:
            test_timeout = float(os.environ.get("SELFPLAY_TEST_TIMEOUT_SEC", "2.5") or 2.5)
        except Exception:
            test_timeout = 2.5
        passed, total = 0, len(spec["tests"])
        timeouts = 0
        first_failure: Any | None = None
        first_mismatch: Dict[str, Any] | None = None


    # Numeric tolerance for floats
    try:
        import numbers as _nums  # type: ignore
        if isinstance(a, _nums.Number) and isinstance(b, _nums.Number):
            # Use isclose for floats; ints will compare exactly with isclose as well
            return math.isclose(float(a), float(b), rel_tol=1e-9, abs_tol=1e-9)
    except Exception:
        pass
    # Sequences
    if isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)):
        if len(a) != len(b):
            return False
        return all(_values_equal(x, y) for x, y in zip(a, b))
    # Dicts
    if isinstance(a, dict) and isinstance(b, dict):
        if set(a.keys()) != set(b.keys()):
            return False
        return all(_values_equal(a[k], b[k]) for k in a.keys())
    return a == b

def run_and_test(code: str, spec: Dict[str, Any], ex: Any | None = None) -> Tuple[bool, Dict[str, Any]]:
    g: Dict[str, Any] = _safe_builtins()
    loc: Dict[str, Any] = {}
    try:
        # Normalize incoming code: accept either full def solve(...) or body-only
        txt = code.strip()
        # Strip accidental code fences via shared helper
        blk = extract_first_fenced_block(txt)
        if blk is not None:
            txt = blk.strip()
        # Per-test timeout using tiny subprocess per test case
        try:
            test_timeout = float(os.environ.get("SELFPLAY_TEST_TIMEOUT_SEC", "2.5") or 2.5)
        except Exception:
            test_timeout = 2.5
        passed, total = 0, len(spec["tests"])
        timeouts = 0
        first_failure: Any | None = None
        first_mismatch: Dict[str, Any] | None = None
        for tcase in spec["tests"]:
            ok_case, out_val = _worker_run_one_subproc(txt, spec["signature"], tcase["inp"], timeout_sec=test_timeout)
            if (not ok_case) and out_val == "timeout":
                timeouts += 1
            if ok_case and _values_equal(out_val, tcase["out"]):
                passed += 1
            else:
                # Record first exception or first mismatch for healing context
                if (not ok_case) and first_failure is None:
                    first_failure = out_val
                if ok_case and first_mismatch is None:
                    try:
                        first_mismatch = {"inp": tcase.get("inp"), "want": tcase.get("out"), "got": out_val}
                    except Exception:
                        first_mismatch = None
        ok = passed == total
        rep: Dict[str, Any] = {"passed": passed, "total": total, "timeouts": timeouts}
        if first_failure is not None:
            rep["first_failure"] = first_failure
        if first_mismatch is not None:
            rep["first_mismatch"] = first_mismatch
        return ok, rep
    except Exception as e:
        return False, {"err": str(e)}

def run_subset_tests(code: str, spec: Dict[str, Any], k: int = 3) -> Tuple[float, Dict[str, Any]]:
    """Run a quick subset of tests (first k) to cheaply screen candidates.

    Returns (pass_ratio_on_subset, rep_dict_like_full_runner).
    """
    g: Dict[str, Any] = _safe_builtins()
    loc: Dict[str, Any] = {}
    try:
        # Normalize incoming code similar to run_and_test
        txt = code.strip()
        blk2 = extract_first_fenced_block(txt)
        if blk2 is not None:
            txt = blk2.strip()
        try:
            test_timeout = float(os.environ.get("SELFPLAY_TEST_TIMEOUT_SEC", "2.5") or 2.5)
        except Exception:
            test_timeout = 2.5
        try:


            else:
                # Record first exception or first mismatch for healing context
                if (not ok_case) and first_failure is None:
                    first_failure = out_val
                if ok_case and first_mismatch is None:
                    try:
                        first_mismatch = {"inp": tcase.get("inp"), "want": tcase.get("out"), "got": out_val}
                    except Exception:
                        first_mismatch = None
        ok = passed == total
        rep: Dict[str, Any] = {"passed": passed, "total": total, "timeouts": timeouts}
        if first_failure is not None:
            rep["first_failure"] = first_failure
        if first_mismatch is not None:
            rep["first_mismatch"] = first_mismatch
        return ok, rep
    except Exception as e:
        return False, {"err": str(e)}

def run_subset_tests(code: str, spec: Dict[str, Any], k: int = 3) -> Tuple[float, Dict[str, Any]]:
    """Run a quick subset of tests (first k) to cheaply screen candidates.

    Returns (pass_ratio_on_subset, rep_dict_like_full_runner).
    """
    g: Dict[str, Any] = _safe_builtins()
    loc: Dict[str, Any] = {}
    try:
        # Normalize incoming code similar to run_and_test
        txt = code.strip()
        blk2 = extract_first_fenced_block(txt)
        if blk2 is not None:
            txt = blk2.strip()
        try:
            test_timeout = float(os.environ.get("SELFPLAY_TEST_TIMEOUT_SEC", "2.5") or 2.5)
        except Exception:
            test_timeout = 2.5
        try:
            subset_k = max(1, int(k))
        except Exception:
            subset_k = max(1, int(os.environ.get("SELFPLAY_PRECHECK_K", "3") or 3))
        tests = spec.get("tests", [])
        # Deterministic prefix subset for stability
        sub = tests[: min(len(tests), subset_k)]
        passed, total = 0, len(sub)
        timeouts = 0
        first_failure: Any | None = None
        for tcase in sub:
            ok_case, out_val = _worker_run_one_subproc(txt, spec["signature"], tcase["inp"], timeout_sec=test_timeout)
            if (not ok_case) and out_val == "timeout":
                timeouts += 1
            if ok_case and _values_equal(out_val, tcase["out"]):
                passed += 1
            elif (not ok_case) and first_failure is None:
                first_failure = out_val
        rep: Dict[str, Any] = {"passed": passed, "total": total, "timeouts": timeouts}
        if first_failure is not None:
            rep["first_failure"] = first_failure
        pr = (float(passed) / float(max(1, total))) if total > 0 else 0.0
        return pr, rep
    except Exception as e:
        return 0.0, {"err": str(e), "passed": 0, "total": 0, "timeouts": 0}


from __future__ import annotations

from pathlib import Path
from typing import Any, Optional
import os

def _find_repo_root(anchor: Optional[Path] = None) -> Path:
    p = (anchor or Path(__file__)).resolve()
    for parent in [p] + list(p.parents):
        try:
            if parent.name == "agi_dw":
                return parent
        except Exception:
            continue
    # Fallback heuristics: go up 3 levels from modules/, else 2
    try:
        return p.parents[3]
    except Exception:
        return p.parents[2]

def get_data_root(cfg: Optional[dict[str, Any]] = None, anchor: Optional[Path] = None) -> Path:
    # Env has highest priority
    v = os.environ.get("SELFPLAY_DATA_ROOT")
    if v and str(v).strip():
        return Path(str(v)).resolve()
    # Config override
    if cfg is not None:
        try:
            v2 = cfg.get("data_root")
            if v2:
                return Path(str(v2)).resolve()
        except Exception:
            pass
    # Default to repo_root/data
    return _find_repo_root(anchor) / "data"

def data_path(*parts: str, cfg: Optional[dict[str, Any]] = None, anchor: Optional[Path] = None) -> Path:
    base = get_data_root(cfg=cfg, anchor=anchor)
    try:
        return base.joinpath(*parts)
    except Exception:
        # Safe fallback to manual join
        p = base
        for s in parts:
            p = p / str(s)
        return p


from __future__ import annotations

from pathlib import Path
from typing import Any, Optional
import os

def _find_repo_root(anchor: Optional[Path] = None) -> Path:
    p = (anchor or Path(__file__)).resolve()
    for parent in [p] + list(p.parents):
        try:
            if parent.name == "agi_dw":
                return parent
        except Exception:
            continue
    # Fallback heuristics: go up 3 levels from modules/, else 2
    try:
        return p.parents[3]
    except Exception:
        return p.parents[2]

def get_data_root(cfg: Optional[dict[str, Any]] = None, anchor: Optional[Path] = None) -> Path:
    # Env has highest priority
    v = os.environ.get("SELFPLAY_DATA_ROOT")
    if v and str(v).strip():
        return Path(str(v)).resolve()
    # Config override
    if cfg is not None:
        try:
            v2 = cfg.get("data_root")
            if v2:
                return Path(str(v2)).resolve()
        except Exception:
            pass
    # Default to repo_root/data
    return _find_repo_root(anchor) / "data"

def data_path(*parts: str, cfg: Optional[dict[str, Any]] = None, anchor: Optional[Path] = None) -> Path:


from pathlib import Path
from typing import Any, Optional
import os

def _find_repo_root(anchor: Optional[Path] = None) -> Path:
    p = (anchor or Path(__file__)).resolve()
    for parent in [p] + list(p.parents):
        try:
            if parent.name == "agi_dw":
                return parent
        except Exception:
            continue
    # Fallback heuristics: go up 3 levels from modules/, else 2
    try:
        return p.parents[3]
    except Exception:
        return p.parents[2]

def get_data_root(cfg: Optional[dict[str, Any]] = None, anchor: Optional[Path] = None) -> Path:
    # Env has highest priority
    v = os.environ.get("SELFPLAY_DATA_ROOT")
    if v and str(v).strip():
        return Path(str(v)).resolve()
    # Config override
    if cfg is not None:
        try:
            v2 = cfg.get("data_root")
            if v2:
                return Path(str(v2)).resolve()
        except Exception:
            pass
    # Default to repo_root/data
    return _find_repo_root(anchor) / "data"

def data_path(*parts: str, cfg: Optional[dict[str, Any]] = None, anchor: Optional[Path] = None) -> Path:
    base = get_data_root(cfg=cfg, anchor=anchor)
    try:
        return base.joinpath(*parts)
    except Exception:
        # Safe fallback to manual join
        p = base
        for s in parts:
            p = p / str(s)
        return p


        return p.parents[2]

def get_data_root(cfg: Optional[dict[str, Any]] = None, anchor: Optional[Path] = None) -> Path:
    # Env has highest priority
    v = os.environ.get("SELFPLAY_DATA_ROOT")
    if v and str(v).strip():
        return Path(str(v)).resolve()
    # Config override
    if cfg is not None:
        try:
            v2 = cfg.get("data_root")
            if v2:
                return Path(str(v2)).resolve()
        except Exception:
            pass
    # Default to repo_root/data
    return _find_repo_root(anchor) / "data"

def data_path(*parts: str, cfg: Optional[dict[str, Any]] = None, anchor: Optional[Path] = None) -> Path:
    base = get_data_root(cfg=cfg, anchor=anchor)
    try:
        return base.joinpath(*parts)
    except Exception:
        # Safe fallback to manual join
        p = base
        for s in parts:
            p = p / str(s)
        return p


from .common_imports import *
from .text_utils import extract_first_fenced_block

def _trace_io(kind: str, data: Dict[str, Any]) -> None:
    try:
        root = Path(__file__).resolve().parents[3]
        p = root / "scripts" / "data" / "traces" / ("dev_loop.jsonl" if kind == "text" else "loop_run.jsonl")
        p.parent.mkdir(parents=True, exist_ok=True)
        obj = {"kind": kind, **data}
        with p.open("a", encoding="utf-8") as f:
            f.write(json.dumps(obj, ensure_ascii=False) + "\n")
    except Exception:
        pass

class SanitizeLogits(LogitsProcessor):
    """Clamp and sanitize logits to avoid NaN/Inf during sampling on CUDA.

    Replaces NaN with a large negative and +/-Inf with finite limits, then clamp tightly
    """

    def __call__(self, input_ids: torch.LongTensor, scores: torch.Tensor) -> torch.Tensor:  # type: ignore[override]
        # Replace NaN with a large negative and +/-Inf with finite limits, then clamp tightly
        scores = torch.nan_to_num(scores, nan=-50.0, neginf=-50.0, posinf=50.0)
        # Clamp to keep logits within a safe, numerically stable range
        return scores.clamp(min=-50.0, max=50.0)

def _apply_stops(text: str, stops: Optional[List[str]]) -> str:
    if not stops:
        return text
    try:
        cut = len(text)
        for s in stops:
            if not s:
                continue
            i = text.find(s)
            if i != -1:
                cut = min(cut, i)
        return text[:cut]
    except Exception:
        return text

def beam_candidates(tok, model, prompt: str, num_beams: int, max_new_tokens: int, *, early_stopping: bool = True) -> List[str]:
    """Generate beam candidates using deterministic beam search.

    Returns list of raw decoded texts (not post-processed into code blocks).
    """
    device = next(model.parameters()).device
    enc = tok(prompt, return_tensors="pt")
    enc = {k: v.to(device) for k, v in enc.items()}
    logits_processor = LogitsProcessorList([SanitizeLogits()])
    gen_kwargs: Dict[str, Any] = {
        "do_sample": False,
        "num_beams": max(1, int(num_beams)),
        "num_return_sequences": max(1, int(num_beams)),
        "max_new_tokens": int(max_new_tokens),
        "early_stopping": bool(early_stopping),
        "eos_token_id": getattr(tok, "eos_token_id", None),
        "pad_token_id": getattr(tok, "pad_token_id", getattr(tok, "eos_token_id", None)),
        "logits_processor": logits_processor,
        "use_cache": True,
    }
    with torch.inference_mode():
        out = model.generate(**enc, **{k: v for k, v in gen_kwargs.items() if v is not None})
    in_len = int(enc["input_ids"].shape[1])
    texts = [tok.decode(o[in_len :], skip_special_tokens=True) for o in out]
    try:
        _trace_io("text", {"stage": "beam_candidates", "prompt_head": (prompt or "")[:800], "num_beams": int(num_beams), "max_new_tokens": int(max_new_tokens)})
    except Exception:
        pass
    return texts

def speculative_accept_reject(tok, big_model, draft_model, prompt: str, max_new_tokens: int = 128, alpha: int = 4) -> str:
    """Speculative decoding: draft proposes tokens; big model accepts with cheap validation.

    alpha: validate every alpha draft tokens with the big model.
    Simplified accept/reject heuristic: if big model's top-1 matches draft token(s) over the alpha window, accept; else force big model step.
    """
    device = next(big_model.parameters()).device
    enc = tok(prompt, return_tensors="pt")
    enc = {k: v.to(device) for k, v in enc.items()}
    ids = enc["input_ids"]
    out_ids = ids.clone()
    steps = 0
    draft_cache = None
    while steps < int(max_new_tokens):
        with torch.inference_mode():
            # Draft propose alpha tokens
            d_out = draft_model.generate(**enc, max_new_tokens=max(1, int(alpha)), do_sample=True, num_return_sequences=1)
            draft_new = d_out[0, ids.shape[1] :]
            # Validate with big model on the same prefix
            b_out = big_model(input_ids=enc["input_ids"], attention_mask=enc.get("attention_mask"))
            logits = b_out.logits[:, -1, :]
            next_id = int(logits.argmax(-1)[0].item())
            # Heuristic: accept first draft token if matches big's argmax; else use big's token
            if draft_new.numel() > 0 and int(draft_new[0].item()) == next_id:
                out_ids = torch.cat([out_ids, draft_new[:1].unsqueeze(0)], dim=1)
                steps += 1
            else:
                out_ids = torch.cat([out_ids, torch.tensor([[next_id]], device=out_ids.device)], dim=1)
                steps += 1
            enc["input_ids"] = out_ids
            if enc.get("attention_mask") is not None:
                am = enc["attention_mask"]
                enc["attention_mask"] = torch.cat([am, torch.ones_like(am[:, :1])], dim=1)
    text = tok.decode(out_ids[0][ids.shape[1] :], skip_special_tokens=True)
    return text

def sample(tok, model, prompt: str, n: int, max_new_tokens: int, *, temperature: Optional[float] = None, top_p: Optional[float] = None, stop: Optional[List[str]] = None) -> List[str]:
    device = next(model.parameters()).device
    enc = tok(prompt, return_tensors="pt")
    # Move full encoding to device (input_ids and attention_mask)
    enc = {k: v.to(device) for k, v in enc.items()}
    # Add logits sanitizer to guard against NaN/Inf leading to CUDA asserts
    logits_processor = LogitsProcessorList([SanitizeLogits()])
    # Sampling knobs from env/meta
    try:
        temp_env = float(os.environ.get("SAMPLING_TEMPERATURE", "0.2") or 0.2)
    except Exception:
        temp_env = 0.2
    try:
        topp_env = float(os.environ.get("SAMPLING_TOP_P", "0.9") or 0.9)
    except Exception:
        topp_env = 0.9
    # Clamp temperature to a sane floor to avoid zero-temp edge cases
    try:
        temp_env = max(0.1, float(temp_env))
    except Exception:
        temp_env = 0.8
    gen_kwargs: Dict[str, Any] = {
        "do_sample": True,
        "temperature": (float(temperature) if temperature is not None else temp_env),
        "top_p": (float(top_p) if top_p is not None else topp_env),
        "num_return_sequences": n,
        "max_new_tokens": max_new_tokens,
        "eos_token_id": getattr(tok, "eos_token_id", None),
        "pad_token_id": getattr(tok, "pad_token_id", getattr(tok, "eos_token_id", None)),
        "logits_processor": logits_processor,
        "use_cache": True,
    }
    with torch.inference_mode():
        out = model.generate(**enc, **{k: v for k, v in gen_kwargs.items() if v is not None})
    # Trace I/O
    try:
        _trace_io("code", {"stage": "sample", "prompt_head": (prompt or "")[:800], "n": int(n), "max_new_tokens": int(max_new_tokens)})
    except Exception:
        pass
    # Use input length from encoding for prompt trimming
    in_len = int(enc["input_ids"].shape[1])
    texts = [tok.decode(o[in_len :], skip_special_tokens=True) for o in out]
    # Apply stop strings to raw texts first
    texts = [_apply_stops(t, stop) for t in texts]
    codes: List[str] = []
    # Prefer exactly one fenced code block per output: extract and accept that block
    # early to capture the model's intended single solution.
    for t in texts:
        blk = extract_first_fenced_block(t)
        if blk is not None:
            codes.append(blk)
    if codes:
        return codes
    def _extract_blocks(txt: str) -> List[str]:
        blocks: List[str] = []
        # Prefer a shared fenced extraction to keep behavior consistent.
        blk = extract_first_fenced_block(txt)
        if blk is not None:
            txt = blk
        # Drop a leading language header line if present (e.g., 'python')
        try:
            _lines_probe = [ln for ln in txt.splitlines()]
            j = next((i for i, ln in enumerate(_lines_probe) if ln.strip()), None)
            if j is not None and _lines_probe[j].strip().lower() in ("python", "python3", "py"):
                txt = "\n".join(_lines_probe[j + 1 :])
        except Exception:
            pass
        # Find the line with def solve(...):
        lines = txt.splitlines()
        idx = -1
        for i, ln in enumerate(lines):
            if re.match(r"^\s*def\s+solve\s*\([^)]*\)\s*(?:->\s*[^:]+)?\s*:\s*(?:#.*)?$", ln):
                idx = i
                break
        if idx == -1:
            return blocks
        # Determine indentation of function body (next non-empty line after signature)
        sig_line = lines[idx]
        body_lines: List[str] = [sig_line]
        i = idx + 1
        # Consume indented body lines only; stop at first non-indented, non-empty top-level line
        while i < len(lines):
            ln = lines[i]
            if not ln.strip():
                body_lines.append(ln)
                i += 1
                continue
            # top-level when no leading whitespace or startswith 'def ' or 'class '
            if re.match(r"^\S", ln) or re.match(r"^def\s+|^class\s+", ln):
                break
            body_lines.append(ln)
            i += 1
        block = "\n".join(body_lines).rstrip()
        # Drop trailing main-guard if somehow included
        block = re.split(r"^\s*if\s+__name__\s*==\s*['\"]__main__['\"]\s*:\s*$", block, maxsplit=1, flags=re.MULTILINE)[0].rstrip()
        if block.strip():
            blocks.append(block)
        return blocks
    for t in texts:
        blocks = _extract_blocks(t)
        codes.extend(blocks)
        try:
            _trace_io("code", {"stage": "sample_out", "text_head": (t or "")[:800], "blocks": len(blocks)})
        except Exception:
            pass
    # Fallback: if nothing extracted, try a looser parse for any function and rename to solve
    if not codes:
        for t in texts:
            # get first fenced block or entire text
            txt = t
            if txt.strip().startswith("```"):
                try:
                    first_nl = txt.find("\n"); last_fence = txt.rfind("```")
                    if first_nl != -1 and last_fence != -1 and last_fence > first_nl:
                        txt = txt[first_nl+1:last_fence]
                except Exception:
                    pass
            m = re.search(r"^\s*def\s+([A-Za-z_]\w*)\s*\(.*\):", txt, flags=re.MULTILINE)
            if not m:
                continue
            name = m.group(1)
            blocks = _extract_blocks(txt)
            if not blocks:
                continue
            block = blocks[0]
            if name != "solve":
                block = re.sub(r"^\s*def\s+" + re.escape(name) + r"\s*\(", "def solve(", block, count=1, flags=re.MULTILINE)
            codes.append(block)
    # Final fallback: treat fenced block or raw text as body-only code
    if not codes:
        for t in texts:
            txt = t
            blk2 = extract_first_fenced_block(txt)
            if blk2 is not None:
                txt = blk2
            codes.append(txt.strip()[:2000])
    # Deduplicate by AST shape to encourage diversity
    try:
        import ast as _ast
        def _ast_key(s: str) -> str:
            try:
                t = _ast.parse(s)
                return json.dumps(ast_to_tuple(t))
            except Exception:
                return s.strip()[:80]
        def ast_to_tuple(node):
            if isinstance(node, list):
                return [ast_to_tuple(x) for x in node]
            try:
                import ast as __ast
                fields = []
                for name in getattr(node, '_fields', []):
                    val = getattr(node, name)
                    if name in ('lineno','col_offset','end_lineno','end_col_offset'):
                        continue
                    fields.append((name, ast_to_tuple(val)))
                return (node.__class__.__name__, tuple(fields))
            except Exception:
                return str(node)
        seen: set[str] = set()
        uniq: list[str] = []
        for c in codes:
            k = _ast_key(c)
            if k in seen:
                continue
            seen.add(k)
            uniq.append(c)
        codes = uniq
    except Exception:
        pass
    # Signature-primed retry: if still no def solve block, ask model to complete the signature
    if not any(re.search(r"^\s*def\s+solve\s*\([^)]*\)\s*(?:->\s*[^:]+)?\s*:\s*", c, flags=re.MULTILINE) for c in codes):
        try:
            # heuristically extract signature from prompt if present
            m = re.search(r"^\s*def\s+solve\s*\([^)]*\)\s*(?:->\s*[^:]+)?\s*:\s*", prompt, flags=re.MULTILINE)
            if m:
                sig = m.group(0)
                primed = prompt.rstrip() + "\n\nComplete the following function:\n" + sig + "\n"
                t2 = generate_text(tok, model, primed, max_new_tokens=max_new_tokens)
                blocks2 = _extract_blocks(t2)
                if blocks2:
                    codes.extend(blocks2)
                else:
                    # As a last resort, if the primed output contains a def solve, accept it
                    if re.search(r"^\s*def\s+solve\s*\([^)]*\)\s*(?:->\s*[^:]+)?\s*:\s*", t2, flags=re.MULTILINE):
                        codes.append(t2)
        except Exception:
            pass
    return codes

def _parse_first_json(text: str) -> Optional[Dict[str, Any]]:
    try:
        obj = json.loads(text)
        if isinstance(obj, dict):
            return obj
    except Exception:
        pass
    try:
        i = text.find("{")
        j = text.rfind("}")
        if i != -1 and j != -1 and j > i:
            sub = text[i:j+1]
            obj = json.loads(sub)
            if isinstance(obj, dict):
                return obj
    except Exception:
        return None
    return None

def chat_json(tok, model, messages: List[Dict[str, str]], max_new_tokens: int, *, temperature: Optional[float] = 0.2, top_p: Optional[float] = 0.9, stop: Optional[List[str]] = None, retries: int = 1) -> Dict[str, Any]:
    outs = chat_sample(tok, model, messages, n=max(1, 1 + int(retries)), max_new_tokens=max_new_tokens, temperature=temperature, top_p=top_p, stop=stop)
    for t in outs:
        obj = _parse_first_json(t.strip())
        if obj is not None:
            return obj
    # Best-effort: return empty dict if nothing parsed
    return {}

def json_from_prompt(tok, model, prompt: str, max_new_tokens: int, *, temperature: Optional[float] = 0.2, top_p: Optional[float] = 0.9, stop: Optional[List[str]] = None, retries: int = 1) -> Dict[str, Any]:
    texts = sample(tok, model, prompt, n=max(1, 1 + int(retries)), max_new_tokens=max_new_tokens, temperature=temperature, top_p=top_p, stop=stop)
    for t in texts:
        obj = _parse_first_json(t.strip())
        if obj is not None:
            return obj
    return {}

def generate_text(tok, model, prompt: str, max_new_tokens: int, *, temperature: Optional[float] = None, top_p: Optional[float] = None, stop: Optional[List[str]] = None) -> str:
    device = next(model.parameters()).device
    enc = tok(prompt, return_tensors="pt")
    enc = {k: v.to(device) for k, v in enc.items()}
    logits_processor = LogitsProcessorList([SanitizeLogits()])
    try:
        temp_env = float(os.environ.get("SAMPLING_TEMPERATURE", "0.2") or 0.2)
    except Exception:
        temp_env = 0.2
    try:
        topp_env = float(os.environ.get("SAMPLING_TOP_P", "0.9") or 0.9)
    except Exception:
        topp_env = 0.9
    try:
        temp_env = max(0.1, float(temp_env))
    except Exception:
        temp_env = 0.2
    gen_kwargs: Dict[str, Any] = {
        "do_sample": True,
        "temperature": (float(temperature) if temperature is not None else temp_env),
        "top_p": (float(top_p) if top_p is not None else topp_env),
        "num_return_sequences": 1,
        "max_new_tokens": max_new_tokens,
        "eos_token_id": getattr(tok, "eos_token_id", None),
        "pad_token_id": getattr(tok, "pad_token_id", getattr(tok, "eos_token_id", None)),
        "logits_processor": logits_processor,
        "use_cache": True,
    }
    with torch.inference_mode():
        out = model.generate(**enc, **{k: v for k, v in gen_kwargs.items() if v is not None})
    in_len = int(enc["input_ids"].shape[1])
    text = tok.decode(out[0][in_len :], skip_special_tokens=True)
    text = _apply_stops(text, stop)
    try:
        _trace_io("text", {"stage": "generate_text", "prompt_head": (prompt or "")[:800], "max_new_tokens": int(max_new_tokens), "text_head": (text or "")[:800]})
    except Exception:
        pass
    return text

def chat_sample(tok, model, messages: List[Dict[str, str]], n: int, max_new_tokens: int, *, temperature: Optional[float] = None, top_p: Optional[float] = None, stop: Optional[List[str]] = None) -> List[str]:
    # Build a chat-formatted prompt using tokenizer chat template when available
    prompt = None
    if hasattr(tok, "apply_chat_template"):
        try:
            prompt = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
        except Exception:
            prompt = None
    if prompt is None:
        parts: List[str] = []
        for m in messages:
            role = m.get("role", "user")
            content = m.get("content", "")
            parts.append(f"{role}: {content}")
        prompt = "\n".join(parts) + "\nassistant:"
    return sample(tok, model, prompt, n=n, max_new_tokens=max_new_tokens, temperature=temperature, top_p=top_p, stop=stop)


from .common_imports import *
from .text_utils import extract_first_fenced_block

def _trace_io(kind: str, data: Dict[str, Any]) -> None:
    try:
        root = Path(__file__).resolve().parents[3]
        p = root / "scripts" / "data" / "traces" / ("dev_loop.jsonl" if kind == "text" else "loop_run.jsonl")
        p.parent.mkdir(parents=True, exist_ok=True)
        obj = {"kind": kind, **data}
        with p.open("a", encoding="utf-8") as f:
            f.write(json.dumps(obj, ensure_ascii=False) + "\n")
    except Exception:
        pass

class SanitizeLogits(LogitsProcessor):
    """Clamp and sanitize logits to avoid NaN/Inf during sampling on CUDA.

    Replaces NaN with a large negative and +/-Inf with finite limits, then clamp tightly
    """

    def __call__(self, input_ids: torch.LongTensor, scores: torch.Tensor) -> torch.Tensor:  # type: ignore[override]
        # Replace NaN with a large negative and +/-Inf with finite limits, then clamp tightly
        scores = torch.nan_to_num(scores, nan=-50.0, neginf=-50.0, posinf=50.0)
        # Clamp to keep logits within a safe, numerically stable range
        return scores.clamp(min=-50.0, max=50.0)

def _apply_stops(text: str, stops: Optional[List[str]]) -> str:
    if not stops:
        return text
    try:
        cut = len(text)
        for s in stops:
            if not s:


SanitizeLogits

from .common_imports import *
from .text_utils import extract_first_fenced_block

def _trace_io(kind: str, data: Dict[str, Any]) -> None:
    try:
        root = Path(__file__).resolve().parents[3]
        p = root / "scripts" / "data" / "traces" / ("dev_loop.jsonl" if kind == "text" else "loop_run.jsonl")
        p.parent.mkdir(parents=True, exist_ok=True)
        obj = {"kind": kind, **data}
        with p.open("a", encoding="utf-8") as f:
            f.write(json.dumps(obj, ensure_ascii=False) + "\n")
    except Exception:
        pass

class SanitizeLogits(LogitsProcessor):
    """Clamp and sanitize logits to avoid NaN/Inf during sampling on CUDA.

    Replaces NaN with a large negative and +/-Inf with finite limits, then clamp tightly
    """

    def __call__(self, input_ids: torch.LongTensor, scores: torch.Tensor) -> torch.Tensor:  # type: ignore[override]
        # Replace NaN with a large negative and +/-Inf with finite limits, then clamp tightly
        scores = torch.nan_to_num(scores, nan=-50.0, neginf=-50.0, posinf=50.0)
        # Clamp to keep logits within a safe, numerically stable range
        return scores.clamp(min=-50.0, max=50.0)

def _apply_stops(text: str, stops: Optional[List[str]]) -> str:
    if not stops:
        return text
    try:
        cut = len(text)
        for s in stops:
            if not s:
                continue
            i = text.find(s)
            if i != -1:
                cut = min(cut, i)
        return text[:cut]
    except Exception:
        return text

def beam_candidates(tok, model, prompt: str, num_beams: int, max_new_tokens: int, *, early_stopping: bool = True) -> List[str]:
    """Generate beam candidates using deterministic beam search.

    Returns list of raw decoded texts (not post-processed into code blocks).


        p = root / "scripts" / "data" / "traces" / ("dev_loop.jsonl" if kind == "text" else "loop_run.jsonl")
        p.parent.mkdir(parents=True, exist_ok=True)
        obj = {"kind": kind, **data}
        with p.open("a", encoding="utf-8") as f:
            f.write(json.dumps(obj, ensure_ascii=False) + "\n")
    except Exception:
        pass

class SanitizeLogits(LogitsProcessor):
    """Clamp and sanitize logits to avoid NaN/Inf during sampling on CUDA.

    Replaces NaN with a large negative and +/-Inf with finite limits, then clamp tightly
    """

    def __call__(self, input_ids: torch.LongTensor, scores: torch.Tensor) -> torch.Tensor:  # type: ignore[override]
        # Replace NaN with a large negative and +/-Inf with finite limits, then clamp tightly
        scores = torch.nan_to_num(scores, nan=-50.0, neginf=-50.0, posinf=50.0)
        # Clamp to keep logits within a safe, numerically stable range
        return scores.clamp(min=-50.0, max=50.0)

def _apply_stops(text: str, stops: Optional[List[str]]) -> str:
    if not stops:
        return text
    try:
        cut = len(text)
        for s in stops:
            if not s:
                continue
            i = text.find(s)
            if i != -1:
                cut = min(cut, i)
        return text[:cut]
    except Exception:
        return text

def beam_candidates(tok, model, prompt: str, num_beams: int, max_new_tokens: int, *, early_stopping: bool = True) -> List[str]:
    """Generate beam candidates using deterministic beam search.

    Returns list of raw decoded texts (not post-processed into code blocks).
    """
    device = next(model.parameters()).device
    enc = tok(prompt, return_tensors="pt")
    enc = {k: v.to(device) for k, v in enc.items()}
    logits_processor = LogitsProcessorList([SanitizeLogits()])
    gen_kwargs: Dict[str, Any] = {
        "do_sample": False,
        "num_beams": max(1, int(num_beams)),
        "num_return_sequences": max(1, int(num_beams)),
        "max_new_tokens": int(max_new_tokens),
        "early_stopping": bool(early_stopping),
        "eos_token_id": getattr(tok, "eos_token_id", None),
        "pad_token_id": getattr(tok, "pad_token_id", getattr(tok, "eos_token_id", None)),
        "logits_processor": logits_processor,
        "use_cache": True,


        # Replace NaN with a large negative and +/-Inf with finite limits, then clamp tightly
        scores = torch.nan_to_num(scores, nan=-50.0, neginf=-50.0, posinf=50.0)
        # Clamp to keep logits within a safe, numerically stable range
        return scores.clamp(min=-50.0, max=50.0)

def _apply_stops(text: str, stops: Optional[List[str]]) -> str:
    if not stops:
        return text
    try:
        cut = len(text)
        for s in stops:
            if not s:
                continue
            i = text.find(s)
            if i != -1:
                cut = min(cut, i)
        return text[:cut]
    except Exception:
        return text

def beam_candidates(tok, model, prompt: str, num_beams: int, max_new_tokens: int, *, early_stopping: bool = True) -> List[str]:
    """Generate beam candidates using deterministic beam search.

    Returns list of raw decoded texts (not post-processed into code blocks).
    """
    device = next(model.parameters()).device
    enc = tok(prompt, return_tensors="pt")
    enc = {k: v.to(device) for k, v in enc.items()}
    logits_processor = LogitsProcessorList([SanitizeLogits()])
    gen_kwargs: Dict[str, Any] = {
        "do_sample": False,
        "num_beams": max(1, int(num_beams)),
        "num_return_sequences": max(1, int(num_beams)),
        "max_new_tokens": int(max_new_tokens),
        "early_stopping": bool(early_stopping),
        "eos_token_id": getattr(tok, "eos_token_id", None),
        "pad_token_id": getattr(tok, "pad_token_id", getattr(tok, "eos_token_id", None)),
        "logits_processor": logits_processor,
        "use_cache": True,
    }
    with torch.inference_mode():
        out = model.generate(**enc, **{k: v for k, v in gen_kwargs.items() if v is not None})
    in_len = int(enc["input_ids"].shape[1])
    texts = [tok.decode(o[in_len :], skip_special_tokens=True) for o in out]
    try:
        _trace_io("text", {"stage": "beam_candidates", "prompt_head": (prompt or "")[:800], "num_beams": int(num_beams), "max_new_tokens": int(max_new_tokens)})
    except Exception:
        pass
    return texts

def speculative_accept_reject(tok, big_model, draft_model, prompt: str, max_new_tokens: int = 128, alpha: int = 4) -> str:
    """Speculative decoding: draft proposes tokens; big model accepts with cheap validation.

    alpha: validate every alpha draft tokens with the big model.
    Simplified accept/reject heuristic: if big model's top-1 matches draft token(s) over the alpha window, accept; else force big model step.
    """
    device = next(big_model.parameters()).device
    enc = tok(prompt, return_tensors="pt")
    enc = {k: v.to(device) for k, v in enc.items()}
    ids = enc["input_ids"]
    out_ids = ids.clone()
    steps = 0
    draft_cache = None
    while steps < int(max_new_tokens):
        with torch.inference_mode():
            # Draft propose alpha tokens
            d_out = draft_model.generate(**enc, max_new_tokens=max(1, int(alpha)), do_sample=True, num_return_sequences=1)
            draft_new = d_out[0, ids.shape[1] :]
            # Validate with big model on the same prefix


        "do_sample": False,
        "num_beams": max(1, int(num_beams)),
        "num_return_sequences": max(1, int(num_beams)),
        "max_new_tokens": int(max_new_tokens),
        "early_stopping": bool(early_stopping),
        "eos_token_id": getattr(tok, "eos_token_id", None),
        "pad_token_id": getattr(tok, "pad_token_id", getattr(tok, "eos_token_id", None)),
        "logits_processor": logits_processor,
        "use_cache": True,
    }
    with torch.inference_mode():
        out = model.generate(**enc, **{k: v for k, v in gen_kwargs.items() if v is not None})
    in_len = int(enc["input_ids"].shape[1])
    texts = [tok.decode(o[in_len :], skip_special_tokens=True) for o in out]
    try:
        _trace_io("text", {"stage": "beam_candidates", "prompt_head": (prompt or "")[:800], "num_beams": int(num_beams), "max_new_tokens": int(max_new_tokens)})
    except Exception:
        pass
    return texts

def speculative_accept_reject(tok, big_model, draft_model, prompt: str, max_new_tokens: int = 128, alpha: int = 4) -> str:
    """Speculative decoding: draft proposes tokens; big model accepts with cheap validation.

    alpha: validate every alpha draft tokens with the big model.
    Simplified accept/reject heuristic: if big model's top-1 matches draft token(s) over the alpha window, accept; else force big model step.
    """
    device = next(big_model.parameters()).device
    enc = tok(prompt, return_tensors="pt")
    enc = {k: v.to(device) for k, v in enc.items()}
    ids = enc["input_ids"]
    out_ids = ids.clone()
    steps = 0
    draft_cache = None
    while steps < int(max_new_tokens):
        with torch.inference_mode():
            # Draft propose alpha tokens
            d_out = draft_model.generate(**enc, max_new_tokens=max(1, int(alpha)), do_sample=True, num_return_sequences=1)
            draft_new = d_out[0, ids.shape[1] :]
            # Validate with big model on the same prefix
            b_out = big_model(input_ids=enc["input_ids"], attention_mask=enc.get("attention_mask"))
            logits = b_out.logits[:, -1, :]
            next_id = int(logits.argmax(-1)[0].item())
            # Heuristic: accept first draft token if matches big's argmax; else use big's token
            if draft_new.numel() > 0 and int(draft_new[0].item()) == next_id:
                out_ids = torch.cat([out_ids, draft_new[:1].unsqueeze(0)], dim=1)
                steps += 1
            else:
                out_ids = torch.cat([out_ids, torch.tensor([[next_id]], device=out_ids.device)], dim=1)
                steps += 1
            enc["input_ids"] = out_ids
            if enc.get("attention_mask") is not None:
                am = enc["attention_mask"]
                enc["attention_mask"] = torch.cat([am, torch.ones_like(am[:, :1])], dim=1)
    text = tok.decode(out_ids[0][ids.shape[1] :], skip_special_tokens=True)
    return text

def sample(tok, model, prompt: str, n: int, max_new_tokens: int, *, temperature: Optional[float] = None, top_p: Optional[float] = None, stop: Optional[List[str]] = None) -> List[str]:
    device = next(model.parameters()).device
    enc = tok(prompt, return_tensors="pt")
    # Move full encoding to device (input_ids and attention_mask)
    enc = {k: v.to(device) for k, v in enc.items()}
    # Add logits sanitizer to guard against NaN/Inf leading to CUDA asserts
    logits_processor = LogitsProcessorList([SanitizeLogits()])
    # Sampling knobs from env/meta
    try:
        temp_env = float(os.environ.get("SAMPLING_TEMPERATURE", "0.2") or 0.2)
    except Exception:
        temp_env = 0.2
    try:
        topp_env = float(os.environ.get("SAMPLING_TOP_P", "0.9") or 0.9)
    except Exception:
        topp_env = 0.9
    # Clamp temperature to a sane floor to avoid zero-temp edge cases
    try:
        temp_env = max(0.1, float(temp_env))


sample

            d_out = draft_model.generate(**enc, max_new_tokens=max(1, int(alpha)), do_sample=True, num_return_sequences=1)
            draft_new = d_out[0, ids.shape[1] :]
            # Validate with big model on the same prefix
            b_out = big_model(input_ids=enc["input_ids"], attention_mask=enc.get("attention_mask"))
            logits = b_out.logits[:, -1, :]
            next_id = int(logits.argmax(-1)[0].item())
            # Heuristic: accept first draft token if matches big's argmax; else use big's token
            if draft_new.numel() > 0 and int(draft_new[0].item()) == next_id:
                out_ids = torch.cat([out_ids, draft_new[:1].unsqueeze(0)], dim=1)
                steps += 1
            else:
                out_ids = torch.cat([out_ids, torch.tensor([[next_id]], device=out_ids.device)], dim=1)
                steps += 1
            enc["input_ids"] = out_ids
            if enc.get("attention_mask") is not None:
                am = enc["attention_mask"]
                enc["attention_mask"] = torch.cat([am, torch.ones_like(am[:, :1])], dim=1)
    text = tok.decode(out_ids[0][ids.shape[1] :], skip_special_tokens=True)
    return text

def sample(tok, model, prompt: str, n: int, max_new_tokens: int, *, temperature: Optional[float] = None, top_p: Optional[float] = None, stop: Optional[List[str]] = None) -> List[str]:
    device = next(model.parameters()).device
    enc = tok(prompt, return_tensors="pt")
    # Move full encoding to device (input_ids and attention_mask)
    enc = {k: v.to(device) for k, v in enc.items()}
    # Add logits sanitizer to guard against NaN/Inf leading to CUDA asserts
    logits_processor = LogitsProcessorList([SanitizeLogits()])
    # Sampling knobs from env/meta
    try:
        temp_env = float(os.environ.get("SAMPLING_TEMPERATURE", "0.2") or 0.2)
    except Exception:
        temp_env = 0.2
    try:
        topp_env = float(os.environ.get("SAMPLING_TOP_P", "0.9") or 0.9)
    except Exception:
        topp_env = 0.9
    # Clamp temperature to a sane floor to avoid zero-temp edge cases
    try:
        temp_env = max(0.1, float(temp_env))
    except Exception:
        temp_env = 0.8
    gen_kwargs: Dict[str, Any] = {
        "do_sample": True,
        "temperature": (float(temperature) if temperature is not None else temp_env),
        "top_p": (float(top_p) if top_p is not None else topp_env),
        "num_return_sequences": n,
        "max_new_tokens": max_new_tokens,
        "eos_token_id": getattr(tok, "eos_token_id", None),
        "pad_token_id": getattr(tok, "pad_token_id", getattr(tok, "eos_token_id", None)),
        "logits_processor": logits_processor,
        "use_cache": True,
    }
    with torch.inference_mode():
        out = model.generate(**enc, **{k: v for k, v in gen_kwargs.items() if v is not None})
    # Trace I/O
    try:
        _trace_io("code", {"stage": "sample", "prompt_head": (prompt or "")[:800], "n": int(n), "max_new_tokens": int(max_new_tokens)})
    except Exception:
        pass
    # Use input length from encoding for prompt trimming
    in_len = int(enc["input_ids"].shape[1])
    texts = [tok.decode(o[in_len :], skip_special_tokens=True) for o in out]
    # Apply stop strings to raw texts first
    texts = [_apply_stops(t, stop) for t in texts]
    codes: List[str] = []
    # Prefer exactly one fenced code block per output: extract and accept that block
    # early to capture the model's intended single solution.
    for t in texts:
        blk = extract_first_fenced_block(t)
        if blk is not None:
            codes.append(blk)
    if codes:
        return codes
    def _extract_blocks(txt: str) -> List[str]:
        blocks: List[str] = []
        # Prefer a shared fenced extraction to keep behavior consistent.
        blk = extract_first_fenced_block(txt)
        if blk is not None:
            txt = blk
        # Drop a leading language header line if present (e.g., 'python')
        try:
            _lines_probe = [ln for ln in txt.splitlines()]
            j = next((i for i, ln in enumerate(_lines_probe) if ln.strip()), None)
            if j is not None and _lines_probe[j].strip().lower() in ("python", "python3", "py"):
                txt = "\n".join(_lines_probe[j + 1 :])
        except Exception:
            pass
        # Find the line with def solve(...):
        lines = txt.splitlines()
        idx = -1
        for i, ln in enumerate(lines):
            if re.match(r"^\s*def\s+solve\s*\([^)]*\)\s*(?:->\s*[^:]+)?\s*:\s*(?:#.*)?$", ln):
                idx = i
                break
        if idx == -1:
            return blocks
        # Determine indentation of function body (next non-empty line after signature)
        sig_line = lines[idx]
        body_lines: List[str] = [sig_line]
        i = idx + 1
        # Consume indented body lines only; stop at first non-indented, non-empty top-level line
        while i < len(lines):
            ln = lines[i]
            if not ln.strip():
                body_lines.append(ln)
                i += 1
                continue
            # top-level when no leading whitespace or startswith 'def ' or 'class '
            if re.match(r"^\S", ln) or re.match(r"^def\s+|^class\s+", ln):
                break
            body_lines.append(ln)
            i += 1
        block = "\n".join(body_lines).rstrip()
        # Drop trailing main-guard if somehow included
        block = re.split(r"^\s*if\s+__name__\s*==\s*['\"]__main__['\"]\s*:\s*$", block, maxsplit=1, flags=re.MULTILINE)[0].rstrip()
        if block.strip():
            blocks.append(block)
        return blocks
    for t in texts:
        blocks = _extract_blocks(t)
        codes.extend(blocks)
        try:
            _trace_io("code", {"stage": "sample_out", "text_head": (t or "")[:800], "blocks": len(blocks)})
        except Exception:
            pass
    # Fallback: if nothing extracted, try a looser parse for any function and rename to solve
    if not codes:
        for t in texts:
            # get first fenced block or entire text
            txt = t
            if txt.strip().startswith("```"):
                try:
                    first_nl = txt.find("\n"); last_fence = txt.rfind("```")
                    if first_nl != -1 and last_fence != -1 and last_fence > first_nl:
                        txt = txt[first_nl+1:last_fence]
                except Exception:
                    pass
            m = re.search(r"^\s*def\s+([A-Za-z_]\w*)\s*\(.*\):", txt, flags=re.MULTILINE)
            if not m:
                continue
            name = m.group(1)
            blocks = _extract_blocks(txt)
            if not blocks:
                continue
            block = blocks[0]
            if name != "solve":
                block = re.sub(r"^\s*def\s+" + re.escape(name) + r"\s*\(", "def solve(", block, count=1, flags=re.MULTILINE)
            codes.append(block)
    # Final fallback: treat fenced block or raw text as body-only code
    if not codes:
        for t in texts:
            txt = t
            blk2 = extract_first_fenced_block(txt)
            if blk2 is not None:
                txt = blk2
            codes.append(txt.strip()[:2000])
    # Deduplicate by AST shape to encourage diversity
    try:
        import ast as _ast
        def _ast_key(s: str) -> str:
            try:
                t = _ast.parse(s)
                return json.dumps(ast_to_tuple(t))
            except Exception:
                return s.strip()[:80]
        def ast_to_tuple(node):
            if isinstance(node, list):
                return [ast_to_tuple(x) for x in node]
            try:
                import ast as __ast
                fields = []
                for name in getattr(node, '_fields', []):
                    val = getattr(node, name)
                    if name in ('lineno','col_offset','end_lineno','end_col_offset'):
                        continue
                    fields.append((name, ast_to_tuple(val)))
                return (node.__class__.__name__, tuple(fields))
            except Exception:
                return str(node)
        seen: set[str] = set()
        uniq: list[str] = []
        for c in codes:
            k = _ast_key(c)
            if k in seen:
                continue
            seen.add(k)
            uniq.append(c)
        codes = uniq
    except Exception:
        pass
    # Signature-primed retry: if still no def solve block, ask model to complete the signature
    if not any(re.search(r"^\s*def\s+solve\s*\([^)]*\)\s*(?:->\s*[^:]+)?\s*:\s*", c, flags=re.MULTILINE) for c in codes):
        try:
            # heuristically extract signature from prompt if present
            m = re.search(r"^\s*def\s+solve\s*\([^)]*\)\s*(?:->\s*[^:]+)?\s*:\s*", prompt, flags=re.MULTILINE)
            if m:
                sig = m.group(0)
                primed = prompt.rstrip() + "\n\nComplete the following function:\n" + sig + "\n"
                t2 = generate_text(tok, model, primed, max_new_tokens=max_new_tokens)
                blocks2 = _extract_blocks(t2)
                if blocks2:
                    codes.extend(blocks2)
                else:
                    # As a last resort, if the primed output contains a def solve, accept it
                    if re.search(r"^\s*def\s+solve\s*\([^)]*\)\s*(?:->\s*[^:]+)?\s*:\s*", t2, flags=re.MULTILINE):
                        codes.append(t2)
        except Exception:
            pass
    return codes

def _parse_first_json(text: str) -> Optional[Dict[str, Any]]:
    try:
        obj = json.loads(text)
        if isinstance(obj, dict):
            return obj
    except Exception:
        pass
    try:
        i = text.find("{")
        j = text.rfind("}")
        if i != -1 and j != -1 and j > i:
            sub = text[i:j+1]
            obj = json.loads(sub)
            if isinstance(obj, dict):
                return obj
    except Exception:
        return None
    return None


    # Signature-primed retry: if still no def solve block, ask model to complete the signature
    if not any(re.search(r"^\s*def\s+solve\s*\([^)]*\)\s*(?:->\s*[^:]+)?\s*:\s*", c, flags=re.MULTILINE) for c in codes):
        try:
            # heuristically extract signature from prompt if present
            m = re.search(r"^\s*def\s+solve\s*\([^)]*\)\s*(?:->\s*[^:]+)?\s*:\s*", prompt, flags=re.MULTILINE)
            if m:
                sig = m.group(0)
                primed = prompt.rstrip() + "\n\nComplete the following function:\n" + sig + "\n"
                t2 = generate_text(tok, model, primed, max_new_tokens=max_new_tokens)
                blocks2 = _extract_blocks(t2)
                if blocks2:
                    codes.extend(blocks2)
                else:
                    # As a last resort, if the primed output contains a def solve, accept it
                    if re.search(r"^\s*def\s+solve\s*\([^)]*\)\s*(?:->\s*[^:]+)?\s*:\s*", t2, flags=re.MULTILINE):
                        codes.append(t2)
        except Exception:
            pass
    return codes

def _parse_first_json(text: str) -> Optional[Dict[str, Any]]:
    try:
        obj = json.loads(text)
        if isinstance(obj, dict):
            return obj
    except Exception:
        pass
    try:
        i = text.find("{")
        j = text.rfind("}")
        if i != -1 and j != -1 and j > i:
            sub = text[i:j+1]
            obj = json.loads(sub)
            if isinstance(obj, dict):
                return obj
    except Exception:
        return None
    return None

def chat_json(tok, model, messages: List[Dict[str, str]], max_new_tokens: int, *, temperature: Optional[float] = 0.2, top_p: Optional[float] = 0.9, stop: Optional[List[str]] = None, retries: int = 1) -> Dict[str, Any]:
    outs = chat_sample(tok, model, messages, n=max(1, 1 + int(retries)), max_new_tokens=max_new_tokens, temperature=temperature, top_p=top_p, stop=stop)
    for t in outs:
        obj = _parse_first_json(t.strip())
        if obj is not None:
            return obj
    # Best-effort: return empty dict if nothing parsed
    return {}

def json_from_prompt(tok, model, prompt: str, max_new_tokens: int, *, temperature: Optional[float] = 0.2, top_p: Optional[float] = 0.9, stop: Optional[List[str]] = None, retries: int = 1) -> Dict[str, Any]:
    texts = sample(tok, model, prompt, n=max(1, 1 + int(retries)), max_new_tokens=max_new_tokens, temperature=temperature, top_p=top_p, stop=stop)
    for t in texts:
        obj = _parse_first_json(t.strip())
        if obj is not None:
            return obj
    return {}

def generate_text(tok, model, prompt: str, max_new_tokens: int, *, temperature: Optional[float] = None, top_p: Optional[float] = None, stop: Optional[List[str]] = None) -> str:
    device = next(model.parameters()).device


def _parse_first_json(text: str) -> Optional[Dict[str, Any]]:
    try:
        obj = json.loads(text)
        if isinstance(obj, dict):
            return obj
    except Exception:
        pass
    try:
        i = text.find("{")
        j = text.rfind("}")
        if i != -1 and j != -1 and j > i:
            sub = text[i:j+1]
            obj = json.loads(sub)
            if isinstance(obj, dict):
                return obj
    except Exception:
        return None
    return None

def chat_json(tok, model, messages: List[Dict[str, str]], max_new_tokens: int, *, temperature: Optional[float] = 0.2, top_p: Optional[float] = 0.9, stop: Optional[List[str]] = None, retries: int = 1) -> Dict[str, Any]:
    outs = chat_sample(tok, model, messages, n=max(1, 1 + int(retries)), max_new_tokens=max_new_tokens, temperature=temperature, top_p=top_p, stop=stop)
    for t in outs:
        obj = _parse_first_json(t.strip())
        if obj is not None:
            return obj
    # Best-effort: return empty dict if nothing parsed
    return {}

def json_from_prompt(tok, model, prompt: str, max_new_tokens: int, *, temperature: Optional[float] = 0.2, top_p: Optional[float] = 0.9, stop: Optional[List[str]] = None, retries: int = 1) -> Dict[str, Any]:
    texts = sample(tok, model, prompt, n=max(1, 1 + int(retries)), max_new_tokens=max_new_tokens, temperature=temperature, top_p=top_p, stop=stop)
    for t in texts:
        obj = _parse_first_json(t.strip())
        if obj is not None:
            return obj
    return {}

def generate_text(tok, model, prompt: str, max_new_tokens: int, *, temperature: Optional[float] = None, top_p: Optional[float] = None, stop: Optional[List[str]] = None) -> str:
    device = next(model.parameters()).device
    enc = tok(prompt, return_tensors="pt")
    enc = {k: v.to(device) for k, v in enc.items()}
    logits_processor = LogitsProcessorList([SanitizeLogits()])
    try:
        temp_env = float(os.environ.get("SAMPLING_TEMPERATURE", "0.2") or 0.2)
    except Exception:
        temp_env = 0.2
    try:
        topp_env = float(os.environ.get("SAMPLING_TOP_P", "0.9") or 0.9)


        i = text.find("{")
        j = text.rfind("}")
        if i != -1 and j != -1 and j > i:
            sub = text[i:j+1]
            obj = json.loads(sub)
            if isinstance(obj, dict):
                return obj
    except Exception:
        return None
    return None

def chat_json(tok, model, messages: List[Dict[str, str]], max_new_tokens: int, *, temperature: Optional[float] = 0.2, top_p: Optional[float] = 0.9, stop: Optional[List[str]] = None, retries: int = 1) -> Dict[str, Any]:
    outs = chat_sample(tok, model, messages, n=max(1, 1 + int(retries)), max_new_tokens=max_new_tokens, temperature=temperature, top_p=top_p, stop=stop)
    for t in outs:
        obj = _parse_first_json(t.strip())
        if obj is not None:
            return obj
    # Best-effort: return empty dict if nothing parsed
    return {}

def json_from_prompt(tok, model, prompt: str, max_new_tokens: int, *, temperature: Optional[float] = 0.2, top_p: Optional[float] = 0.9, stop: Optional[List[str]] = None, retries: int = 1) -> Dict[str, Any]:
    texts = sample(tok, model, prompt, n=max(1, 1 + int(retries)), max_new_tokens=max_new_tokens, temperature=temperature, top_p=top_p, stop=stop)
    for t in texts:
        obj = _parse_first_json(t.strip())
        if obj is not None:
            return obj
    return {}

def generate_text(tok, model, prompt: str, max_new_tokens: int, *, temperature: Optional[float] = None, top_p: Optional[float] = None, stop: Optional[List[str]] = None) -> str:
    device = next(model.parameters()).device
    enc = tok(prompt, return_tensors="pt")
    enc = {k: v.to(device) for k, v in enc.items()}
    logits_processor = LogitsProcessorList([SanitizeLogits()])
    try:
        temp_env = float(os.environ.get("SAMPLING_TEMPERATURE", "0.2") or 0.2)
    except Exception:
        temp_env = 0.2
    try:
        topp_env = float(os.environ.get("SAMPLING_TOP_P", "0.9") or 0.9)
    except Exception:
        topp_env = 0.9
    try:
        temp_env = max(0.1, float(temp_env))
    except Exception:
        temp_env = 0.2
    gen_kwargs: Dict[str, Any] = {
        "do_sample": True,


        return None
    return None

def chat_json(tok, model, messages: List[Dict[str, str]], max_new_tokens: int, *, temperature: Optional[float] = 0.2, top_p: Optional[float] = 0.9, stop: Optional[List[str]] = None, retries: int = 1) -> Dict[str, Any]:
    outs = chat_sample(tok, model, messages, n=max(1, 1 + int(retries)), max_new_tokens=max_new_tokens, temperature=temperature, top_p=top_p, stop=stop)
    for t in outs:
        obj = _parse_first_json(t.strip())
        if obj is not None:
            return obj
    # Best-effort: return empty dict if nothing parsed
    return {}

def json_from_prompt(tok, model, prompt: str, max_new_tokens: int, *, temperature: Optional[float] = 0.2, top_p: Optional[float] = 0.9, stop: Optional[List[str]] = None, retries: int = 1) -> Dict[str, Any]:
    texts = sample(tok, model, prompt, n=max(1, 1 + int(retries)), max_new_tokens=max_new_tokens, temperature=temperature, top_p=top_p, stop=stop)
    for t in texts:
        obj = _parse_first_json(t.strip())
        if obj is not None:
            return obj
    return {}

def generate_text(tok, model, prompt: str, max_new_tokens: int, *, temperature: Optional[float] = None, top_p: Optional[float] = None, stop: Optional[List[str]] = None) -> str:
    device = next(model.parameters()).device
    enc = tok(prompt, return_tensors="pt")
    enc = {k: v.to(device) for k, v in enc.items()}
    logits_processor = LogitsProcessorList([SanitizeLogits()])
    try:
        temp_env = float(os.environ.get("SAMPLING_TEMPERATURE", "0.2") or 0.2)
    except Exception:
        temp_env = 0.2
    try:
        topp_env = float(os.environ.get("SAMPLING_TOP_P", "0.9") or 0.9)
    except Exception:
        topp_env = 0.9
    try:
        temp_env = max(0.1, float(temp_env))
    except Exception:
        temp_env = 0.2
    gen_kwargs: Dict[str, Any] = {
        "do_sample": True,
        "temperature": (float(temperature) if temperature is not None else temp_env),
        "top_p": (float(top_p) if top_p is not None else topp_env),
        "num_return_sequences": 1,
        "max_new_tokens": max_new_tokens,
        "eos_token_id": getattr(tok, "eos_token_id", None),
        "pad_token_id": getattr(tok, "pad_token_id", getattr(tok, "eos_token_id", None)),
        "logits_processor": logits_processor,
        "use_cache": True,
    }
    with torch.inference_mode():
        out = model.generate(**enc, **{k: v for k, v in gen_kwargs.items() if v is not None})
    in_len = int(enc["input_ids"].shape[1])
    text = tok.decode(out[0][in_len :], skip_special_tokens=True)
    text = _apply_stops(text, stop)
    try:
        _trace_io("text", {"stage": "generate_text", "prompt_head": (prompt or "")[:800], "max_new_tokens": int(max_new_tokens), "text_head": (text or "")[:800]})
    except Exception:
        pass
    return text

def chat_sample(tok, model, messages: List[Dict[str, str]], n: int, max_new_tokens: int, *, temperature: Optional[float] = None, top_p: Optional[float] = None, stop: Optional[List[str]] = None) -> List[str]:
    # Build a chat-formatted prompt using tokenizer chat template when available
    prompt = None
    if hasattr(tok, "apply_chat_template"):
        try:
            prompt = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
        except Exception:
            prompt = None
    if prompt is None:
        parts: List[str] = []
        for m in messages:
            role = m.get("role", "user")
            content = m.get("content", "")
            parts.append(f"{role}: {content}")
        prompt = "\n".join(parts) + "\nassistant:"
    return sample(tok, model, prompt, n=n, max_new_tokens=max_new_tokens, temperature=temperature, top_p=top_p, stop=stop)


        "temperature": (float(temperature) if temperature is not None else temp_env),
        "top_p": (float(top_p) if top_p is not None else topp_env),
        "num_return_sequences": 1,
        "max_new_tokens": max_new_tokens,
        "eos_token_id": getattr(tok, "eos_token_id", None),
        "pad_token_id": getattr(tok, "pad_token_id", getattr(tok, "eos_token_id", None)),
        "logits_processor": logits_processor,
        "use_cache": True,
    }
    with torch.inference_mode():
        out = model.generate(**enc, **{k: v for k, v in gen_kwargs.items() if v is not None})
    in_len = int(enc["input_ids"].shape[1])
    text = tok.decode(out[0][in_len :], skip_special_tokens=True)
    text = _apply_stops(text, stop)
    try:
        _trace_io("text", {"stage": "generate_text", "prompt_head": (prompt or "")[:800], "max_new_tokens": int(max_new_tokens), "text_head": (text or "")[:800]})
    except Exception:
        pass
    return text

def chat_sample(tok, model, messages: List[Dict[str, str]], n: int, max_new_tokens: int, *, temperature: Optional[float] = None, top_p: Optional[float] = None, stop: Optional[List[str]] = None) -> List[str]:
    # Build a chat-formatted prompt using tokenizer chat template when available
    prompt = None
    if hasattr(tok, "apply_chat_template"):
        try:
            prompt = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
        except Exception:
            prompt = None
    if prompt is None:
        parts: List[str] = []
        for m in messages:
            role = m.get("role", "user")
            content = m.get("content", "")
            parts.append(f"{role}: {content}")
        prompt = "\n".join(parts) + "\nassistant:"
    return sample(tok, model, prompt, n=n, max_new_tokens=max_new_tokens, temperature=temperature, top_p=top_p, stop=stop)


        out = model.generate(**enc, **{k: v for k, v in gen_kwargs.items() if v is not None})
    # Trace I/O
    try:
        _trace_io("code", {"stage": "sample", "prompt_head": (prompt or "")[:800], "n": int(n), "max_new_tokens": int(max_new_tokens)})
    except Exception:
        pass
    # Use input length from encoding for prompt trimming
    in_len = int(enc["input_ids"].shape[1])
    texts = [tok.decode(o[in_len :], skip_special_tokens=True) for o in out]
    # Apply stop strings to raw texts first
    texts = [_apply_stops(t, stop) for t in texts]
    codes: List[str] = []
    # Prefer exactly one fenced code block per output: extract and accept that block
    # early to capture the model's intended single solution.
    for t in texts:
        blk = extract_first_fenced_block(t)
        if blk is not None:
            codes.append(blk)
    if codes:
        return codes
    def _extract_blocks(txt: str) -> List[str]:
        blocks: List[str] = []
        # Prefer a shared fenced extraction to keep behavior consistent.
        blk = extract_first_fenced_block(txt)
        if blk is not None:
            txt = blk
        # Drop a leading language header line if present (e.g., 'python')
        try:
            _lines_probe = [ln for ln in txt.splitlines()]
            j = next((i for i, ln in enumerate(_lines_probe) if ln.strip()), None)
            if j is not None and _lines_probe[j].strip().lower() in ("python", "python3", "py"):
                txt = "\n".join(_lines_probe[j + 1 :])
        except Exception:
            pass
        # Find the line with def solve(...):
        lines = txt.splitlines()
        idx = -1
        for i, ln in enumerate(lines):
            if re.match(r"^\s*def\s+solve\s*\([^)]*\)\s*(?:->\s*[^:]+)?\s*:\s*(?:#.*)?$", ln):
                idx = i
                break
        if idx == -1:
            return blocks
        # Determine indentation of function body (next non-empty line after signature)
        sig_line = lines[idx]
        body_lines: List[str] = [sig_line]
        i = idx + 1
        # Consume indented body lines only; stop at first non-indented, non-empty top-level line
        while i < len(lines):
            ln = lines[i]
            if not ln.strip():
                body_lines.append(ln)
                i += 1
                continue
            # top-level when no leading whitespace or startswith 'def ' or 'class '
            if re.match(r"^\S", ln) or re.match(r"^def\s+|^class\s+", ln):
                break
            body_lines.append(ln)
            i += 1
        block = "\n".join(body_lines).rstrip()
        # Drop trailing main-guard if somehow included
        block = re.split(r"^\s*if\s+__name__\s*==\s*['\"]__main__['\"]\s*:\s*$", block, maxsplit=1, flags=re.MULTILINE)[0].rstrip()
        if block.strip():
            blocks.append(block)
        return blocks
    for t in texts:
        blocks = _extract_blocks(t)
        codes.extend(blocks)
        try:
            _trace_io("code", {"stage": "sample_out", "text_head": (t or "")[:800], "blocks": len(blocks)})
        except Exception:
            pass
    # Fallback: if nothing extracted, try a looser parse for any function and rename to solve
    if not codes:
        for t in texts:
            # get first fenced block or entire text
            txt = t
            if txt.strip().startswith("```"):
                try:
                    first_nl = txt.find("\n"); last_fence = txt.rfind("```")
                    if first_nl != -1 and last_fence != -1 and last_fence > first_nl:
                        txt = txt[first_nl+1:last_fence]
                except Exception:
                    pass
            m = re.search(r"^\s*def\s+([A-Za-z_]\w*)\s*\(.*\):", txt, flags=re.MULTILINE)


                continue
            name = m.group(1)
            blocks = _extract_blocks(txt)
            if not blocks:
                continue
            block = blocks[0]
            if name != "solve":
                block = re.sub(r"^\s*def\s+" + re.escape(name) + r"\s*\(", "def solve(", block, count=1, flags=re.MULTILINE)
            codes.append(block)
    # Final fallback: treat fenced block or raw text as body-only code
    if not codes:
        for t in texts:
            txt = t
            blk2 = extract_first_fenced_block(txt)
            if blk2 is not None:
                txt = blk2
            codes.append(txt.strip()[:2000])
    # Deduplicate by AST shape to encourage diversity
    try:
        import ast as _ast
        def _ast_key(s: str) -> str:
            try:
                t = _ast.parse(s)
                return json.dumps(ast_to_tuple(t))
            except Exception:
                return s.strip()[:80]
        def ast_to_tuple(node):
            if isinstance(node, list):
                return [ast_to_tuple(x) for x in node]
            try:
                import ast as __ast
                fields = []
                for name in getattr(node, '_fields', []):
                    val = getattr(node, name)
                    if name in ('lineno','col_offset','end_lineno','end_col_offset'):
                        continue
                    fields.append((name, ast_to_tuple(val)))
                return (node.__class__.__name__, tuple(fields))
            except Exception:
                return str(node)
        seen: set[str] = set()
        uniq: list[str] = []
        for c in codes:
            k = _ast_key(c)
            if k in seen:
                continue


            if name != "solve":
                block = re.sub(r"^\s*def\s+" + re.escape(name) + r"\s*\(", "def solve(", block, count=1, flags=re.MULTILINE)
            codes.append(block)
    # Final fallback: treat fenced block or raw text as body-only code
    if not codes:
        for t in texts:
            txt = t
            blk2 = extract_first_fenced_block(txt)
            if blk2 is not None:
                txt = blk2
            codes.append(txt.strip()[:2000])
    # Deduplicate by AST shape to encourage diversity
    try:
        import ast as _ast
        def _ast_key(s: str) -> str:
            try:
                t = _ast.parse(s)
                return json.dumps(ast_to_tuple(t))
            except Exception:
                return s.strip()[:80]
        def ast_to_tuple(node):
            if isinstance(node, list):
                return [ast_to_tuple(x) for x in node]
            try:
                import ast as __ast
                fields = []
                for name in getattr(node, '_fields', []):
                    val = getattr(node, name)
                    if name in ('lineno','col_offset','end_lineno','end_col_offset'):
                        continue
                    fields.append((name, ast_to_tuple(val)))
                return (node.__class__.__name__, tuple(fields))
            except Exception:
                return str(node)
        seen: set[str] = set()
        uniq: list[str] = []
        for c in codes:
            k = _ast_key(c)
            if k in seen:
                continue
            seen.add(k)
            uniq.append(c)
        codes = uniq
    except Exception:
        pass
    # Signature-primed retry: if still no def solve block, ask model to complete the signature
    if not any(re.search(r"^\s*def\s+solve\s*\([^)]*\)\s*(?:->\s*[^:]+)?\s*:\s*", c, flags=re.MULTILINE) for c in codes):
        try:
            # heuristically extract signature from prompt if present
            m = re.search(r"^\s*def\s+solve\s*\([^)]*\)\s*(?:->\s*[^:]+)?\s*:\s*", prompt, flags=re.MULTILINE)
            if m:
                sig = m.group(0)
                primed = prompt.rstrip() + "\n\nComplete the following function:\n" + sig + "\n"
                t2 = generate_text(tok, model, primed, max_new_tokens=max_new_tokens)


from .common_imports import *

def strip_non_code_preamble(src: str) -> str:
    try:
        s = (src or "").lstrip()
        if s.lower().startswith("python"):
            s = s[len("python"):].lstrip()
        s = s.replace("```python", "").replace("```", "")
        return s.strip()
    except Exception:
        return src

def ensure_top_import(src: str, line: str) -> str:
    try:
        lines = [l for l in (src or "").splitlines() if not l.strip().startswith("#")]
        if any(l.strip() == line for l in lines):
            return src
        for i, l in enumerate(lines):
            if l.strip().startswith("def solve("):
                lines.insert(i, line)
                return "\n".join(lines)
        return line + "\n" + (src or "")
    except Exception:
        return src

def force_function_name(src: str, signature: str) -> str:
    try:
        return re.sub(r"def\s+\w+\s*\(", "def solve(", src or "", count=1)
    except Exception:
        return src

def rewrite_body_to_return(src: str, ret_line: str) -> str:
    try:
        lines = (src or "").splitlines()
        out: list[str] = []
        inserted = False
        for i, ln in enumerate(lines):
            out.append(ln)
            if re.match(r"^\s*def\s+solve\s*\(.*\):\s*$", ln):
                out.append("    " + ret_line.strip())
                inserted = True
                # Skip existing body until next top-level
                j = i + 1
                while j < len(lines) and (not re.match(r"^\S", lines[j])):
                    j += 1
                out.extend(lines[j:])
                break
        return "\n".join(out) if inserted else src
    except Exception:
        return src

def enforce_pipeline_body(src: str, params: Dict[str, str]) -> str:
    try:
        body = (
            "from functools import reduce\n"
            "def solve(arr:list[int])->int:\n"
            "    mapped = (x*x for x in arr)\n"
            "    filtered = (x for x in mapped if x % 2 == 0)\n"
            "    return reduce(lambda a,b: a+b, filtered, 0)\n"
        )
        return body
    except Exception:
        return src

def diagnose(src: str, task: Dict[str, Any], stage_score: Any, hints: list[str]) -> list[tuple[str, Dict[str, Any]]]:
    fixes: list[tuple[str, Dict[str, Any]]] = []
    s = src or ""
    if s.strip().startswith("python") or "```" in s:
        fixes.append(("strip_preamble", {}))
    sig = str(task.get("signature", "def solve():"))
    if not re.search(r"^\s*def\s+solve\s*\(", s, flags=re.MULTILINE):
        fixes.append(("rename_function_to_solve", {"signature": sig}))
    if any("undefined: reduce" in h for h in (hints or [])) or re.search(r"\breduce\s*\(", s):
        fixes.append(("add_import", {"line": "from functools import reduce"}))
    tname = str(task.get("name", ""))
    if tname == "rev_str" or tname == "rev_str_involution":
        fixes.append(("enforce_reverse_slice", {}))
    if tname == "pipeline_map_filter_reduce":
        fixes.append(("enforce_pipeline", {"map": "x*x", "filter": "x%2==0", "reduce": "x+y"}))
    return fixes

def apply_fix(src: str, fix: tuple[str, Dict[str, Any]]) -> str:
    kind, params = fix
    if kind == "strip_preamble":
        return strip_non_code_preamble(src)
    if kind == "rename_function_to_solve":
        return force_function_name(src, params.get("signature", "def solve():"))
    if kind == "add_import":
        return ensure_top_import(src, params.get("line", ""))
    if kind == "enforce_reverse_slice":
        return rewrite_body_to_return(src, "return s[::-1]")
    if kind == "enforce_pipeline":
        return enforce_pipeline_body(src, params)
    return src


from .common_imports import *

def strip_non_code_preamble(src: str) -> str:
    try:
        s = (src or "").lstrip()
        if s.lower().startswith("python"):
            s = s[len("python"):].lstrip()
        s = s.replace("```python", "").replace("```", "")
        return s.strip()
    except Exception:
        return src

def ensure_top_import(src: str, line: str) -> str:
    try:
        lines = [l for l in (src or "").splitlines() if not l.strip().startswith("#")]
        if any(l.strip() == line for l in lines):
            return src
        for i, l in enumerate(lines):
            if l.strip().startswith("def solve("):
                lines.insert(i, line)
                return "\n".join(lines)
        return line + "\n" + (src or "")
    except Exception:
        return src

def force_function_name(src: str, signature: str) -> str:
    try:
        return re.sub(r"def\s+\w+\s*\(", "def solve(", src or "", count=1)
    except Exception:


from .common_imports import *

def strip_non_code_preamble(src: str) -> str:
    try:
        s = (src or "").lstrip()
        if s.lower().startswith("python"):
            s = s[len("python"):].lstrip()
        s = s.replace("```python", "").replace("```", "")
        return s.strip()
    except Exception:
        return src

def ensure_top_import(src: str, line: str) -> str:
    try:
        lines = [l for l in (src or "").splitlines() if not l.strip().startswith("#")]
        if any(l.strip() == line for l in lines):
            return src
        for i, l in enumerate(lines):
            if l.strip().startswith("def solve("):
                lines.insert(i, line)
                return "\n".join(lines)
        return line + "\n" + (src or "")
    except Exception:
        return src

def force_function_name(src: str, signature: str) -> str:
    try:
        return re.sub(r"def\s+\w+\s*\(", "def solve(", src or "", count=1)
    except Exception:
        return src

def rewrite_body_to_return(src: str, ret_line: str) -> str:
    try:
        lines = (src or "").splitlines()
        out: list[str] = []
        inserted = False
        for i, ln in enumerate(lines):
            out.append(ln)
            if re.match(r"^\s*def\s+solve\s*\(.*\):\s*$", ln):
                out.append("    " + ret_line.strip())
                inserted = True
                # Skip existing body until next top-level


        s = s.replace("```python", "").replace("```", "")
        return s.strip()
    except Exception:
        return src

def ensure_top_import(src: str, line: str) -> str:
    try:
        lines = [l for l in (src or "").splitlines() if not l.strip().startswith("#")]
        if any(l.strip() == line for l in lines):
            return src
        for i, l in enumerate(lines):
            if l.strip().startswith("def solve("):
                lines.insert(i, line)
                return "\n".join(lines)
        return line + "\n" + (src or "")
    except Exception:
        return src

def force_function_name(src: str, signature: str) -> str:
    try:
        return re.sub(r"def\s+\w+\s*\(", "def solve(", src or "", count=1)
    except Exception:
        return src

def rewrite_body_to_return(src: str, ret_line: str) -> str:
    try:
        lines = (src or "").splitlines()
        out: list[str] = []
        inserted = False
        for i, ln in enumerate(lines):
            out.append(ln)
            if re.match(r"^\s*def\s+solve\s*\(.*\):\s*$", ln):
                out.append("    " + ret_line.strip())
                inserted = True
                # Skip existing body until next top-level
                j = i + 1
                while j < len(lines) and (not re.match(r"^\S", lines[j])):
                    j += 1
                out.extend(lines[j:])
                break
        return "\n".join(out) if inserted else src
    except Exception:


    try:
        lines = [l for l in (src or "").splitlines() if not l.strip().startswith("#")]
        if any(l.strip() == line for l in lines):
            return src
        for i, l in enumerate(lines):
            if l.strip().startswith("def solve("):
                lines.insert(i, line)
                return "\n".join(lines)
        return line + "\n" + (src or "")
    except Exception:
        return src

def force_function_name(src: str, signature: str) -> str:
    try:
        return re.sub(r"def\s+\w+\s*\(", "def solve(", src or "", count=1)
    except Exception:
        return src

def rewrite_body_to_return(src: str, ret_line: str) -> str:
    try:
        lines = (src or "").splitlines()
        out: list[str] = []
        inserted = False
        for i, ln in enumerate(lines):
            out.append(ln)
            if re.match(r"^\s*def\s+solve\s*\(.*\):\s*$", ln):
                out.append("    " + ret_line.strip())
                inserted = True
                # Skip existing body until next top-level
                j = i + 1
                while j < len(lines) and (not re.match(r"^\S", lines[j])):
                    j += 1
                out.extend(lines[j:])
                break
        return "\n".join(out) if inserted else src
    except Exception:
        return src

def enforce_pipeline_body(src: str, params: Dict[str, str]) -> str:
    try:
        body = (
            "from functools import reduce\n"
            "def solve(arr:list[int])->int:\n"
            "    mapped = (x*x for x in arr)\n"
            "    filtered = (x for x in mapped if x % 2 == 0)\n"
            "    return reduce(lambda a,b: a+b, filtered, 0)\n"
        )
        return body
    except Exception:
        return src

def diagnose(src: str, task: Dict[str, Any], stage_score: Any, hints: list[str]) -> list[tuple[str, Dict[str, Any]]]:
    fixes: list[tuple[str, Dict[str, Any]]] = []
    s = src or ""
    if s.strip().startswith("python") or "```" in s:


    try:
        lines = (src or "").splitlines()
        out: list[str] = []
        inserted = False
        for i, ln in enumerate(lines):
            out.append(ln)
            if re.match(r"^\s*def\s+solve\s*\(.*\):\s*$", ln):
                out.append("    " + ret_line.strip())
                inserted = True
                # Skip existing body until next top-level
                j = i + 1
                while j < len(lines) and (not re.match(r"^\S", lines[j])):
                    j += 1
                out.extend(lines[j:])
                break
        return "\n".join(out) if inserted else src
    except Exception:
        return src

def enforce_pipeline_body(src: str, params: Dict[str, str]) -> str:
    try:
        body = (
            "from functools import reduce\n"
            "def solve(arr:list[int])->int:\n"
            "    mapped = (x*x for x in arr)\n"
            "    filtered = (x for x in mapped if x % 2 == 0)\n"
            "    return reduce(lambda a,b: a+b, filtered, 0)\n"
        )
        return body
    except Exception:
        return src

def diagnose(src: str, task: Dict[str, Any], stage_score: Any, hints: list[str]) -> list[tuple[str, Dict[str, Any]]]:
    fixes: list[tuple[str, Dict[str, Any]]] = []
    s = src or ""
    if s.strip().startswith("python") or "```" in s:
        fixes.append(("strip_preamble", {}))
    sig = str(task.get("signature", "def solve():"))
    if not re.search(r"^\s*def\s+solve\s*\(", s, flags=re.MULTILINE):
        fixes.append(("rename_function_to_solve", {"signature": sig}))
    if any("undefined: reduce" in h for h in (hints or [])) or re.search(r"\breduce\s*\(", s):
        fixes.append(("add_import", {"line": "from functools import reduce"}))
    tname = str(task.get("name", ""))
    if tname == "rev_str" or tname == "rev_str_involution":
        fixes.append(("enforce_reverse_slice", {}))
    if tname == "pipeline_map_filter_reduce":
        fixes.append(("enforce_pipeline", {"map": "x*x", "filter": "x%2==0", "reduce": "x+y"}))
    return fixes


diagnose

                break
        return "\n".join(out) if inserted else src
    except Exception:
        return src

def enforce_pipeline_body(src: str, params: Dict[str, str]) -> str:
    try:
        body = (
            "from functools import reduce\n"
            "def solve(arr:list[int])->int:\n"
            "    mapped = (x*x for x in arr)\n"
            "    filtered = (x for x in mapped if x % 2 == 0)\n"
            "    return reduce(lambda a,b: a+b, filtered, 0)\n"
        )
        return body
    except Exception:
        return src

def diagnose(src: str, task: Dict[str, Any], stage_score: Any, hints: list[str]) -> list[tuple[str, Dict[str, Any]]]:
    fixes: list[tuple[str, Dict[str, Any]]] = []
    s = src or ""
    if s.strip().startswith("python") or "```" in s:
        fixes.append(("strip_preamble", {}))
    sig = str(task.get("signature", "def solve():"))
    if not re.search(r"^\s*def\s+solve\s*\(", s, flags=re.MULTILINE):
        fixes.append(("rename_function_to_solve", {"signature": sig}))
    if any("undefined: reduce" in h for h in (hints or [])) or re.search(r"\breduce\s*\(", s):
        fixes.append(("add_import", {"line": "from functools import reduce"}))
    tname = str(task.get("name", ""))
    if tname == "rev_str" or tname == "rev_str_involution":
        fixes.append(("enforce_reverse_slice", {}))
    if tname == "pipeline_map_filter_reduce":
        fixes.append(("enforce_pipeline", {"map": "x*x", "filter": "x%2==0", "reduce": "x+y"}))
    return fixes

def apply_fix(src: str, fix: tuple[str, Dict[str, Any]]) -> str:
    kind, params = fix
    if kind == "strip_preamble":
        return strip_non_code_preamble(src)
    if kind == "rename_function_to_solve":
        return force_function_name(src, params.get("signature", "def solve():"))
    if kind == "add_import":
        return ensure_top_import(src, params.get("line", ""))
    if kind == "enforce_reverse_slice":
        return rewrite_body_to_return(src, "return s[::-1]")
    if kind == "enforce_pipeline":
        return enforce_pipeline_body(src, params)
    return src


def diagnose(src: str, task: Dict[str, Any], stage_score: Any, hints: list[str]) -> list[tuple[str, Dict[str, Any]]]:
    fixes: list[tuple[str, Dict[str, Any]]] = []
    s = src or ""
    if s.strip().startswith("python") or "```" in s:
        fixes.append(("strip_preamble", {}))
    sig = str(task.get("signature", "def solve():"))
    if not re.search(r"^\s*def\s+solve\s*\(", s, flags=re.MULTILINE):
        fixes.append(("rename_function_to_solve", {"signature": sig}))
    if any("undefined: reduce" in h for h in (hints or [])) or re.search(r"\breduce\s*\(", s):
        fixes.append(("add_import", {"line": "from functools import reduce"}))
    tname = str(task.get("name", ""))
    if tname == "rev_str" or tname == "rev_str_involution":
        fixes.append(("enforce_reverse_slice", {}))
    if tname == "pipeline_map_filter_reduce":
        fixes.append(("enforce_pipeline", {"map": "x*x", "filter": "x%2==0", "reduce": "x+y"}))
    return fixes

def apply_fix(src: str, fix: tuple[str, Dict[str, Any]]) -> str:
    kind, params = fix
    if kind == "strip_preamble":
        return strip_non_code_preamble(src)
    if kind == "rename_function_to_solve":
        return force_function_name(src, params.get("signature", "def solve():"))
    if kind == "add_import":
        return ensure_top_import(src, params.get("line", ""))
    if kind == "enforce_reverse_slice":
        return rewrite_body_to_return(src, "return s[::-1]")
    if kind == "enforce_pipeline":
        return enforce_pipeline_body(src, params)
    return src


from .common_imports import *

def extract_first_fenced_block(text: str) -> str | None:
    """Return the contents of the first fenced code block, sans optional language header.

    Heuristic:
    - Find the first opening fence ```
    - Slice from the first newline after the opening to the first subsequent closing fence
    - Drop a one-line language header like 'python'/'python3'/'py' if present
    - Return stripped text or None if not found
    """
    try:
        s = str(text).lstrip("\ufeff")
        i0 = s.find("```")
        if i0 == -1:
            return None
        first_nl = s.find("\n", i0)
        if first_nl == -1:
            return None
        i1 = s.find("```", first_nl + 1)
        if i1 == -1:
            i1 = s.rfind("```")
        if i1 == -1 or i1 <= first_nl:
            return None
        inner = s[first_nl + 1:i1]
        lines = inner.splitlines()
        if lines and lines[0].strip().lower() in ("python", "python3", "py"):
            inner = "\n".join(lines[1:])
        inner = inner.strip()
        return inner if inner else None
    except Exception:
        return None


from .common_imports import *
from dataclasses import dataclass

# Lightweight staged verifier with partial credit shaping

@dataclass
class StageScore:
    format_ok: float
    compile_ok: float
    signature_ok: float
    style_ok: float
    tests_pass: float

_FENCE_RE = re.compile(r"^```", re.MULTILINE)

def looks_like_pure_code(src: str) -> bool:
    try:
        if _FENCE_RE.search(src or ""):
            return False
        s = (src or "").lstrip()
        if s.lower().startswith("python"):
            return False
        # Heuristic: must contain either a top import or a def solve line
        return bool(re.search(r"^\s*(from\s+\w|import\s+\w|def\s+solve\s*\(", s, flags=re.MULTILINE))
    except Exception:
        return False

def signature_matches(src: str, signature: str) -> bool:
    try:
        want_name = "solve"
        m = re.search(r"def\s+(\w+)\s*\(", signature or "")
        if m:
            want_name = m.group(1)
        m2 = re.search(r"^\s*def\s+(\w+)\s*\(", src or "", flags=re.MULTILINE)
        return bool(m2 and m2.group(1) == want_name)
    except Exception:
        return False

def _static_checks(src: str, *, allowed_stdlib: bool = True) -> tuple[float, list[str]]:
    import ast as _ast
    try:
        tree = _ast.parse(src)
    except Exception as e:
        return 0.0, [f"parse_error: {e}"]
    names: set[str] = set()
    imports: list[str] = []
    banned_names = {"open", "subprocess", "os.system", "sys.exit", "eval", "exec"}
    hints: list[str] = []
    for n in _ast.walk(tree):
        if isinstance(n, _ast.Name):
            names.add(n.id)
        if isinstance(n, (_ast.Import, _ast.ImportFrom)):
            if isinstance(n, _ast.ImportFrom):
                if n.module:
                    imports.append(n.module.split(".")[0])
            else:
                for a in n.names:
                    imports.append(a.name.split(".")[0])
        if isinstance(n, _ast.Call) and isinstance(getattr(n, "func", None), _ast.Name) and getattr(n.func, "id", "") in {"eval", "exec"}:
            return 0.0, ["banned: eval/exec"]
    try:
        if any(b in src for b in banned_names):
            return 0.0, ["banned name used"]
    except Exception:
        pass
    if allowed_stdlib:
        try:
            import sys as _sys
            allowed = set(getattr(_sys, "stdlib_module_names", set()) or set())
        except Exception:
            allowed = {"math", "functools", "itertools", "collections", "heapq", "bisect", "random"}
        for m in imports:
            if m and m not in allowed:
                hints.append(f"non-stdlib import: {m}")
                return 0.0, hints
    # Undefined names hinting (best-effort)
    try:
        import builtins as _bi
        undefined = [n for n in names if not hasattr(_bi, n)]
        if undefined:
            hints.append("undefined: " + ",".join(sorted(undefined)))
    except Exception:
        pass
    return 1.0, hints

def _safe_compile(src: str) -> tuple[bool, Any]:
    g: Dict[str, Any] = {"__builtins__": {}}
    loc: Dict[str, Any] = {}
    for k in [
        "abs","all","any","enumerate","len","range","min","max","sum","map","filter",
        "zip","sorted","reversed","list","tuple","dict","set","bool","int","float","str","print","__import__",
    ]:
        try:
            g["__builtins__"][k] = getattr(__builtins__, k)
        except Exception:
            pass
    try:
        exec(src, g, loc)
        return True, loc
    except Exception as e:
        return False, str(e)

def _run_tests(compiled_loc: Dict[str, Any] | None, tests: list[dict]) -> float:
    if not isinstance(compiled_loc, dict):
        return 0.0
    fn = compiled_loc.get("solve")
    if not callable(fn):
        return 0.0
    passed = 0
    total = max(1, len(tests or []))
    try:
        for t in (tests or []):
            ok = False
            try:
                out = fn(*t.get("inp", []))
                ok = (out == t.get("out"))
            except Exception:
                ok = False
            if ok:
                passed += 1
    except Exception:
        return 0.0
    return float(passed) / float(total)

def grade_staged(src: str, task: Dict[str, Any]) -> tuple[StageScore, list[str]]:
    fmt = 1.0 if looks_like_pure_code(src) else 0.0
    sig_ok = 1.0 if signature_matches(src, str(task.get("signature", "def solve():"))) else 0.0
    style_ok, hints = _static_checks(src, allowed_stdlib=True)
    comp_ok, mod_or_err = _safe_compile(src)
    tests_pass = _run_tests(mod_or_err if comp_ok else None, task.get("tests", [])) if comp_ok else 0.0
    return StageScore(fmt, 1.0 if comp_ok else 0.0, sig_ok, float(style_ok), float(tests_pass)), hints

def stage_reward(sc: StageScore) -> float:
    return (
        0.10 * sc.format_ok
        + 0.10 * sc.signature_ok
        + 0.20 * sc.style_ok
        + 0.20 * sc.compile_ok
        + 0.40 * sc.tests_pass
    )


StageScore

from .common_imports import *
from dataclasses import dataclass

# Lightweight staged verifier with partial credit shaping

@dataclass
class StageScore:
    format_ok: float
    compile_ok: float
    signature_ok: float
    style_ok: float
    tests_pass: float

_FENCE_RE = re.compile(r"^```", re.MULTILINE)

def looks_like_pure_code(src: str) -> bool:
    try:
        if _FENCE_RE.search(src or ""):
            return False
        s = (src or "").lstrip()
        if s.lower().startswith("python"):
            return False
        # Heuristic: must contain either a top import or a def solve line
        return bool(re.search(r"^\s*(from\s+\w|import\s+\w|def\s+solve\s*\(", s, flags=re.MULTILINE))
    except Exception:
        return False

def signature_matches(src: str, signature: str) -> bool:
    try:


from .common_imports import *
from dataclasses import dataclass

# Lightweight staged verifier with partial credit shaping

@dataclass
class StageScore:
    format_ok: float
    compile_ok: float
    signature_ok: float
    style_ok: float
    tests_pass: float

_FENCE_RE = re.compile(r"^```", re.MULTILINE)

def looks_like_pure_code(src: str) -> bool:
    try:
        if _FENCE_RE.search(src or ""):
            return False
        s = (src or "").lstrip()
        if s.lower().startswith("python"):
            return False
        # Heuristic: must contain either a top import or a def solve line
        return bool(re.search(r"^\s*(from\s+\w|import\s+\w|def\s+solve\s*\(", s, flags=re.MULTILINE))
    except Exception:
        return False

def signature_matches(src: str, signature: str) -> bool:
    try:
        want_name = "solve"
        m = re.search(r"def\s+(\w+)\s*\(", signature or "")
        if m:
            want_name = m.group(1)
        m2 = re.search(r"^\s*def\s+(\w+)\s*\(", src or "", flags=re.MULTILINE)
        return bool(m2 and m2.group(1) == want_name)
    except Exception:
        return False

def _static_checks(src: str, *, allowed_stdlib: bool = True) -> tuple[float, list[str]]:
    import ast as _ast
    try:
        tree = _ast.parse(src)
    except Exception as e:
        return 0.0, [f"parse_error: {e}"]


    style_ok: float
    tests_pass: float

_FENCE_RE = re.compile(r"^```", re.MULTILINE)

def looks_like_pure_code(src: str) -> bool:
    try:
        if _FENCE_RE.search(src or ""):
            return False
        s = (src or "").lstrip()
        if s.lower().startswith("python"):
            return False
        # Heuristic: must contain either a top import or a def solve line
        return bool(re.search(r"^\s*(from\s+\w|import\s+\w|def\s+solve\s*\(", s, flags=re.MULTILINE))
    except Exception:
        return False

def signature_matches(src: str, signature: str) -> bool:
    try:
        want_name = "solve"
        m = re.search(r"def\s+(\w+)\s*\(", signature or "")
        if m:
            want_name = m.group(1)
        m2 = re.search(r"^\s*def\s+(\w+)\s*\(", src or "", flags=re.MULTILINE)
        return bool(m2 and m2.group(1) == want_name)
    except Exception:
        return False

def _static_checks(src: str, *, allowed_stdlib: bool = True) -> tuple[float, list[str]]:
    import ast as _ast
    try:
        tree = _ast.parse(src)
    except Exception as e:
        return 0.0, [f"parse_error: {e}"]
    names: set[str] = set()
    imports: list[str] = []
    banned_names = {"open", "subprocess", "os.system", "sys.exit", "eval", "exec"}
    hints: list[str] = []
    for n in _ast.walk(tree):
        if isinstance(n, _ast.Name):
            names.add(n.id)
        if isinstance(n, (_ast.Import, _ast.ImportFrom)):
            if isinstance(n, _ast.ImportFrom):
                if n.module:
                    imports.append(n.module.split(".")[0])
            else: