                    mn = os.popen(f"git -C {root} rev-parse HEAD").read().strip()
                except Exception:
                    mn = None
            if mn:
                short = mn[:12]
                text = text.rstrip() + f"\n\nAnswer valid for commit {short}."
        except Exception:
            pass

    # Clean up hooks
    for h in hooks:
        try:
            h.remove()
        except Exception:
            pass

    return text

def generate_answer_structured(
    model_id: str,
    adapters_npz: str,
    prompt: str,
    *,
    program_root: Optional[str] = None,
    delta_cap: float = 0.05,
    # Same knobs as generate_answer with a few extras
    cache_dir: Optional[str] = None,
    device: str = "cuda" if torch.cuda.is_available() else "cpu",
    device_map: str = "none",
    gpu_ids: Optional[str] = None,
    of_sources: str = "question",
    zoom_symbol: Optional[str] = None,
    zoom_radius: int = 0,
    ignore: Optional[List[str]] = None,
    pack_context: bool = True,
    pack_mode: str = "windows",
    context_tokens: int = 3000,
    function_first: bool = True,
    ff_max_candidates: int = 24,
    ff_window_lines: int = 80,
    ff_threshold: float = 0.55,
    ff_noise_penalty: float = 0.30,
    alpha: float = 16.0,
    rank: int = 12,
    gsub: float = 0.75,
    beta: float = 0.1,
    entropy_aware: bool = False,
    rank_min: int = 8,
    rank_max: int = 32,
    gsub_min: float = 0.6,
    gsub_max: float = 0.9,
    entropy_weights: str = "program=0.4,subgraph=0.4,question=0.2",
    target_weights: Optional[str] = None,
    layer_schedule: bool = False,
    q_aware_weights: bool = False,
    mixture_m: int = 0,
    adapters_bank: Optional[str] = None,
    # Extra mixing/decoding knobs (opt-in)
    per_target_rank_schedule: bool = False,
    rank_budget: int = 0,
    ablate_attn: bool = False,
    ablate_mlp: bool = False,
    alpha_warmup: bool = False,
    adapter_aware_decoding: bool = False,
    layer_rank_tiers: bool = False,
    cone_rank: int = 2,
    cone_weight: float = 0.5,
    round_lora: bool = False,
    round_threshold: float = 0.5,
    require_citations: bool = True,
    citations_per_paragraph: bool = False,
    rerank: bool = True,
    self_queries_path: Optional[str] = None,
    do_sample: bool = False,
    temperature: float = 0.7,
    top_p: float = 0.9,
    repetition_penalty: float = 1.1,
    min_new_tokens: int = 64,
    max_new_tokens: int = 256,
    kv_window: int = 0,
    head_device: str = "same",
    seed: int = 0,
    verbose: bool = False,
    commit_footer: bool = False,
    # New dcpo/lfp controls
    lfp_iters: int = 1,
    budget_H: float = 0.0,
    monotone_selection: bool = True,
    program_state_path: Optional[str] = None,
    # Powerdomain strength
    samples: int = 1,
    # Delta join semantics
    cone_join: str = "concat",  # "concat" | "weighted"
    # Telemetry
    telemetry_out: Optional[str] = None,
    telemetry_verify_tests: bool = False,
) -> Dict[str, Any]:
    """Generate a structured answer with citations and program state updates.

    Returns a dict: {text, citations, must, may, selection, lfp_passes, converged, provenance, confidence}.
    """
    # Program root (path or URI) for reading artifacts/windows/state
    root = str(program_root) if (program_root is not None and str(program_root).strip()) else ""
    t_start = time.time()
    if gpu_ids and str(gpu_ids).strip():
        os.environ["CUDA_VISIBLE_DEVICES"] = str(gpu_ids).strip()
    os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")

    # Seed
    try:
        import random
        random.seed(int(seed))
        np.random.seed(int(seed))
        torch.manual_seed(int(seed))
    except Exception:
        pass

    # Cache
    if not cache_dir:
        env_cache = os.environ.get("TRANSFORMER_CACHE_DIR") or os.environ.get("HF_HOME")
        cache_dir = env_cache or os.path.join(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")), "checkpoints")
    try:
        os.makedirs(cache_dir, exist_ok=True)
    except Exception:
        pass

    # Local tokenizer (no transformers) and adapter for context/scoring utilities
    snap_dir = ensure_snapshot(model_id, cache_dir)
    tok_local = LocalLlamaTokenizer(snap_dir)
    class _TokAdapter:
        def __init__(self, base):
            self._b = base
        class _Ret:
            def __init__(self, ids):
                self.input_ids = ids
        def __call__(self, text: str, add_special_tokens: bool = False, return_tensors: str | None = None):
            ids = self._b.encode(text)
            if return_tensors == "pt":
                return {"input_ids": torch.tensor([ids], dtype=torch.long)}
            return _TokAdapter._Ret(ids)
    tok = _TokAdapter(tok_local)
    torch_dtype = torch.bfloat16 if str(device).startswith("cuda") else torch.float32
    model, _cfg = build_local_llama_from_snapshot(snap_dir, device, torch_dtype, device_map=device_map, gpu_ids=gpu_ids)

    base_layers = load_adapters_npz(adapters_npz)["layers"]
    # Program-agnostic structured path: skip graph embedder; retain main flow
    _ps_path = program_state_path if (program_state_path is not None and str(program_state_path).strip()) else None
    state_prev = load_program_state(root, path=_ps_path)

    must_set: Set[Tuple[str, int, int]] = set()
    union_set: Set[Tuple[str, int, int]] = set()
    selected_mods_acc: Set[str] = set(state_prev.candidates_modules) if monotone_selection else set()
    selected_files_acc: Set[str] = set(state_prev.candidates_files) if monotone_selection else set()
    lfp_passes = 0
    converged = False

    def _one_pass(mods_hint: List[str], files_hint: List[str]) -> Tuple[str, List[str], List[str], Dict[str, Any], Set[Tuple[str,int,int]], Set[Tuple[str,int,int]], Dict[str, Any]]:
        nonlocal model
        # Initial selection (question or zoom), union with hints for monotonicity
        ignore_list = [s for s in (ignore or []) if s]
        adapters_dir = os.path.dirname(adapters_npz)
        windows = load_windows_index(adapters_dir)
        files = pick_files_from_windows(root, windows, prompt, k=max(8, int(ff_max_candidates)))
        # initialize modules list for return and monotone selection merging
        modules: List[str] = []
        if monotone_selection:
            modules = sorted(list(set(mods_hint) | set(modules)))
            files = sorted(list(set(files_hint) | set(files)))
        # Subgraph emb
        # No subgraph embedding in program-agnostic structured path
        # Shapes/targets
        tmap = targets_map("local")
        t_shapes = infer_target_shapes(model)
        num_layers = len(getattr(model, "blocks", []))
        d_model_local = int(t_shapes.get("q_proj", (0, 0))[0]) or int(getattr(getattr(model, "cfg", None), "d_model", 0) or 0)
        # Entropy-aware
        scaled_rank = int(rank)
        scaled_gsub = float(gsub)
            # Build sub adapters
        sub = {"layers": []}
        # Optional cones
        cr = max(0, int(cone_rank))
        if bool(function_first) and cr > 0:
            try:
                pass
            except Exception:
                pass
        # Optional mixture bank: mix top-m module adapters from bank by concatenation (Σ π_i Δθ_i)
        if adapters_bank and int(mixture_m) > 0:
            try:
                import glob
                bank_root = os.path.abspath(os.path.expanduser(os.path.expandvars(adapters_bank)))
                sel_mods = [m for m in (modules or []) if m]
                picked = 0
                for mod in sel_mods:
                    if picked >= int(mixture_m):
                        break
                    mod_dir1 = os.path.join(bank_root, "sub_adapters", mod.replace("/", "_"))
                    mod_dir2 = os.path.join(bank_root, mod.replace("/", "_"))
                    cand = None
                    for d in (mod_dir1, mod_dir2):
                        fp = os.path.join(d, "adapters.npz")
                        if os.path.isfile(fp):
                            cand = fp
                            break
                    if not cand:
                        continue
                    try:
                        bank_ad = load_adapters_npz(cand)
                    except Exception:
                        continue
                    # uniform π for now
                    w = 1.0 / float(min(len(sel_mods), int(mixture_m)))
                    sw = float(max(0.0, min(1.0, w))) ** 0.5
                    # per-layer concat
                    merged_layers = []
                    for i in range(num_layers):
                        baseL = sub["layers"][i]
                        bL = bank_ad["layers"][i] if i < len(bank_ad.get("layers", [])) else {}
                        dst: Dict[str, Dict[str, np.ndarray]] = {}
                        for name in tmap.keys():
                            if (name in baseL) and (name in bL):
                                A1 = baseL[name]["A"]; B1 = baseL[name]["B"]
                                A2 = (sw * bL[name]["A"]).astype(np.float32); B2 = (sw * bL[name]["B"]).astype(np.float32)
                                A = np.concatenate([A1, A2], axis=1)
                                B = np.concatenate([B1, B2], axis=0)
                                dst[name] = {"A": A, "B": B, "gate": baseL[name].get("gate", np.array([0.0], dtype=np.float32))}
                            elif name in baseL:
                                dst[name] = baseL[name]
                            elif name in bL:
                                A = (sw * bL[name]["A"]).astype(np.float32)
                                dst[name] = {"A": A, "B": bL[name]["B"], "gate": bL[name].get("gate", np.array([0.0], dtype=np.float32))}
                        merged_layers.append(dst)
                    sub = {"layers": merged_layers}
                    # try infer rank increment from a present target
                    try:
                        for i in range(num_layers):
                            any_name = next((n for n in tmap.keys() if n in bank_ad["layers"][i]), None)
                            if any_name:
                                inc = int(bank_ad["layers"][i][any_name]["B"].shape[0])
                                scaled_rank = int(scaled_rank + inc)
                                break
                    except Exception:
                        pass
                    picked += 1
            except Exception:
                pass
        # Rounding
        if bool(round_lora):
            try:
                thr = float(max(0.0, round_threshold))
                for i in range(len(sub.get("layers", []))):
                    for name, tensors in sub["layers"][i].items():
                        for key in ("A", "B"):
                            arr = tensors.get(key)
                            if not isinstance(arr, np.ndarray):
                                continue
                            q = float(np.median(np.abs(arr))) if arr.size > 0 else 0.0
                            if q <= 0:
                                continue
                            out = np.where(np.abs(arr) < (thr * q), 0.0, np.sign(arr) * q).astype(np.float32)
                            tensors[key] = out
            except Exception:
                pass

        # Delta norm diagnostics (approximate AB norms per target averaged across layers)
        delta_norms: Dict[str, float] = {}
        try:
            sums: Dict[str, float] = {}
            counts: Dict[str, int] = {}
            for i in range(len(sub.get("layers", []))):
                for name, tensors in sub["layers"][i].items():
                    try:
                        A = tensors.get("A"); B = tensors.get("B")
                        if isinstance(A, np.ndarray) and isinstance(B, np.ndarray):
                            AB = (A @ B)
                            n = float(np.linalg.norm(AB)) if AB.size > 0 else 0.0
                            sums[name] = float(sums.get(name, 0.0) + n)
                            counts[name] = int(counts.get(name, 0) + 1)
                    except Exception:
                        continue
            for k, v in sums.items():
                c = max(1, int(counts.get(k, 1)))
                delta_norms[k] = float(v / float(c))
        except Exception:
            delta_norms = {}

        # Prompt + context
        final_prompt = prompt
        files_for_ctx = list(files)
        if bool(pack_context) and files_for_ctx:
            packed = ""
            ident_header = ""
            if pack_mode == "windows":
                if bool(function_first):
                    # Simplified: pack top window per file like non-structured path
                    adapters_dir = os.path.dirname(adapters_npz)
                    try:
                        windows = load_windows_index(adapters_dir)
                    except Exception:
                        windows = []
                    out_lines: List[str] = ["Program windows (function-first):"]
                    used = 0
                    budget = int(context_tokens)
                    for rel in files_for_ctx:
                        cand = [w for w in windows if (w.get("path") or "").replace("\\", "/") == rel.replace("\\", "/")]
                        if not cand:
                            continue
                        w = cand[0]
                        a = int(w.get("start_line", 1)); b = int(w.get("end_line", a + 60))
                        abs_fp = rel if os.path.isabs(rel) else os.path.abspath(os.path.join(root, rel))
                        try:
                            src_lines = open(abs_fp, "r", encoding="utf-8", errors="ignore").read().splitlines()
                        except Exception:
                            continue
                        a = max(1, min(a, len(src_lines))); b = max(a, min(b, len(src_lines)))
                        block = [f"[ctx] path: {rel}:{a}-{b}"] + src_lines[a - 1 : b] + [""]
                        text_block = "\n".join(block) + "\n"
                        t = len(tok(text_block).input_ids)
                        if used + t > budget:
                            continue
                        out_lines.extend(block)
                        used += t
                        if used >= budget:
                            break
                    packed = "\n".join(out_lines) if len(out_lines) > 1 else ""
                else:
                    # Fallback: identical to function-first simplified path
                    adapters_dir = os.path.dirname(adapters_npz)
                    try:
                        windows = load_windows_index(adapters_dir)
                    except Exception:
                        windows = []
                    out_lines: List[str] = ["Program windows:"]
                    used = 0
                    budget = int(context_tokens)
                    for rel in files_for_ctx:
                        cand = [w for w in windows if (w.get("path") or "").replace("\\", "/") == rel.replace("\\", "/")]
                        if not cand:
                            continue
                        w = cand[0]
                        a = int(w.get("start_line", 1)); b = int(w.get("end_line", a + 60))
                        abs_fp = rel if os.path.isabs(rel) else os.path.abspath(os.path.join(root, rel))
                        try:
                            src_lines = open(abs_fp, "r", encoding="utf-8", errors="ignore").read().splitlines()
                        except Exception:
                            continue
                        a = max(1, min(a, len(src_lines))); b = max(a, min(b, len(src_lines)))
                        block = [f"[ctx] path: {rel}:{a}-{b}"] + src_lines[a - 1 : b] + [""]
                        text_block = "\n".join(block) + "\n"
                        t = len(tok(text_block).input_ids)
                        if used + t > budget:
                            continue
                        out_lines.extend(block)
                        used += t
                        if used >= budget:
                            break
                    packed = "\n".join(out_lines) if len(out_lines) > 1 else ""
            else:
                packed = ""  # heads mode omitted in program-agnostic core
            if packed:
                final_prompt = (ident_header + packed + "\n\n" + final_prompt) if ident_header else (packed + "\n\n" + final_prompt)
        if require_citations:
            try:
                example_rel = files_for_ctx[0] if files_for_ctx else None
                example_path = example_rel if example_rel else "file.py"
            except Exception:
                example_path = "file.py"
            final_prompt = (
                final_prompt
                + f"\n\nInstruction: For EVERY claim, append a citation of the form {example_path}:START-END.\n"
                  "Use only files shown in [ctx] above. Provide at least 3 citations overall.\n"
            )
            # Adapter-aware decoding prompt nudge (pointer-first) for consistency
            if bool(adapter_aware_decoding):
                try:
                    example_rel = files_for_ctx[0] if files_for_ctx else None
                    example_path = example_rel if example_rel else "file.py"
                except Exception:
                    example_path = "file.py"
                final_prompt = (
                    f"[pointer-first] Start with a citation like [{example_path}:A-B], then explain.\n\n"
                    + final_prompt
                )

        # Apply mixed adapters
        x_ids = torch.tensor([[i for i in tok_local.encode(final_prompt)]], dtype=torch.long, device=device)
        x = {"input_ids": x_ids}
        def _parse_target_weights(spec: Optional[str]) -> Optional[Dict[str, float]]:
            if not spec:
                return None
            out: Dict[str, float] = {}
            try:
                for part in str(spec).split(","):
                    part = part.strip()
                    if not part:
                        continue
                    if "=" in part:
                        k, v = part.split("=", 1)
                        k = k.strip(); out[k] = float(v)
                    else:
                        out[part] = 1.0
                return out or None
            except Exception:
                return None
        tw = _parse_target_weights(target_weights) or {}
        # Optional question-aware reweighting
        if bool(q_aware_weights):
            try:
                ql = str(prompt).lower()
                mul: Dict[str, float] = {}
                if any(k in ql for k in ["signature", "param", "argument", "type", "prototype"]):
                    mul.update({"o_proj": 1.10, "v_proj": 1.08})
                if any(k in ql for k in ["why", "fail", "error", "behavior", "incorrect", "bug"]):
                    mul.update({"up_proj": 1.06, "down_proj": 1.05, "gate_proj": 1.04})
                if any(k in ql for k in ["where", "defined", "definition", "locate", "find"]):
                    mul.update({"q_proj": 1.03})
                for k, m in mul.items():
                    tw[k] = float(tw.get(k, 1.0)) * float(m)
            except Exception:
                pass
        # Optional per-layer schedule
        layer_multipliers: Optional[List[float]] = None
        if bool(layer_schedule):
            try:
                L_layers = len(base_layers or [])
                if L_layers > 0:
                    layer_multipliers = []
                    for i in range(L_layers):
                        frac = float(i) / float(max(1, L_layers - 1))
                        if frac < (1.0 / 3.0):
                            lm = 0.95 + 0.15 * (frac / (1.0 / 3.0))
                        elif frac < (2.0 / 3.0):
                            lm = 1.05 + 0.05 * ((frac - (1.0 / 3.0))
# ... truncated ...


    try:
        import random
        random.seed(int(seed))
        np.random.seed(int(seed))
        torch.manual_seed(int(seed))
    except Exception:
        pass

    # Cache
    if not cache_dir:
        env_cache = os.environ.get("TRANSFORMER_CACHE_DIR") or os.environ.get("HF_HOME")
        cache_dir = env_cache or os.path.join(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")), "checkpoints")
    try:
        os.makedirs(cache_dir, exist_ok=True)
    except Exception:
        pass

    # Local tokenizer (no transformers) and adapter for context/scoring utilities
    snap_dir = ensure_snapshot(model_id, cache_dir)
    tok_local = LocalLlamaTokenizer(snap_dir)
    class _TokAdapter:
        def __init__(self, base):
            self._b = base
        class _Ret:
            def __init__(self, ids):
                self.input_ids = ids
        def __call__(self, text: str, add_special_tokens: bool = False, return_tensors: str | None = None):
            ids = self._b.encode(text)
            if return_tensors == "pt":
                return {"input_ids": torch.tensor([ids], dtype=torch.long)}
            return _TokAdapter._Ret(ids)
    tok = _TokAdapter(tok_local)
    torch_dtype = torch.bfloat16 if str(device).startswith("cuda") else torch.float32
    model, _cfg = build_local_llama_from_snapshot(snap_dir, device, torch_dtype, device_map=device_map, gpu_ids=gpu_ids)

    base_layers = load_adapters_npz(adapters_npz)["layers"]
    # Program-agnostic structured path: skip graph embedder; retain main flow
    _ps_path = program_state_path if (program_state_path is not None and str(program_state_path).strip()) else None
    state_prev = load_program_state(root, path=_ps_path)

    must_set: Set[Tuple[str, int, int]] = set()
    union_set: Set[Tuple[str, int, int]] = set()
    selected_mods_acc: Set[str] = set(state_prev.candidates_modules) if monotone_selection else set()
    selected_files_acc: Set[str] = set(state_prev.candidates_files) if monotone_selection else set()
    lfp_passes = 0
    converged = False

    def _one_pass(mods_hint: List[str], files_hint: List[str]) -> Tuple[str, List[str], List[str], Dict[str, Any], Set[Tuple[str,int,int]], Set[Tuple[str,int,int]], Dict[str, Any]]:
        nonlocal model
        # Initial selection (question or zoom), union with hints for monotonicity
        ignore_list = [s for s in (ignore or []) if s]


    try:
        os.makedirs(cache_dir, exist_ok=True)
    except Exception:
        pass

    # Local tokenizer (no transformers)
    snap_dir = ensure_snapshot(model_id, cache_dir)
    tok_local = LocalLlamaTokenizer(snap_dir)
    class _TokAdapter:
        def __init__(self, base):
            self._b = base
        class _Ret:
            def __init__(self, ids):
                self.input_ids = ids
        def __call__(self, text: str, add_special_tokens: bool = False, return_tensors: str | None = None):
            ids = self._b.encode(text)
            if return_tensors == "pt":
                return {"input_ids": torch.tensor([ids], dtype=torch.long)}
            return _TokAdapter._Ret(ids)
    tok = _TokAdapter(tok_local)
    def _tok_encode(text: str) -> List[int]:
        return tok_local.encode(text)
    def _tok_len(text: str) -> int:
        try:
            return len(tok_local.encode(text))
        except Exception:
            return 0
    def _tok_decode(ids: torch.Tensor | List[int]) -> str:
        if isinstance(ids, torch.Tensor):
            try:
                ids_l = ids.to("cpu").tolist()
            except Exception:
                ids_l = []
        else:
            ids_l = list(ids)
        try:
            return tok_local.decode(ids_l, skip_special_tokens=True)
        except Exception:
            return tok_local.decode(ids_l)
    torch_dtype = torch.bfloat16 if str(device).startswith("cuda") else torch.float32

    model, _cfg = build_local_llama_from_snapshot(snap_dir, device, torch_dtype, device_map=device_map, gpu_ids=gpu_ids)


    except Exception:
        pass

    # Local tokenizer (no transformers)
    snap_dir = ensure_snapshot(model_id, cache_dir)
    tok_local = LocalLlamaTokenizer(snap_dir)
    class _TokAdapter:
        def __init__(self, base):
            self._b = base
        class _Ret:
            def __init__(self, ids):
                self.input_ids = ids
        def __call__(self, text: str, add_special_tokens: bool = False, return_tensors: str | None = None):
            ids = self._b.encode(text)
            if return_tensors == "pt":
                return {"input_ids": torch.tensor([ids], dtype=torch.long)}
            return _TokAdapter._Ret(ids)
    tok = _TokAdapter(tok_local)
    def _tok_encode(text: str) -> List[int]:
        return tok_local.encode(text)
    def _tok_len(text: str) -> int:
        try:
            return len(tok_local.encode(text))
        except Exception:
            return 0
    def _tok_decode(ids: torch.Tensor | List[int]) -> str:
        if isinstance(ids, torch.Tensor):
            try:
                ids_l = ids.to("cpu").tolist()
            except Exception:
                ids_l = []
        else:
            ids_l = list(ids)
        try:
            return tok_local.decode(ids_l, skip_special_tokens=True)
        except Exception:
            return tok_local.decode(ids_l)
    torch_dtype = torch.bfloat16 if str(device).startswith("cuda") else torch.float32

    model, _cfg = build_local_llama_from_snapshot(snap_dir, device, torch_dtype, device_map=device_map, gpu_ids=gpu_ids)

    # Load base adapters
    base_layers = load_adapters_npz(adapters_npz)["layers"]

    # Selection via caches (program-agnostic): prefer windows with prompt overlap


    tok_local = LocalLlamaTokenizer(snap_dir)
    class _TokAdapter:
        def __init__(self, base):
            self._b = base
        class _Ret:
            def __init__(self, ids):
                self.input_ids = ids
        def __call__(self, text: str, add_special_tokens: bool = False, return_tensors: str | None = None):
            ids = self._b.encode(text)
            if return_tensors == "pt":
                return {"input_ids": torch.tensor([ids], dtype=torch.long)}
            return _TokAdapter._Ret(ids)
    tok = _TokAdapter(tok_local)
    def _tok_encode(text: str) -> List[int]:
        return tok_local.encode(text)
    def _tok_len(text: str) -> int:
        try:
            return len(tok_local.encode(text))
        except Exception:
            return 0
    def _tok_decode(ids: torch.Tensor | List[int]) -> str:
        if isinstance(ids, torch.Tensor):
            try:
                ids_l = ids.to("cpu").tolist()
            except Exception:
                ids_l = []
        else:
            ids_l = list(ids)
        try:
            return tok_local.decode(ids_l, skip_special_tokens=True)
        except Exception:
            return tok_local.decode(ids_l)
    torch_dtype = torch.bfloat16 if str(device).startswith("cuda") else torch.float32

    model, _cfg = build_local_llama_from_snapshot(snap_dir, device, torch_dtype, device_map=device_map, gpu_ids=gpu_ids)

    # Load base adapters
    base_layers = load_adapters_npz(adapters_npz)["layers"]

    # Selection via caches (program-agnostic): prefer windows with prompt overlap
    files: List[str] = []
    try:
        adapters_dir = os.path.dirname(adapters_npz)
        windows = load_windows_index(adapters_dir)
        files = pick_files_from_windows(root, windows, prompt, k=max(8, int(ff_max_candidates)))
    except Exception:
        files = []
    modules: List[str] = []

    # Subgraph embedding
    # Program-agnostic: skip subgraph embedding if no backend embedder; use base only
    sub = {"layers": []}


            final_prompt = (
                final_prompt
                + f"\n\nInstruction: For EVERY claim, append a citation of the form {example_path}:START-END.\n"
                  "Use only files shown in [ctx] above. Provide at least 3 citations overall.\n"
            )
            # Adapter-aware decoding prompt nudge (pointer-first) for consistency
            if bool(adapter_aware_decoding):
                try:
                    example_rel = files_for_ctx[0] if files_for_ctx else None
                    example_path = example_rel if example_rel else "file.py"
                except Exception:
                    example_path = "file.py"
                final_prompt = (
                    f"[pointer-first] Start with a citation like [{example_path}:A-B], then explain.\n\n"
                    + final_prompt
                )

        # Apply mixed adapters
        x_ids = torch.tensor([[i for i in tok_local.encode(final_prompt)]], dtype=torch.long, device=device)
        x = {"input_ids": x_ids}
        def _parse_target_weights(spec: Optional[str]) -> Optional[Dict[str, float]]:
            if not spec:
                return None
            out: Dict[str, float] = {}
            try:
                for part in str(spec).split(","):
                    part = part.strip()
                    if not part:
                        continue
                    if "=" in part:
                        k, v = part.split("=", 1)
                        k = k.strip(); out[k] = float(v)
                    else:
                        out[part] = 1.0
                return out or None
            except Exception:
                return None
        tw = _parse_target_weights(target_weights) or {}
        # Optional question-aware reweighting
        if bool(q_aware_weights):
            try:
                ql = str(prompt).lower()
                mul: Dict[str, float] = {}
                if any(k in ql for k in ["signature", "param", "argument", "type", "prototype"]):
                    mul.update({"o_proj": 1.10, "v_proj": 1.08})
                if any(k in ql for k in ["why", "fail", "error", "behavior", "incorrect", "bug"]):
                    mul.update({"up_proj": 1.06, "down_proj": 1.05, "gate_proj": 1.04})
                if any(k in ql for k in ["where", "defined", "definition", "locate", "find"]):
                    mul.update({"q_proj": 1.03})
                for k, m in mul.items():
                    tw[k] = float(tw.get(k, 1.0)) * float(m)
            except Exception:
                pass
        # Optional per-layer schedule
        layer_multipliers: Optional[List[float]] = None
        if bool(layer_schedule):
            try:


            ids = self._b.encode(text)
            if return_tensors == "pt":
                return {"input_ids": torch.tensor([ids], dtype=torch.long)}
            return _TokAdapter._Ret(ids)
    tok = _TokAdapter(tok_local)
    torch_dtype = torch.bfloat16 if str(device).startswith("cuda") else torch.float32
    model, _cfg = build_local_llama_from_snapshot(snap_dir, device, torch_dtype, device_map=device_map, gpu_ids=gpu_ids)

    base_layers = load_adapters_npz(adapters_npz)["layers"]
    # Program-agnostic structured path: skip graph embedder; retain main flow
    _ps_path = program_state_path if (program_state_path is not None and str(program_state_path).strip()) else None
    state_prev = load_program_state(root, path=_ps_path)

    must_set: Set[Tuple[str, int, int]] = set()
    union_set: Set[Tuple[str, int, int]] = set()
    selected_mods_acc: Set[str] = set(state_prev.candidates_modules) if monotone_selection else set()
    selected_files_acc: Set[str] = set(state_prev.candidates_files) if monotone_selection else set()
    lfp_passes = 0
    converged = False

    def _one_pass(mods_hint: List[str], files_hint: List[str]) -> Tuple[str, List[str], List[str], Dict[str, Any], Set[Tuple[str,int,int]], Set[Tuple[str,int,int]], Dict[str, Any]]:
        nonlocal model
        # Initial selection (question or zoom), union with hints for monotonicity
        ignore_list = [s for s in (ignore or []) if s]
        adapters_dir = os.path.dirname(adapters_npz)
        windows = load_windows_index(adapters_dir)
        files = pick_files_from_windows(root, windows, prompt, k=max(8, int(ff_max_candidates)))
        # initialize modules list for return and monotone selection merging
        modules: List[str] = []
        if monotone_selection:
            modules = sorted(list(set(mods_hint) | set(modules)))
            files = sorted(list(set(files_hint) | set(files)))
        # Subgraph emb
        # No subgraph embedding in program-agnostic structured path
        # Shapes/targets
        tmap = targets_map("local")
        t_shapes = infer_target_shapes(model)
        num_layers = len(getattr(model, "blocks", []))
        d_model_local = int(t_shapes.get("q_proj", (0, 0))[0]) or int(getattr(getattr(model, "cfg", None), "d_model", 0) or 0)
        # Entropy-aware
        scaled_rank = int(rank)
        scaled_gsub = float(gsub)
            # Build sub adapters
        sub = {"layers": []}
        # Optional cones
        cr = max(0, int(cone_rank))
        if bool(function_first) and cr > 0:
            try:
                pass
            except Exception:
                pass
        # Optional mixture bank: mix top-m module adapters from bank by concatenation (Σ π_i Δθ_i)
        if adapters_bank and int(mixture_m) > 0:
            try:
                import glob
                bank_root = os.path.abspath(os.path.expanduser(os.path.expandvars(adapters_bank)))
                sel_mods = [m for m in (modules or []) if m]
                picked = 0
                for mod in sel_mods:
                    if picked >= int(mixture_m):
                        break
                    mod_dir1 = os.path.join(bank_root, "sub_adapters", mod.replace("/", "_"))
                    mod_dir2 = os.path.join(bank_root, mod.replace("/", "_"))
                    cand = None
                    for d in (mod_dir1, mod_dir2):
                        fp = os.path.join(d, "adapters.npz")
                        if os.path.isfile(fp):
                            cand = fp
                            break
                    if not cand:
                        continue
                    try:
                        bank_ad = load_adapters_npz(cand)
                    except Exception:
                        continue
                    # uniform π for now
                    w = 1.0 / float(min(len(sel_mods), int(mixture_m)))
                    sw = float(max(0.0, min(1.0, w))) ** 0.5
                    # per-layer concat
                    merged_layers = []
                    for i in range(num_layers):
                        baseL = sub["layers"][i]
                        bL = bank_ad["layers"][i] if i < len(bank_ad.get("layers", [])) else {}
                        dst: Dict[str, Dict[str, np.ndarray]] = {}
                        for name in tmap.keys():
                            if (name in baseL) and (name in bL):
                                A1 = baseL[name]["A"]; B1 = baseL[name]["B"]
                                A2 = (sw * bL[name]["A"]).astype(np.float32); B2 = (sw * bL[name]["B"]).astype(np.float32)
                                A = np.concatenate([A1, A2], axis=1)
                                B = np.concatenate([B1, B2], axis=0)
                                dst[name] = {"A": A, "B": B, "gate": baseL[name].get("gate", np.array([0.0], dtype=np.float32))}
                            elif name in baseL:
                                dst[name] = baseL[name]
                            elif name in bL:
                                A = (sw * bL[name]["A"]).astype(np.float32)
                                dst[name] = {"A": A, "B": bL[name]["B"], "gate": bL[name].get("gate", np.array([0.0], dtype=np.float32))}
                        merged_layers.append(dst)
                    sub = {"layers": merged_layers}
                    # try infer rank increment from a present target
                    try:
                        for i in range(num_layers):
                            any_name = next((n for n in tmap.keys() if n in bank_ad["layers"][i]), None)
                            if any_name:
                                inc = int(bank_ad["layers"][i][any_name]["B"].shape[0])
                                scaled_rank = int(scaled_rank + inc)
                                break
                    except Exception:
                        pass
                    picked += 1
            except Exception:
                pass
        # Rounding
        if bool(round_lora):
            try:
                thr = float(max(0.0, round_threshold))
                for i in range(len(sub.get("layers", []))):
                    for name, tensors in sub["layers"][i].items():
                        for key in ("A", "B"):
                            arr = tensors.get(key)
                            if not isinstance(arr, np.ndarray):
                                continue
                            q = float(np.median(np.abs(arr))) if arr.size > 0 else 0.0
                            if q <= 0:
                                continue
                            out = np.where(np.abs(arr) < (thr * q), 0.0, np.sign(arr) * q).astype(np.float32)
                            tensors[key] = out
            except Exception:
                pass

        # Delta norm diagnostics (approximate AB norms per target averaged across layers)
        delta_norms: Dict[str, float] = {}
        try:
            sums: Dict[str, float] = {}
            counts: Dict[str, int] = {}
            for i in range(len(sub.get("layers", []))):
                for name, tensors in sub["layers"][i].items():
                    try:
                        A = tensors.get("A"); B = tensors.get("B")
                        if isinstance(A, np.ndarray) and isinstance(B, np.ndarray):
                            AB = (A @ B)
                            n = float(np.linalg.norm(AB)) if AB.size > 0 else 0.0
                            sums[name] = float(sums.get(name, 0.0) + n)
                            counts[name] = int(counts.get(name, 0) + 1)
                    except Exception:
                        continue
            for k, v in sums.items():
                c = max(1, int(counts.get(k, 1)))
                delta_norms[k] = float(v / float(c))
        except Exception:
            delta_norms = {}

        # Prompt + context
        final_prompt = prompt
        files_for_ctx = list(files)
        if bool(pack_context) and files_for_ctx:
            packed = ""
            ident_header = ""
            if pack_mode == "windows":
                if bool(function_first):
                    # Simplified: pack top window per file like non-structured path
                    adapters_dir = os.path.dirname(adapters_npz)
                    try:
                        windows = load_windows_index(adapters_dir)
                    except Exception:
                        windows = []
                    out_lines: List[str] = ["Program windows (function-first):"]
                    used = 0
                    budget = int(context_tokens)
                    for rel in files_for_ctx:
                        cand = [w for w in windows if (w.get("path") or "").replace("\\", "/") == rel.replace("\\", "/")]
                        if not cand:
                            continue
                        w = cand[0]
                        a = int(w.get("start_line", 1)); b = int(w.get("end_line", a + 60))
                        abs_fp = rel if os.path.isabs(rel) else os.path.abspath(os.path.join(root, rel))
                        try:
                            src_lines = open(abs_fp, "r", encoding="utf-8", errors="ignore").read().splitlines()
                        except Exception:
                            continue
                        a = max(1, min(a, len(src_lines))); b = max(a, min(b, len(src_lines)))
                        block = [f"[ctx] path: {rel}:{a}-{b}"] + src_lines[a - 1 : b] + [""]
                        text_block = "\n".join(block) + "\n"
                        t = len(tok(text_block).input_ids)
                        if used + t > budget:
                            continue
                        out_lines.extend(block)
                        used += t
                        if used >= budget:
                            break
                    packed = "\n".join(out_lines) if len(out_lines) > 1 else ""
                else:
                    # Fallback: identical to function-first simplified path
                    adapters_dir = os.path.dirname(adapters_npz)
                    try:
                        windows = load_windows_index(adapters_dir)
                    except Exception:
                        windows = []
                    out_lines: List[str] = ["Program windows:"]
                    used = 0
                    budget = int(context_tokens)
                    for rel in files_for_ctx:
                        cand = [w for w in windows if (w.get("path") or "").replace("\\", "/") == rel.replace("\\", "/")]
                        if not cand:
                            continue
                        w = cand[0]
                        a = int(w.get("start_line", 1)); b = int(w.get("end_line", a + 60))
                        abs_fp = rel if os.path.isabs(rel) else os.path.abspath(os.path.join(root, rel))
                        try:
                            src_lines = open(abs_fp, "r", encoding="utf-8", errors="ignore").read().splitlines()
                        except Exception:
                            continue
                        a = max(1, min(a, len(src_lines))); b = max(a, min(b, len(src_lines)))
                        block = [f"[ctx] path: {rel}:{a}-{b}"] + src_lines[a - 1 : b] + [""]
                        text_block = "\n".join(block) + "\n"
                        t = len(tok(text_block).input_ids)
                        if used + t > budget:
                            continue
                        out_lines.extend(block)
                        used += t
                        if used >= budget:
                            break
                    packed = "\n".join(out_lines) if len(out_lines) > 1 else ""
            else:
                packed = ""  # heads mode omitted in program-agnostic core
            if packed:
                final_prompt = (ident_header + packed + "\n\n" + final_prompt) if ident_header else (packed + "\n\n" + final_prompt)
        if require_citations:
            try:
                example_rel = files_for_ctx[0] if files_for_ctx else None
                example_path = example_rel if example_rel else "file.py"
            except Exception:
                example_path = "file.py"
            final_prompt = (
                final_prompt
                + f"\n\nInstruction: For EVERY claim, append a citation of the form {example_path}:START-END.\n"
                  "Use only files shown in [ctx] above. Provide at least 3 citations overall.\n"
            )
            # Adapter-aware decoding prompt nudge (pointer-first) for consistency
            if bool(adapter_aware_decoding):
                try:
                    example_rel = files_for_ctx[0] if files_for_ctx else None
                    example_path = example_rel if example_rel else "file.py"
                except Exception:
                    example_path = "file.py"
                final_prompt = (
                    f"[pointer-first] Start with a citation like [{example_path}:A-B], then explain.\n\n"
                    + final_prompt
                )

        # Apply mixed adapters
        x_ids = torch.tensor([[i for i in tok_local.encode(final_prompt)]], dtype=torch.long, device=device)
        x = {"input_ids": x_ids}
        def _parse_target_weights(spec: Optional[str]) -> Optional[Dict[str, float]]:
            if not spec:
                return None
            out: Dict[str, float] = {}
            try:
                for part in str(spec).split(","):
                    part = part.strip()
                    if not part:
                        continue
                    if "=" in part:
                        k, v = part.split("=", 1)
                        k = k.strip(); out[k] = float(v)
                    else:
                        out[part] = 1.0
                return out or None
            except Exception:
                return None
        tw = _parse_target_weights(target_weights) or {}
        # Optional question-aware reweighting
        if bool(q_aware_weights):
            try:
                ql = str(prompt).lower()
                mul: Dict[str, float] = {}
                if any(k in ql for k in ["signature", "param", "argument", "type", "prototype"]):
                    mul.update({"o_proj": 1.10, "v_proj": 1.08})
                if any(k in ql for k in ["why", "fail", "error", "behavior", "incorrect", "bug"]):
                    mul.update({"up_proj": 1.06, "down_proj": 1.05, "gate_proj": 1.04})
                if any(k in ql for k in ["where", "defined", "definition", "locate", "find"]):
                    mul.update({"q_proj": 1.03})
                for k, m in mul.items():
                    tw[k] = float(tw.get(k, 1.0)) * float(m)
            except Exception:
                pass
        # Optional per-layer schedule
        layer_multipliers: Optional[List[float]] = None
        if bool(layer_schedule):
            try:
                L_layers = len(base_layers or [])
                if L_layers > 0:
                    layer_multipliers = []
                    for i in range(L_layers):
                        frac = float(i) / float(max(1, L_layers - 1))
                        if frac < (1.0 / 3.0):
                            lm = 0.95 + 0.15 * (frac / (1.0 / 3.0))
                        elif frac < (2.0 / 3.0):
                            lm = 1.05 + 0.05 * ((frac - (1.0 / 3.0)) / (1.0 / 3.0))
                        else:
                            lm = 1.10 + 0.05 * ((frac - (2.0 / 3.0)) / (1.0 / 3.0))
                        layer_multipliers.append(float(lm))
            except Exception:
                layer_multipliers = None
        # Apply ablations
        if bool(ablate_attn):
            for k in ("q_proj", "k_proj", "v_proj", "o_proj"):
                tw[k] = 0.0
        if bool(ablate_mlp):
            for k in ("up_proj", "down_proj", "gate_proj"):
                tw[k] = 0.0
        # Optional per-target rank trimming
        per_target_keep: Optional[Dict[str, int]] = None
        if bool(per_target_rank_schedule):
            try:
                base_frac: Dict[str, float] = {
                    "o_proj": 1.00, "up_proj": 1.00, "down_proj": 0.90, "gate_proj": 0.80,
                    "q_proj": 0.70, "k_proj": 0.65, "v_proj": 0.60,
                }
                per_target_keep = {}
                for t, frac in base_frac.items():
                    keep = int(max(1, min(int(scaled_rank), round(int(scaled_rank) * float(frac)))))
                    per_target_keep[t] = keep
                budget = int(max(0, int(rank_budget)))
                if budget > 0 and per_target_keep:
                    total = int(sum(int(v) for v in per_target_keep.values()))
                    if total > budget:
                        scale = float(budget) / float(max(1, total))
                        for t in list(per_target_keep.keys()):
                            per_target_keep[t] = int(max(1, round(int(per_target_keep[t]) * scale)))
            except Exception:
                per_target_keep = None
        # Optional per-layer rank tiers
        per_target_keep_layers: Optional[List[Dict[str, int]]] = None
        if bool(layer_rank_tiers):
            try:
                L_layers = len(base_layers or [])
                if L_layers > 0:
                    per_target_keep_layers = []
                    def tier_for(frac: float) -> str:
                        if frac < (1.0/3.0): return "low"
                        if frac < (2.0/3.0): return "mid"
                        return "top"
                    for i in range(L_layers):
                        frac = float(i) / float(max(1, L_layers - 1))
                        tier = tier_for(frac)
                        if tier == "low":
                            vals = {"q_proj": 2, "k_proj": 2, "v_proj": 8, "o_proj": 8, "up_proj": 12, "down_proj": 12}
                            vals["gate_proj"] = min(8, int(0.5 * vals["up_proj"]))
                        elif tier == "mid":
                            vals = {"q_proj": 3, "k_proj": 3, "v_proj": 12, "o_proj": 12, "up_proj": 16, "down_proj": 16}
                            vals["gate_proj"] = min(8, int(0.5 * vals["up_proj"]))
                        else:
                            vals = {"q_proj": 4, "k_proj": 4, "v_proj": 16, "o_proj": 16, "up_proj": 24, "down_proj": 24}
                            vals["gate_proj"] = min(8, int(0.5 * vals["up_proj"]))
                        for k in list(vals.keys()):
                            vals[k] = int(max(1, min(int(scaled_rank), int(vals[k]))))
                        per_target_keep_layers.append(vals)
            except Exception:
                per_target_keep_layers = None
        # Alpha warmup on first structured pass
        alpha_used = float(alpha * (0.5 if (bool(alpha_warmup) and int(lfp_passes) == 0) else 1.0))
        hooks = register_hook_mixed_adapters(
            model,
            base_layers,
            sub.get("layers"),
            alpha_star=float(alpha_used),
            g_sub=float(scaled_gsub),
            rank=int(scaled_rank),
            beta=float(beta),
            target_weights=tw,
            backend="local",
            layer_multipliers=layer_multipliers,
            per_target_keep=per_target_keep,
            per_target_keep_layers=per_target_keep_layers,
            delta_cap=float(max(0.0, delta_cap)),
        )

        # Generate (possibly multiple samples)
        union_cites: Set[Tuple[str,int,int]] = set()
        inter_cites: Optional[Set[Tuple[str,int,int]]] = None
        text_first = ""
        gen_err = None
        oom_retry = False
        for si in range(max(1, int(samples))):
            try:
                gen_kwargs = {"max_new_tokens": int(max_new_tokens), "do_sample": bool(do_sample)}
                if do_sample:
                    gen_kwargs.update({
                        "temperature": float(temperature),
# ... truncated ...


        torch.manual_seed(int(seed))
    except Exception:
        pass

    # Cache
    if not cache_dir:
        env_cache = os.environ.get("TRANSFORMER_CACHE_DIR") or os.environ.get("HF_HOME")
        cache_dir = env_cache or os.path.join(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")), "checkpoints")
    try:
        os.makedirs(cache_dir, exist_ok=True)
    except Exception:
        pass

    # Local tokenizer (no transformers) and adapter for context/scoring utilities
    snap_dir = ensure_snapshot(model_id, cache_dir)
    tok_local = LocalLlamaTokenizer(snap_dir)
    class _TokAdapter:
        def __init__(self, base):
            self._b = base
        class _Ret:
            def __init__(self, ids):
                self.input_ids = ids
        def __call__(self, text: str, add_special_tokens: bool = False, return_tensors: str | None = None):
            ids = self._b.encode(text)
            if return_tensors == "pt":
                return {"input_ids": torch.tensor([ids], dtype=torch.long)}
            return _TokAdapter._Ret(ids)
    tok = _TokAdapter(tok_local)
    torch_dtype = torch.bfloat16 if str(device).startswith("cuda") else torch.float32
    model, _cfg = build_local_llama_from_snapshot(snap_dir, device, torch_dtype, device_map=device_map, gpu_ids=gpu_ids)

    base_layers = load_adapters_npz(adapters_npz)["layers"]
    # Program-agnostic structured path: skip graph embedder; retain main flow
    _ps_path = program_state_path if (program_state_path is not None and str(program_state_path).strip()) else None
    state_prev = load_program_state(root, path=_ps_path)

    must_set: Set[Tuple[str, int, int]] = set()
    union_set: Set[Tuple[str, int, int]] = set()
    selected_mods_acc: Set[str] = set(state_prev.candidates_modules) if monotone_selection else set()
    selected_files_acc: Set[str] = set(state_prev.candidates_files) if monotone_selection else set()
    lfp_passes = 0
    converged = False


        np.random.seed(int(seed))
        torch.manual_seed(int(seed))
    except Exception:
        pass

    # Cache
    if not cache_dir:
        env_cache = os.environ.get("TRANSFORMER_CACHE_DIR") or os.environ.get("HF_HOME")
        cache_dir = env_cache or os.path.join(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")), "checkpoints")
    try:
        os.makedirs(cache_dir, exist_ok=True)
    except Exception:
        pass

    # Local tokenizer (no transformers) and adapter for context/scoring utilities
    snap_dir = ensure_snapshot(model_id, cache_dir)
    tok_local = LocalLlamaTokenizer(snap_dir)
    class _TokAdapter:
        def __init__(self, base):
            self._b = base
        class _Ret:
            def __init__(self, ids):
                self.input_ids = ids
        def __call__(self, text: str, add_special_tokens: bool = False, return_tensors: str | None = None):
            ids = self._b.encode(text)
            if return_tensors == "pt":
                return {"input_ids": torch.tensor([ids], dtype=torch.long)}
            return _TokAdapter._Ret(ids)
    tok = _TokAdapter(tok_local)
    torch_dtype = torch.bfloat16 if str(device).startswith("cuda") else torch.float32
    model, _cfg = build_local_llama_from_snapshot(snap_dir, device, torch_dtype, device_map=device_map, gpu_ids=gpu_ids)

    base_layers = load_adapters_npz(adapters_npz)["layers"]
    # Program-agnostic structured path: skip graph embedder; retain main flow
    _ps_path = program_state_path if (program_state_path is not None and str(program_state_path).strip()) else None
    state_prev = load_program_state(root, path=_ps_path)

    must_set: Set[Tuple[str, int, int]] = set()
    union_set: Set[Tuple[str, int, int]] = set()
    selected_mods_acc: Set[str] = set(state_prev.candidates_modules) if monotone_selection else set()
    selected_files_acc: Set[str] = set(state_prev.candidates_files) if monotone_selection else set()
    lfp_passes = 0
    converged = False


        pass

    # Cache
    if not cache_dir:
        env_cache = os.environ.get("TRANSFORMER_CACHE_DIR") or os.environ.get("HF_HOME")
        cache_dir = env_cache or os.path.join(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")), "checkpoints")
    try:
        os.makedirs(cache_dir, exist_ok=True)
    except Exception:
        pass

    # Local tokenizer (no transformers) and adapter for context/scoring utilities
    snap_dir = ensure_snapshot(model_id, cache_dir)
    tok_local = LocalLlamaTokenizer(snap_dir)
    class _TokAdapter:
        def __init__(self, base):
            self._b = base
        class _Ret:
            def __init__(self, ids):
                self.input_ids = ids
        def __call__(self, text: str, add_special_tokens: bool = False, return_tensors: str | None = None):
            ids = self._b.encode(text)
            if return_tensors == "pt":
                return {"input_ids": torch.tensor([ids], dtype=torch.long)}
            return _TokAdapter._Ret(ids)
    tok = _TokAdapter(tok_local)
    torch_dtype = torch.bfloat16 if str(device).startswith("cuda") else torch.float32
    model, _cfg = build_local_llama_from_snapshot(snap_dir, device, torch_dtype, device_map=device_map, gpu_ids=gpu_ids)

    base_layers = load_adapters_npz(adapters_npz)["layers"]
    # Program-agnostic structured path: skip graph embedder; retain main flow
    _ps_path = program_state_path if (program_state_path is not None and str(program_state_path).strip()) else None
    state_prev = load_program_state(root, path=_ps_path)

    must_set: Set[Tuple[str, int, int]] = set()
    union_set: Set[Tuple[str, int, int]] = set()
    selected_mods_acc: Set[str] = set(state_prev.candidates_modules) if monotone_selection else set()
    selected_files_acc: Set[str] = set(state_prev.candidates_files) if monotone_selection else set()
    lfp_passes = 0
    converged = False

    def _one_pass(mods_hint: List[str], files_hint: List[str]) -> Tuple[str, List[str], List[str], Dict[str, Any], Set[Tuple[str,int,int]], Set[Tuple[str,int,int]], Dict[str, Any]]:
        nonlocal model
        # Initial selection (question or zoom), union with hints for monotonicity
        ignore_list = [s for s in (ignore or []) if s]


        "selection": {
            "modules": sorted(list(selected_mods_acc)),
            "files": sorted(list(selected_files_acc)),
        },
        "lfp_passes": int(lfp_passes),
        "converged": bool(converged),
        "provenance": {"commit": commit_sha},
        "confidence": float(confidence),
        "metrics": {
            "citations_total": int(len(union_set)),
            "citations_must": int(len(must)),
            "modules_selected": int(len(selected_mods_acc)),
            "files_selected": int(len(selected_files_acc)),
            "elapsed_sec": elapsed,
            "retries": int(retries_used),
            "delta_norms": (diag.get("delta_norms") if isinstance(diag, dict) else None),
        },
    }
    # Optional signature edit distance (rough, per-citation snippet vs answer)
    try:
        def _lev_norm(a: str, b: str) -> float:
            a = a.strip()[:256]; b = b.strip()[:1024]
            if not a or not b:
                return 1.0
            la, lb = len(a), len(b)
            prev = list(range(lb + 1))
            cur = [0] * (lb + 1)
            for i in range(1, la + 1):
                cur[0] = i
                ai = a[i - 1]
                for j in range(1, lb + 1):
                    cost = 0 if ai == b[j - 1] else 1
                    cur[j] = min(cur[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost)
                prev, cur = cur, prev
            dist = float(prev[lb])
            norm = dist / float(max(1, max(la, lb)))
            return max(0.0, min(1.0, norm))
        ed_vals: List[float] = []
        ans_txt = result["text"] or ""
        for (rel, a_ln, b_ln) in result.get("citations", [])[:6]:
            try:
                pth = rel if os.path.isabs(rel) else os.path.abspath(os.path.join(root, rel))
                with open(pth, "r", encoding="utf-8", errors="ignore") as fh:
                    lines = fh.read().splitlines()
                a0 = max(1, int(a_ln)); b0 = min(len(lines), int(b_ln))
                snippet = "\n".join(lines[a0 - 1 : b0])
                ed_vals.append(_lev_norm(snippet, ans_txt))
            except Exception:
                continue
        if ed_vals:
            result["metrics"]["signature_edit_mean"] = float(sum(ed_vals) / float(len(ed_vals)))
            result["metrics"]["signature_edit_min"] = float(min(ed_vals))
    except Exception:
        pass
    # Optional tests verification (best-effort, core is program-agnostic)
    if bool(telemetry_verify_tests):
        # Left as a no-op in core. Example backends can perform verification and


                per_target_keep = {}
                for t, frac in base_frac.items():
                    keep = int(max(1, min(int(scaled_rank), round(int(scaled_rank) * float(frac)))))
                    per_target_keep[t] = keep
                budget = int(max(0, int(rank_budget)))
                if budget > 0 and per_target_keep:
                    total = int(sum(int(v) for v in per_target_keep.values()))
                    if total > budget:
                        scale = float(budget) / float(max(1, total))
                        for t in list(per_target_keep.keys()):
                            per_target_keep[t] = int(max(1, round(int(per_target_keep[t]) * scale)))
            except Exception:
                per_target_keep = None
        # Optional per-layer rank tiers
        per_target_keep_layers: Optional[List[Dict[str, int]]] = None
        if bool(layer_rank_tiers):
            try:
                L_layers = len(base_layers or [])
                if L_layers > 0:
                    per_target_keep_layers = []
                    def tier_for(frac: float) -> str:
                        if frac < (1.0/3.0): return "low"
                        if frac < (2.0/3.0): return "mid"
                        return "top"
                    for i in range(L_layers):
                        frac = float(i) / float(max(1, L_layers - 1))
                        tier = tier_for(frac)
                        if tier == "low":
                            vals = {"q_proj": 2, "k_proj": 2, "v_proj": 8, "o_proj": 8, "up_proj": 12, "down_proj": 12}
                            vals["gate_proj"] = min(8, int(0.5 * vals["up_proj"]))
                        elif tier == "mid":
                            vals = {"q_proj": 3, "k_proj": 3, "v_proj": 12, "o_proj": 12, "up_proj": 16, "down_proj": 16}
                            vals["gate_proj"] = min(8, int(0.5 * vals["up_proj"]))
                        else:
                            vals = {"q_proj": 4, "k_proj": 4, "v_proj": 16, "o_proj": 16, "up_proj": 24, "down_proj": 24}
                            vals["gate_proj"] = min(8, int(0.5 * vals["up_proj"]))
                        for k in list(vals.keys()):
                            vals[k] = int(max(1, min(int(scaled_rank), int(vals[k]))))
                        per_target_keep_layers.append(vals)
            except Exception:
                per_target_keep_layers = None
        # Alpha warmup on first structured pass
        alpha_used = float(alpha * (0.5 if (bool(alpha_warmup) and int(lfp_passes) == 0) else 1.0))
        hooks = register_hook_mixed_adapters(


modules.citations

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Dict, List, Any, Optional, Tuple
import hashlib
import time
import re

from .program_graph import ProgramGraph, Span, ResolvedAnchor

@dataclass
class CitationPolicy:
    enforce: bool = True
    per_paragraph: bool = False
    repair: bool = True

def _extract_identifier_tokens(text: str) -> List[str]:
    toks = re.findall(r"`([A-Za-z_][A-Za-z0-9_\.]+)`", text or "")
    toks2 = re.findall(r"\b([A-Za-z_][A-Za-z0-9_]*)\s*\(", text or "")
    out: List[str] = []
    for t in toks + toks2:
        tt = t.split(".")[-1]
        if tt and tt not in out:
            out.append(tt)
    return out[:24]

class CitationManager:
    def __init__(self, policy: CitationPolicy, pg: ProgramGraph, manifest: Dict[str, Any]):
        self.policy = policy
        self.pg = pg
        self.manifest = manifest or {}

    def collect(self, region_entity_ids: List[str], contexts: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        # Best-effort: attach evidence from region seeds (first-class Program URIs would be passed by caller)
        evidence: List[Dict[str, Any]] = []
        for e in self.pg.entities():
            if e.id in region_entity_ids:
                try:
                    # Resolve existing entity URI to get artifact span/hash
                    ra = self.pg.resolve(e.uri)
                    evidence.append({
                        "uri": e.uri,
                        "artifact_hash": ra.hash,
                        "span": {"start": {"line": ra.span.start_line}, "end": {"line": ra.span.end_line}},
                        "kind": e.kind,
                        "confidence": 0.5,
                        "retrieval": {"score": 0.0, "features": {}},
                    })
                except Exception:
                    continue
        return evidence

    def enforce(self, draft_units: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        if not self.policy.enforce:
            return draft_units
        out = []
        for u in draft_units:
            ev = list(u.get("evidence") or [])
            if self.policy.per_paragraph:
                paras = [p for p in (u.get("text") or "").split("\n\n") if p.strip()]
                if not paras:
                    out.append(u)
                    continue
                # If any paragraph lacks evidence, attempt repair (or drop)
                repaired = self.repair(u) if self.policy.repair else u
                out.append(repaired)
            else:
                if not ev:
                    repaired = self.repair(u) if self.policy.repair else u
                    out.append(repaired)
                else:
                    out.append(u)
        return out

    def repair(self, unit: Dict[str, Any]) -> Dict[str, Any]:
        # Try anchoring based on identifier tokens
        text = str(unit.get("text") or "")
        tokens = _extract_identifier_tokens(text)
        anchors: List[Dict[str, Any]] = []
        for t in tokens:
            try:
                refs = list(self.pg.search_refs(t))
            except Exception:
                refs = []
            for (eid, sp) in refs[:2]:
                # find entity to get its canonical URI (if present)
                euri = None
                for e in self.pg.entities():
                    if e.id == eid:
                        euri = e.uri
                        break
                try:
                    if euri:
                        ra = self.pg.resolve(euri)
                        anchors.append({
                            "uri": euri,
                            "artifact_hash": ra.hash,
                            "span": {"start": {"line": ra.span.start_line}, "end": {"line": ra.span.end_line}},
                            "kind": (e.kind if euri and e else "entity"),
                            "confidence": 0.4,
                            "retrieval": {"score": 0.0, "features": {"repair": True}},
                        })
                except Exception:
                    continue
            if len(anchors) >= 4:
                break
        if anchors:
            unit = dict(unit)
            unit["evidence"] = anchors
        return unit

    def stamp_provenance(self, unit: Dict[str, Any]) -> Dict[str, Any]:
        prov = {
            "program_id": self.manifest.get("program_id"),
            "manifest_sha": self._manifest_sha(),
            "commit": self.manifest.get("commit"),
            "policy": {
                "enforce": bool(self.policy.enforce),
                "per_paragraph": bool(self.policy.per_paragraph),
                "repair": bool(self.policy.repair),
            },
            "ts": time.time(),
        }
        out = dict(unit)
        out["provenance"] = prov
        return out

    def _manifest_sha(self) -> Optional[str]:
        try:
            blob = str(self.manifest).encode("utf-8", errors="ignore")
            return hashlib.sha256(blob).hexdigest()
        except Exception:
            return None


CitationPolicy

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Dict, List, Any, Optional, Tuple
import hashlib
import time
import re

from .program_graph import ProgramGraph, Span, ResolvedAnchor

@dataclass
class CitationPolicy:
    enforce: bool = True
    per_paragraph: bool = False
    repair: bool = True

def _extract_identifier_tokens(text: str) -> List[str]:
    toks = re.findall(r"`([A-Za-z_][A-Za-z0-9_\.]+)`", text or "")
    toks2 = re.findall(r"\b([A-Za-z_][A-Za-z0-9_]*)\s*\(", text or "")
    out: List[str] = []
    for t in toks + toks2:
        tt = t.split(".")[-1]
        if tt and tt not in out:
            out.append(tt)
    return out[:24]

class CitationManager:
    def __init__(self, policy: CitationPolicy, pg: ProgramGraph, manifest: Dict[str, Any]):
        self.policy = policy
        self.pg = pg
        self.manifest = manifest or {}

    def collect(self, region_entity_ids: List[str], contexts: List[Dict[str, Any]]) -> List[Dict[str, Any]]:


from __future__ import annotations

from dataclasses import dataclass, field
from typing import Dict, List, Any, Optional, Tuple
import hashlib
import time
import re

from .program_graph import ProgramGraph, Span, ResolvedAnchor

@dataclass
class CitationPolicy:
    enforce: bool = True
    per_paragraph: bool = False
    repair: bool = True

def _extract_identifier_tokens(text: str) -> List[str]:
    toks = re.findall(r"`([A-Za-z_][A-Za-z0-9_\.]+)`", text or "")
    toks2 = re.findall(r"\b([A-Za-z_][A-Za-z0-9_]*)\s*\(", text or "")
    out: List[str] = []
    for t in toks + toks2:
        tt = t.split(".")[-1]
        if tt and tt not in out:
            out.append(tt)
    return out[:24]

class CitationManager:
    def __init__(self, policy: CitationPolicy, pg: ProgramGraph, manifest: Dict[str, Any]):
        self.policy = policy
        self.pg = pg
        self.manifest = manifest or {}

    def collect(self, region_entity_ids: List[str], contexts: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        # Best-effort: attach evidence from region seeds (first-class Program URIs would be passed by caller)
        evidence: List[Dict[str, Any]] = []
        for e in self.pg.entities():
            if e.id in region_entity_ids:
                try:
                    # Resolve existing entity URI to get artifact span/hash
                    ra = self.pg.resolve(e.uri)
                    evidence.append({
                        "uri": e.uri,
                        "artifact_hash": ra.hash,
                        "span": {"start": {"line": ra.span.start_line}, "end": {"line": ra.span.end_line}},


CitationManager

@dataclass
class CitationPolicy:
    enforce: bool = True
    per_paragraph: bool = False
    repair: bool = True

def _extract_identifier_tokens(text: str) -> List[str]:
    toks = re.findall(r"`([A-Za-z_][A-Za-z0-9_\.]+)`", text or "")
    toks2 = re.findall(r"\b([A-Za-z_][A-Za-z0-9_]*)\s*\(", text or "")
    out: List[str] = []
    for t in toks + toks2:
        tt = t.split(".")[-1]
        if tt and tt not in out:
            out.append(tt)
    return out[:24]

class CitationManager:
    def __init__(self, policy: CitationPolicy, pg: ProgramGraph, manifest: Dict[str, Any]):
        self.policy = policy
        self.pg = pg
        self.manifest = manifest or {}

    def collect(self, region_entity_ids: List[str], contexts: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        # Best-effort: attach evidence from region seeds (first-class Program URIs would be passed by caller)
        evidence: List[Dict[str, Any]] = []
        for e in self.pg.entities():
            if e.id in region_entity_ids:
                try:
                    # Resolve existing entity URI to get artifact span/hash
                    ra = self.pg.resolve(e.uri)
                    evidence.append({
                        "uri": e.uri,
                        "artifact_hash": ra.hash,
                        "span": {"start": {"line": ra.span.start_line}, "end": {"line": ra.span.end_line}},
                        "kind": e.kind,
                        "confidence": 0.5,
                        "retrieval": {"score": 0.0, "features": {}},
                    })
                except Exception:
                    continue
        return evidence

    def enforce(self, draft_units: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        if not self.policy.enforce:
            return draft_units
        out = []
        for u in draft_units:
            ev = list(u.get("evidence") or [])
            if self.policy.per_paragraph:
                paras = [p for p in (u.get("text") or "").split("\n\n") if p.strip()]
                if not paras:
                    out.append(u)
                    continue
                # If any paragraph lacks evidence, attempt repair (or drop)
                repaired = self.repair(u) if self.policy.repair else u
                out.append(repaired)
            else:
                if not ev:
                    repaired = self.repair(u) if self.policy.repair else u
                    out.append(repaired)
                else:
                    out.append(u)
        return out

    def repair(self, unit: Dict[str, Any]) -> Dict[str, Any]:
        # Try anchoring based on identifier tokens
        text = str(unit.get("text") or "")
        tokens = _extract_identifier_tokens(text)
        anchors: List[Dict[str, Any]] = []
        for t in tokens:
            try:
                refs = list(self.pg.search_refs(t))
            except Exception:
                refs = []
            for (eid, sp) in refs[:2]:
                # find entity to get its canonical URI (if present)
                euri = None
                for e in self.pg.entities():
                    if e.id == eid:
                        euri = e.uri
                        break
                try:
                    if euri:
                        ra = self.pg.resolve(euri)
                        anchors.append({
                            "uri": euri,
                            "artifact_hash": ra.hash,
                            "span": {"start": {"line": ra.span.start_line}, "end": {"line": ra.span.end_line}},
                            "kind": (e.kind if euri and e else "entity"),
                            "confidence": 0.4,
                            "retrieval": {"score": 0.0, "features": {"repair": True}},
                        })
                except Exception:
                    continue
            if len(anchors) >= 4:
                break
        if anchors:
            unit = dict(unit)
            unit["evidence"] = anchors
        return unit

    def stamp_provenance(self, unit: Dict[str, Any]) -> Dict[str, Any]:
        prov = {
            "program_id": self.manifest.get("program_id"),
            "manifest_sha": self._manifest_sha(),
            "commit": self.manifest.get("commit"),
            "policy": {
                "enforce": bool(self.policy.enforce),
                "per_paragraph": bool(self.policy.per_paragraph),
                "repair": bool(self.policy.repair),
            },
            "ts": time.time(),
        }
        out = dict(unit)
        out["provenance"] = prov
        return out

    def _manifest_sha(self) -> Optional[str]:
        try:
            blob = str(self.manifest).encode("utf-8", errors="ignore")
            return hashlib.sha256(blob).hexdigest()
        except Exception:
            return None


@dataclass
class CitationPolicy:
    enforce: bool = True
    per_paragraph: bool = False
    repair: bool = True

def _extract_identifier_tokens(text: str) -> List[str]:
    toks = re.findall(r"`([A-Za-z_][A-Za-z0-9_\.]+)`", text or "")
    toks2 = re.findall(r"\b([A-Za-z_][A-Za-z0-9_]*)\s*\(", text or "")
    out: List[str] = []
    for t in toks + toks2:
        tt = t.split(".")[-1]
        if tt and tt not in out:
            out.append(tt)
    return out[:24]

class CitationManager:
    def __init__(self, policy: CitationPolicy, pg: ProgramGraph, manifest: Dict[str, Any]):
        self.policy = policy
        self.pg = pg
        self.manifest = manifest or {}

    def collect(self, region_entity_ids: List[str], contexts: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        # Best-effort: attach evidence from region seeds (first-class Program URIs would be passed by caller)
        evidence: List[Dict[str, Any]] = []
        for e in self.pg.entities():
            if e.id in region_entity_ids:
                try:
                    # Resolve existing entity URI to get artifact span/hash
                    ra = self.pg.resolve(e.uri)
                    evidence.append({
                        "uri": e.uri,
                        "artifact_hash": ra.hash,
                        "span": {"start": {"line": ra.span.start_line}, "end": {"line": ra.span.end_line}},
                        "kind": e.kind,
                        "confidence": 0.5,
                        "retrieval": {"score": 0.0, "features": {}},
                    })
                except Exception:
                    continue
        return evidence


collect

    repair: bool = True

def _extract_identifier_tokens(text: str) -> List[str]:
    toks = re.findall(r"`([A-Za-z_][A-Za-z0-9_\.]+)`", text or "")
    toks2 = re.findall(r"\b([A-Za-z_][A-Za-z0-9_]*)\s*\(", text or "")
    out: List[str] = []
    for t in toks + toks2:
        tt = t.split(".")[-1]
        if tt and tt not in out:
            out.append(tt)
    return out[:24]

class CitationManager:
    def __init__(self, policy: CitationPolicy, pg: ProgramGraph, manifest: Dict[str, Any]):
        self.policy = policy
        self.pg = pg
        self.manifest = manifest or {}

    def collect(self, region_entity_ids: List[str], contexts: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        # Best-effort: attach evidence from region seeds (first-class Program URIs would be passed by caller)
        evidence: List[Dict[str, Any]] = []
        for e in self.pg.entities():
            if e.id in region_entity_ids:
                try:
                    # Resolve existing entity URI to get artifact span/hash
                    ra = self.pg.resolve(e.uri)
                    evidence.append({
                        "uri": e.uri,
                        "artifact_hash": ra.hash,
                        "span": {"start": {"line": ra.span.start_line}, "end": {"line": ra.span.end_line}},
                        "kind": e.kind,
                        "confidence": 0.5,
                        "retrieval": {"score": 0.0, "features": {}},
                    })
                except Exception:
                    continue
        return evidence

    def enforce(self, draft_units: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        if not self.policy.enforce:
            return draft_units
        out = []
        for u in draft_units:
            ev = list(u.get("evidence") or [])
            if self.policy.per_paragraph:
                paras = [p for p in (u.get("text") or "").split("\n\n") if p.strip()]
                if not paras:
                    out.append(u)
                    continue
                # If any paragraph lacks evidence, attempt repair (or drop)
                repaired = self.repair(u) if self.policy.repair else u
                out.append(repaired)
            else:
                if not ev:
                    repaired = self.repair(u) if self.policy.repair else u
                    out.append(repaired)
                else:


enforce

    def collect(self, region_entity_ids: List[str], contexts: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        # Best-effort: attach evidence from region seeds (first-class Program URIs would be passed by caller)
        evidence: List[Dict[str, Any]] = []
        for e in self.pg.entities():
            if e.id in region_entity_ids:
                try:
                    # Resolve existing entity URI to get artifact span/hash
                    ra = self.pg.resolve(e.uri)
                    evidence.append({
                        "uri": e.uri,
                        "artifact_hash": ra.hash,
                        "span": {"start": {"line": ra.span.start_line}, "end": {"line": ra.span.end_line}},
                        "kind": e.kind,
                        "confidence": 0.5,
                        "retrieval": {"score": 0.0, "features": {}},
                    })
                except Exception:
                    continue
        return evidence

    def enforce(self, draft_units: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        if not self.policy.enforce:
            return draft_units
        out = []
        for u in draft_units:
            ev = list(u.get("evidence") or [])
            if self.policy.per_paragraph:
                paras = [p for p in (u.get("text") or "").split("\n\n") if p.strip()]
                if not paras:
                    out.append(u)
                    continue
                # If any paragraph lacks evidence, attempt repair (or drop)
                repaired = self.repair(u) if self.policy.repair else u
                out.append(repaired)
            else:
                if not ev:
                    repaired = self.repair(u) if self.policy.repair else u
                    out.append(repaired)
                else:
                    out.append(u)
        return out

    def repair(self, unit: Dict[str, Any]) -> Dict[str, Any]:
        # Try anchoring based on identifier tokens
        text = str(unit.get("text") or "")
        tokens = _extract_identifier_tokens(text)
        anchors: List[Dict[str, Any]] = []
        for t in tokens:
            try:
                refs = list(self.pg.search_refs(t))
            except Exception:
                refs = []
            for (eid, sp) in refs[:2]:
                # find entity to get its canonical URI (if present)
                euri = None
                for e in self.pg.entities():
                    if e.id == eid:
                        euri = e.uri
                        break
                try:
                    if euri:


repair

            return draft_units
        out = []
        for u in draft_units:
            ev = list(u.get("evidence") or [])
            if self.policy.per_paragraph:
                paras = [p for p in (u.get("text") or "").split("\n\n") if p.strip()]
                if not paras:
                    out.append(u)
                    continue
                # If any paragraph lacks evidence, attempt repair (or drop)
                repaired = self.repair(u) if self.policy.repair else u
                out.append(repaired)
            else:
                if not ev:
                    repaired = self.repair(u) if self.policy.repair else u
                    out.append(repaired)
                else:
                    out.append(u)
        return out

    def repair(self, unit: Dict[str, Any]) -> Dict[str, Any]:
        # Try anchoring based on identifier tokens
        text = str(unit.get("text") or "")
        tokens = _extract_identifier_tokens(text)
        anchors: List[Dict[str, Any]] = []
        for t in tokens:
            try:
                refs = list(self.pg.search_refs(t))
            except Exception:
                refs = []
            for (eid, sp) in refs[:2]:
                # find entity to get its canonical URI (if present)
                euri = None
                for e in self.pg.entities():
                    if e.id == eid:
                        euri = e.uri
                        break
                try:
                    if euri:
                        ra = self.pg.resolve(euri)
                        anchors.append({
                            "uri": euri,
                            "artifact_hash": ra.hash,
                            "span": {"start": {"line": ra.span.start_line}, "end": {"line": ra.span.end_line}},
                            "kind": (e.kind if euri and e else "entity"),
                            "confidence": 0.4,
                            "retrieval": {"score": 0.0, "features": {"repair": True}},
                        })
                except Exception:
                    continue
            if len(anchors) >= 4:
                break
        if anchors:
            unit = dict(unit)
            unit["evidence"] = anchors
        return unit

    def stamp_provenance(self, unit: Dict[str, Any]) -> Dict[str, Any]:
        prov = {
            "program_id": self.manifest.get("program_id"),
            "manifest_sha": self._manifest_sha(),
            "commit": self.manifest.get("commit"),
            "policy": {
                "enforce": bool(self.policy.enforce),
                "per_paragraph": bool(self.policy.per_paragraph),
                "repair": bool(self.policy.repair),
            },
            "ts": time.time(),
        }
        out = dict(unit)
        out["provenance"] = prov
        return out

    def _manifest_sha(self) -> Optional[str]:
        try:
            blob = str(self.manifest).encode("utf-8", errors="ignore")


                try:
                    if euri:
                        ra = self.pg.resolve(euri)
                        anchors.append({
                            "uri": euri,
                            "artifact_hash": ra.hash,
                            "span": {"start": {"line": ra.span.start_line}, "end": {"line": ra.span.end_line}},
                            "kind": (e.kind if euri and e else "entity"),
                            "confidence": 0.4,
                            "retrieval": {"score": 0.0, "features": {"repair": True}},
                        })
                except Exception:
                    continue
            if len(anchors) >= 4:
                break
        if anchors:
            unit = dict(unit)
            unit["evidence"] = anchors
        return unit

    def stamp_provenance(self, unit: Dict[str, Any]) -> Dict[str, Any]:
        prov = {
            "program_id": self.manifest.get("program_id"),
            "manifest_sha": self._manifest_sha(),
            "commit": self.manifest.get("commit"),
            "policy": {
                "enforce": bool(self.policy.enforce),
                "per_paragraph": bool(self.policy.per_paragraph),
                "repair": bool(self.policy.repair),
            },
            "ts": time.time(),
        }
        out = dict(unit)
        out["provenance"] = prov
        return out

    def _manifest_sha(self) -> Optional[str]:
        try:
            blob = str(self.manifest).encode("utf-8", errors="ignore")
            return hashlib.sha256(blob).hexdigest()
        except Exception:
            return None


            unit = dict(unit)
            unit["evidence"] = anchors
        return unit

    def stamp_provenance(self, unit: Dict[str, Any]) -> Dict[str, Any]:
        prov = {
            "program_id": self.manifest.get("program_id"),
            "manifest_sha": self._manifest_sha(),
            "commit": self.manifest.get("commit"),
            "policy": {
                "enforce": bool(self.policy.enforce),
                "per_paragraph": bool(self.policy.per_paragraph),
                "repair": bool(self.policy.repair),
            },
            "ts": time.time(),
        }
        out = dict(unit)
        out["provenance"] = prov
        return out

    def _manifest_sha(self) -> Optional[str]:
        try:
            blob = str(self.manifest).encode("utf-8", errors="ignore")
            return hashlib.sha256(blob).hexdigest()
        except Exception:
            return None


from __future__ import annotations

import os
import sys
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Any, List, Tuple

import torch
import torch.nn.functional as F
from torch.utils.data import DataLoader

@dataclass
class EvalPaths:
	adapters_dir: Path
	out_dir: Path
	state_path: Path

def _ensure_dirs(paths: EvalPaths) -> None:
	paths.adapters_dir.mkdir(parents=True, exist_ok=True)
	paths.out_dir.mkdir(parents=True, exist_ok=True)

def main() -> None:
	example_dir = Path(__file__).resolve().parent
	artifacts_root = example_dir / "datasets" / "hf_lora_mbpp"
	paths = EvalPaths(
		adapters_dir=artifacts_root / "lora_adapter",
		out_dir=artifacts_root / "outputs",
		state_path=artifacts_root / ".program_state.json",
	)
	_ensure_dirs(paths)

	# If not specified, prefer GPUs 0,1
	if not os.getenv("CUDA_VISIBLE_DEVICES"):
		os.environ["CUDA_VISIBLE_DEVICES"] = "0,1"

	model_id = os.getenv("LLAMA_MODEL_ID", "meta-llama/Llama-3.1-8B-Instruct")
	adapter_path = (os.getenv("ADAPTER_PATH") or str(paths.adapters_dir)).strip()
	context_len = int(os.getenv("MAX_LEN", "256"))
	per_device_batch = int(os.getenv("BATCH_SIZE", "1"))
	grad_accum = int(os.getenv("GRAD_ACCUM_STEPS", "8"))
	logging_steps = int(os.getenv("LOG_STEPS", "25"))
	seed = int(os.getenv("SEED", "0"))
	use_qlora = os.getenv("USE_QLORA", "1") == "1"

	torch.manual_seed(seed)
	if torch.cuda.is_available():
		torch.cuda.manual_seed_all(seed)

	from transformers import AutoTokenizer, AutoModelForCausalLM, DataCollatorForLanguageModeling  # type: ignore
	try:
		from peft import get_peft_model, prepare_model_for_kbit_training, PeftModel  # type: ignore
	except Exception as e:
		print(f"[eval] PEFT not available: {e}")
		print("Install 'peft' if you want to evaluate LoRA adapters.")
		PeftModel = None  # type: ignore

	tok = AutoTokenizer.from_pretrained(model_id)
	if tok.pad_token_id is None and tok.eos_token is not None:
		tok.pad_token = tok.eos_token
	pad_id = int(tok.pad_token_id or 0)

	print(f"[eval] loading base model {model_id}")
	model_kwargs: Dict[str, Any] = {}
	visible = os.environ.get("CUDA_VISIBLE_DEVICES", "")
	num_visible = len([v for v in visible.split(",") if v.strip()]) if visible else torch.cuda.device_count()
	if use_qlora:
		try:
			from transformers import BitsAndBytesConfig  # type: ignore
			bnb_config = BitsAndBytesConfig(
				load_in_4bit=True,
				bnb_4bit_quant_type="nf4",
				bnb_4bit_compute_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,
			)
			model_kwargs["quantization_config"] = bnb_config
			model_kwargs["device_map"] = "auto" if num_visible > 1 else None
		except Exception as e:
			print(f"[eval] BitsAndBytes not available, evaluating in full precision: {e}")
			model_kwargs["device_map"] = None
			model_kwargs["torch_dtype"] = torch.bfloat16 if torch.cuda.is_available() else torch.float32
	else:
		model_kwargs["device_map"] = "auto" if num_visible > 1 else None
		model_kwargs["torch_dtype"] = torch.bfloat16 if torch.cuda.is_available() else torch.float32

	device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
	base_model = AutoModelForCausalLM.from_pretrained(model_id, **model_kwargs)
	try:
		base_model.config.use_cache = False
	except Exception:
		pass
	device_map_used = model_kwargs.get("device_map", None) is not None
	if not device_map_used:
		base_model = base_model.to(device)
	if use_qlora:
		try:
			base_model = prepare_model_for_kbit_training(base_model)  # type: ignore
		except Exception:
			pass
	base_model.eval()

	@torch.no_grad()
	def evaluate_model(model: Any, tokenized_ds: Any, pad_token_id: int) -> Dict[str, float]:
		loader = DataLoader(tokenized_ds, batch_size=per_device_batch, shuffle=False)
		total_loss = 0.0
		num_batches = 0
		for batch in loader:
			labels = batch.get("labels", None)
			if labels is None:
				# create labels equal to input_ids to do next-token prediction
				labels = batch["input_ids"].clone()
				batch["labels"] = labels
			# Move to device only when not using a sharded device_map
			if not device_map_used:
				for k, v in list(batch.items()):
					if torch.is_tensor(v):
						batch[k] = v.to(device)
			inp = {k: v for k, v in batch.items() if k != "labels"}
			outputs = model(**inp)
			logits = outputs.get("logits")
			vocab = logits.size(-1)
			shift_logits = logits[..., :-1, :].contiguous()
			shift_labels = batch["labels"][..., 1:].contiguous().to(shift_logits.device)
			shift_labels = shift_labels.masked_fill(shift_labels == int(pad_token_id), -100)
			loss = F.cross_entropy(
				shift_logits.view(-1, vocab),
				shift_labels.view(-1),
				ignore_index=-100,
			)
			total_loss += float(loss.item())
			num_batches += 1
		avg_loss = total_loss / max(1, num_batches)
		try:
			ppl = float(torch.exp(torch.tensor(avg_loss)).item())
		except Exception:
			ppl = float("inf")
		return {"loss": avg_loss, "perplexity": ppl}

	# Data util
	from datasets import Dataset as HFDataset  # type: ignore
	def make_dataset(texts: List[str]) -> Any:
		raw = HFDataset.from_dict({"text": texts})
		def _tok_fn(row: Dict[str, Any]) -> Dict[str, Any]:
			out = tok(row["text"], truncation=True, max_length=context_len, padding="max_length")
			return out
		return raw.map(_tok_fn, batched=False, remove_columns=["text"])

	# Collator
	collator = DataCollatorForLanguageModeling(tokenizer=tok, mlm=False)

	# Resolve datasets one-by-one (sequential, full by default)
	datasets_cfg = os.getenv("DATASETS_CONFIG", "")
	train_new_only = False  # eval should not skip previously seen
	iter_program_texts = None
	try:
		from examples.scripts.datasets_loader import iter_program_texts as _ipt  # type: ignore
		iter_program_texts = _ipt
	except Exception:
		try:
			import importlib.util as _imputil  # type: ignore
			_loader_path = (example_dir.parent / "scripts" / "datasets_loader.py").resolve()
			_spec = _imputil.spec_from_file_location("datasets_loader", str(_loader_path))
			if _spec and _spec.loader:
				_mod = _imputil.module_from_spec(_spec)
				_spec.loader.exec_module(_mod)  # type: ignore
				iter_program_texts = getattr(_mod, "iter_program_texts", None)  # type: ignore
		except Exception:
			iter_program_texts = None
	if iter_program_texts is None:
		print("[eval] could not resolve datasets loader")
		sys.exit(2)

	results: Dict[str, Dict[str, Any]] = {}

	# Evaluate baseline first
	for texts, src_name in iter_program_texts(
		example_dir=str(example_dir),
		config_path=datasets_cfg or None,
		train_new_only=train_new_only,
		state_path=str(paths.state_path),
	):
		if not texts:
			continue
		ds = make_dataset(texts)
		ds = ds.with_format("torch")
		print(f"[eval] baseline on {src_name} ({len(texts)} samples)")
		metrics = evaluate_model(base_model, ds, pad_id)
		results[src_name] = results.get(src_name, {})
		results[src_name]["baseline"] = {
			"loss": float(metrics.get("loss", 0.0)),
			"perplexity": float(metrics.get("perplexity", 0.0)),
		}

	# Attach LoRA and evaluate finetuned
	model_ft = base_model
	if PeftModel is not None and adapter_path and Path(adapter_path).exists():
		print(f"[eval] loading adapter from {adapter_path}")
		model_ft = PeftModel.from_pretrained(base_model, adapter_path, is_trainable=False)  # type: ignore
	else:
		print(f"[eval] adapter not found or PEFT missing; skipping finetuned eval: {adapter_path}")
	model_ft.eval()
	for texts, src_name in iter_program_texts(
		example_dir=str(example_dir),
		config_path=datasets_cfg or None,
		train_new_only=train_new_only,
		state_path=str(paths.state_path),
	):
		if not texts:
			continue
		ds = make_dataset(texts)
		ds = ds.with_format("torch")
		print(f"[eval] finetuned on {src_name} ({len(texts)} samples)")
		metrics = evaluate_model(model_ft, ds, pad_id)
		results[src_name] = results.get(src_name, {})
		results[src_name]["finetuned"] = {
			"loss": float(metrics.get("loss", 0.0)),
			"perplexity": float(metrics.get("perplexity", 0.0)),
		}

	# Save combined report
	report = {
		"model_id": model_id,
		"adapter_path": str(adapter_path),
		"results": results,
	}
	with open(paths.out_dir / "EvalComparison.json", "w", encoding="utf-8") as fh:
		fh.write(json.dumps({"schema_version": 1, "EvalComparison": report}, indent=2))
	print(str(paths.out_dir / "EvalComparison.json"))

if __name__ == "__main__":
	main()


EvalPaths

from __future__ import annotations

import os
import sys
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Any, List, Tuple

import torch
import torch.nn.functional as F
from torch.utils.data import DataLoader

@dataclass
class EvalPaths:
	adapters_dir: Path
	out_dir: Path
	state_path: Path

def _ensure_dirs(paths: EvalPaths) -> None:
	paths.adapters_dir.mkdir(parents=True, exist_ok=True)
	paths.out_dir.mkdir(parents=True, exist_ok=True)

def main() -> None:
	example_dir = Path(__file__).resolve().parent
	artifacts_root = example_dir / "datasets" / "hf_lora_mbpp"
	paths = EvalPaths(
		adapters_dir=artifacts_root / "lora_adapter",
		out_dir=artifacts_root / "outputs",
		state_path=artifacts_root / ".program_state.json",
	)
	_ensure_dirs(paths)

	# If not specified, prefer GPUs 0,1
	if not os.getenv("CUDA_VISIBLE_DEVICES"):
		os.environ["CUDA_VISIBLE_DEVICES"] = "0,1"


import os
import sys
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Any, List, Tuple

import torch
import torch.nn.functional as F
from torch.utils.data import DataLoader

@dataclass
class EvalPaths:
	adapters_dir: Path
	out_dir: Path
	state_path: Path

def _ensure_dirs(paths: EvalPaths) -> None:
	paths.adapters_dir.mkdir(parents=True, exist_ok=True)
	paths.out_dir.mkdir(parents=True, exist_ok=True)

def main() -> None:
	example_dir = Path(__file__).resolve().parent
	artifacts_root = example_dir / "datasets" / "hf_lora_mbpp"
	paths = EvalPaths(
		adapters_dir=artifacts_root / "lora_adapter",
		out_dir=artifacts_root / "outputs",
		state_path=artifacts_root / ".program_state.json",
	)
	_ensure_dirs(paths)

	# If not specified, prefer GPUs 0,1
	if not os.getenv("CUDA_VISIBLE_DEVICES"):
		os.environ["CUDA_VISIBLE_DEVICES"] = "0,1"

	model_id = os.getenv("LLAMA_MODEL_ID", "meta-llama/Llama-3.1-8B-Instruct")
	adapter_path = (os.getenv("ADAPTER_PATH") or str(paths.adapters_dir)).strip()
	context_len = int(os.getenv("MAX_LEN", "256"))
	per_device_batch = int(os.getenv("BATCH_SIZE", "1"))