import logging
import argparse
import json
import subprocess
from pathlib import Path
from typing import Dict, List

def run_once(root: Path, actuator: str, task: str, timeout: int, hf_model: str, extra_args: List[str]) -> Dict:
	cmd = [
		"python3",
		str(root / "scripts" / "run_loop_oscli.py"),
		"--planner-backend",
		"hf",
		"--verifier-backend",
		"hf",
		"--planner-model",
		hf_model,
		"--verifier-model",
		hf_model,
		"--timeout",
		str(timeout),
		"--task",
		task,
		"--actuator",
		actuator,
	]
	# Enable learned router when evaluating router mode
	if actuator == "router":
		cmd.append("--learned-router")
		# Use tuned threshold if available in the packed router model
		cmd.append("--router-use-packed-threshold")
	cmd.extend(extra_args)
	res = subprocess.run(cmd, capture_output=True, text=True)
	out_lines = res.stdout.strip().splitlines()
	# Find last valid JSON object line with a status field
	for line in reversed(out_lines):
		line = line.strip()
		if not (line.startswith("{") and line.endswith("}")):
			continue
		try:
			obj = json.loads(line)
			if isinstance(obj, dict) and "status" in obj:
				return obj
		except Exception:
			continue
	# Fallback: try parsing last line
	last = out_lines[-1] if out_lines else "{}"
	try:
		return json.loads(last)
	except Exception:
		return {"status": "error", "raw": last}

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--hf-model", default="meta-llama/Llama-3.2-3B")
	ap.add_argument("--timeout", type=int, default=60)
	ap.add_argument("--runs", type=int, default=1)
	ap.add_argument("--wm", action="store_true")
	ap.add_argument("--calib", action="store_true")
	ap.add_argument("--only-router", action="store_true")
	args = ap.parse_args()

	extra: List[str] = []
	if args.wm:
		extra.append("--wm-prior")
	if args.calib:
		extra.append("--calibrate-verifier")

	actuators = ["two_head", "t5", "nn", "router", "template"]


main

	out_lines = res.stdout.strip().splitlines()
	# Find last valid JSON object line with a status field
	for line in reversed(out_lines):
		line = line.strip()
		if not (line.startswith("{") and line.endswith("}")):
			continue
		try:
			obj = json.loads(line)
			if isinstance(obj, dict) and "status" in obj:
				return obj
		except Exception:
			continue
	# Fallback: try parsing last line
	last = out_lines[-1] if out_lines else "{}"
	try:
		return json.loads(last)
	except Exception:
		return {"status": "error", "raw": last}

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--hf-model", default="meta-llama/Llama-3.2-3B")
	ap.add_argument("--timeout", type=int, default=60)
	ap.add_argument("--runs", type=int, default=1)
	ap.add_argument("--wm", action="store_true")
	ap.add_argument("--calib", action="store_true")
	ap.add_argument("--only-router", action="store_true")
	args = ap.parse_args()

	extra: List[str] = []
	if args.wm:
		extra.append("--wm-prior")
	if args.calib:
		extra.append("--calibrate-verifier")

	actuators = ["two_head", "t5", "nn", "router", "template"]
	if args.only_router:
		actuators = ["router"]
	tasks = ["count_lines", "grep_error"]

	summary: Dict[str, Dict[str, float]] = {}
	per_task: Dict[str, Dict[str, float]] = {}
	per_expert: Dict[str, Dict[str, float]] = {}
	router_pick_counts: Dict[str, int] = {"t5": 0, "nn": 0, "two_head": 0}
	router_probs: List[float] = []
	for act in actuators:
		total = 0
		success = 0
		for _ in range(args.runs):
			for t in tasks:
				info = run_once(root, act, t, args.timeout, args.hf_model, extra)
				total += 1
				if info.get("status") == "ok":
					success += 1
				key = f"{act}:{t}"
				per_task[key] = per_task.get(key, {"n": 0, "success": 0.0})
				per_task[key]["n"] += 1
				per_task[key]["success"] += 1 if info.get("status") == "ok" else 0
				# Track per-expert aggregates
				per_expert[act] = per_expert.get(act, {"n": 0, "success": 0.0})
				per_expert[act]["n"] += 1
				per_expert[act]["success"] += 1 if info.get("status") == "ok" else 0
				# If router mode, aggregate pick distribution and probabilities
				if act == "router":
					picked = str(info.get("router_picked", ""))
					if picked in router_pick_counts:
						router_pick_counts[picked] += 1
					prob = info.get("router_prob")
					if isinstance(prob, (int, float)):
						router_probs.append(float(prob))
		summary[act] = {"success_rate": (success / total) if total else 0.0, "n": total}

	# Convenience: include router entry as top-level if present
	# Convert counts to rates for per_expert
	per_expert_rates: Dict[str, Dict[str, float]] = {}
	for k, v in per_expert.items():
		n = float(v.get("n", 0.0))
		s = float(v.get("success", 0.0))
		per_expert_rates[k] = {"success_rate": (s / n) if n else 0.0, "n": n}

	# Router metrics
	router_metrics = summary.get("router", {}).copy() if isinstance(summary.get("router", {}), dict) else {}
	if router_probs:
		try:
			import statistics as _stats
			router_metrics.update({
				"router_prob_mean": float(_stats.fmean(router_probs)),
				"router_prob_median": float(_stats.median(router_probs)),
				"router_prob_count": int(len(router_probs)),
			})
			# Entropy over Bernoulli prob p: -[p log p + (1-p) log (1-p)] base 2
			import math as _m
			def _bern_entropy(p: float) -> float:
				p = max(1e-9, min(1 - 1e-9, p))
				return float(- (p * _m.log2(p) + (1 - p) * _m.log2(1 - p)))
			ents = [_bern_entropy(p) for p in router_probs]
			router_metrics.update({
				"router_entropy_mean": float(_stats.fmean(ents)),
				"router_entropy_median": float(_stats.median(ents)),
			})
		except Exception:
			router_metrics["router_prob_count"] = int(len(router_probs))
	router_metrics.update({
		"router_picks": router_pick_counts,
	})
	out = {"router": router_metrics, "all": summary, "per_task": per_task, "per_expert": per_expert_rates}
	print(json.dumps(out))
	return 0

if __name__ == "__main__":
	raise SystemExit(main())


	# Convenience: include router entry as top-level if present
	# Convert counts to rates for per_expert
	per_expert_rates: Dict[str, Dict[str, float]] = {}
	for k, v in per_expert.items():
		n = float(v.get("n", 0.0))
		s = float(v.get("success", 0.0))
		per_expert_rates[k] = {"success_rate": (s / n) if n else 0.0, "n": n}

	# Router metrics
	router_metrics = summary.get("router", {}).copy() if isinstance(summary.get("router", {}), dict) else {}
	if router_probs:
		try:
			import statistics as _stats
			router_metrics.update({
				"router_prob_mean": float(_stats.fmean(router_probs)),
				"router_prob_median": float(_stats.median(router_probs)),
				"router_prob_count": int(len(router_probs)),
			})
			# Entropy over Bernoulli prob p: -[p log p + (1-p) log (1-p)] base 2
			import math as _m
			def _bern_entropy(p: float) -> float:
				p = max(1e-9, min(1 - 1e-9, p))
				return float(- (p * _m.log2(p) + (1 - p) * _m.log2(1 - p)))
			ents = [_bern_entropy(p) for p in router_probs]
			router_metrics.update({
				"router_entropy_mean": float(_stats.fmean(ents)),
				"router_entropy_median": float(_stats.median(ents)),
			})
		except Exception:
			router_metrics["router_prob_count"] = int(len(router_probs))
	router_metrics.update({
		"router_picks": router_pick_counts,
	})
	out = {"router": router_metrics, "all": summary, "per_task": per_task, "per_expert": per_expert_rates}
	print(json.dumps(out))
	return 0

if __name__ == "__main__":
	raise SystemExit(main())


import logging
import argparse
import json
from pathlib import Path

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--summary", default=str(root / "data" / "traces" / "redteam_dom_eval.jsonl"))
	args = ap.parse_args()

	p = Path(args.summary)
	if not p.exists():
		print(json.dumps({"ok": False, "error": "redteam_missing", "path": str(p)}))
		return 1
	# If file ends with .jsonl produced by run_redteam_dom, we can either parse JSONL or rely on stdout summary
	# Here, read JSONL and assert all ok=true
	ok = True
	count = 0
	fails = []
	with p.open("r", encoding="utf-8") as f:
		for line in f:
			line = line.strip()
			if not line:
				continue
			try:
				rec = json.loads(line)
			except Exception:
				continue
			count += 1
			if not bool(rec.get("ok", False)):
				ok = False
				fails.append({"id": rec.get("id"), "action": rec.get("action", {})})
	print(json.dumps({"ok": ok, "total": count, "fails": fails}))
	return 0 if ok else 1

if __name__ == "__main__":
	raise SystemExit(main())


from __future__ import annotations
import logging

import argparse
import json
from pathlib import Path

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--metrics", default=str(root / "data" / "devtools" / "metrics.json"))
	ap.add_argument("--min-ok-rate", type=float, default=0.5)
	ap.add_argument("--max-ttf-p90", type=float, default=120.0)
	args = ap.parse_args()

	try:
		data = json.loads(Path(args.metrics).read_text(encoding="utf-8"))
	except Exception:
		print(json.dumps({"ok": False, "error": "metrics_missing"}))
		return 1

	ok_rate = float(data.get("ok_rate", 0.0))
	ttf_p90 = float(data.get("time_to_fix_p90", 0.0))
	ok = (ok_rate >= float(args.min_ok_rate)) and (ttf_p90 <= float(args.max_ttf_p90))
	print(json.dumps({"ok": ok, "ok_rate": ok_rate, "time_to_fix_p90": ttf_p90, "min_ok_rate": float(args.min_ok_rate), "max_ttf_p90": float(args.max_ttf_p90)}))
	return 0 if ok else 1

if __name__ == "__main__":
	raise SystemExit(main())


import logging
import argparse
import json
import re
from pathlib import Path
from typing import List

DEFAULT_PATTERNS: List[str] = [
	# High-signal patterns
	r"sk-[A-Za-z0-9]{16,}",
	r"ghp_[A-Za-z0-9]{20,}",
	r"ya29\.[A-Za-z0-9\-_]+",
	r"xox[baprs]-[A-Za-z0-9\-]+",
	r"AKIA[0-9A-Z]{16}",
	r"ASIA[0-9A-Z]{16}",
	r"eyJ[\w\-]{10,}\.[\w\-]{10,}\.[\w\-]{10,}",
	r"(?i)(password|token|secret|api[_-]?key)\s*[:=]\s*[^\s\"']{6,}",
]

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--paths", nargs="*", default=[str(root / "data" / "traces"), str(root / "data" / "dashboards")])
	ap.add_argument("--extra-patterns", default="")
	args = ap.parse_args()

	patterns = list(DEFAULT_PATTERNS)
	if args.extra_patterns:
		for raw in re.split(r"[\n,]", str(args.extra_patterns)):
			raw = raw.strip()
			if raw:
				patterns.append(raw)
	compiled = []
	for p in patterns:
		try:
			compiled.append(re.compile(p))
		except Exception:
			pass

	found = []
	for pth in args.paths:
		p = Path(pth)
		if not p.exists():
			continue
		for file in p.rglob("*"):
			if not file.is_file():
				continue
			try:
				txt = file.read_text(encoding="utf-8", errors="ignore")
			except Exception:
				continue
			for rx in compiled:
				m = rx.search(txt)
				if m:
					found.append({"file": str(file), "match": m.group(0)[:64]})

	ok = len(found) == 0
	print(json.dumps({"ok": ok, "findings": found}))
	return 0 if ok else 1

if __name__ == "__main__":
	raise SystemExit(main())


import argparse
import json
import re
from pathlib import Path
from typing import List

DEFAULT_PATTERNS: List[str] = [
	# High-signal patterns
	r"sk-[A-Za-z0-9]{16,}",
	r"ghp_[A-Za-z0-9]{20,}",
	r"ya29\.[A-Za-z0-9\-_]+",
	r"xox[baprs]-[A-Za-z0-9\-]+",
	r"AKIA[0-9A-Z]{16}",
	r"ASIA[0-9A-Z]{16}",
	r"eyJ[\w\-]{10,}\.[\w\-]{10,}\.[\w\-]{10,}",
	r"(?i)(password|token|secret|api[_-]?key)\s*[:=]\s*[^\s\"']{6,}",
]

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--paths", nargs="*", default=[str(root / "data" / "traces"), str(root / "data" / "dashboards")])
	ap.add_argument("--extra-patterns", default="")
	args = ap.parse_args()

	patterns = list(DEFAULT_PATTERNS)
	if args.extra_patterns:
		for raw in re.split(r"[\n,]", str(args.extra_patterns)):
			raw = raw.strip()
			if raw:
				patterns.append(raw)
	compiled = []
	for p in patterns:
		try:
			compiled.append(re.compile(p))
		except Exception:
			pass

	found = []
	for pth in args.paths:
		p = Path(pth)
		if not p.exists():
			continue
		for file in p.rglob("*"):
			if not file.is_file():
				continue
			try:
				txt = file.read_text(encoding="utf-8", errors="ignore")
			except Exception:
				continue
			for rx in compiled:
				m = rx.search(txt)
				if m:
					found.append({"file": str(file), "match": m.group(0)[:64]})

	ok = len(found) == 0
	print(json.dumps({"ok": ok, "findings": found}))
	return 0 if ok else 1

if __name__ == "__main__":
	raise SystemExit(main())


import logging
import argparse
import json
from pathlib import Path

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--kpi", default=str(root / "data" / "benchmarks" / "kpi.json"))
	ap.add_argument("--min-cli", type=float, default=0.7)
	ap.add_argument("--min-dom", type=float, default=0.7)
	ap.add_argument("--min-office", type=float, default=None)
	ap.add_argument("--use-budgeted", action="store_true", help="Gate on budgeted success_rate if available")
	ap.add_argument("--max-cli-cost", type=float, default=None, help="Optional max cost_per_success for CLI summary")
	ap.add_argument("--max-dom-cost", type=float, default=None, help="Optional max cost_per_success for DOM summary")
	ap.add_argument("--max-office-cost", type=float, default=None, help="Optional max cost_per_success for Office summary")
	ap.add_argument("--max-cli-over-budget", type=int, default=None, help="Optional max total over-budget runs in CLI summary")
	ap.add_argument("--max-dom-over-budget", type=int, default=None, help="Optional max total over-budget runs in DOM summary")
	ap.add_argument("--min-cli-mem-hit", type=float, default=None, help="Optional minimum memory hit rate for CLI summary [0..1]")
	ap.add_argument("--min-dom-mem-hit", type=float, default=None, help="Optional minimum memory hit rate for DOM summary [0..1]")
	args = ap.parse_args()

	p = Path(args.kpi)
	if not p.exists():
		print(json.dumps({"ok": False, "error": "kpi_missing", "path": str(p)}))
		return 1
	kpi = json.loads(p.read_text(encoding="utf-8"))
	cli = kpi.get("cli_summary", {})
	dom = kpi.get("dom_summary", {})
	off = kpi.get("office_summary", {})
	if bool(args.use_budgeted):
		cli_rate = float(cli.get("success_rate_budgeted", cli.get("success_rate", 0.0)))
		dom_rate = float(dom.get("success_rate_budgeted", dom.get("success_rate", 0.0)))
	else:
		cli_rate = float(cli.get("success_rate", 0.0))
		dom_rate = float(dom.get("success_rate", 0.0))
	ok = bool(cli_rate >= float(args.min_cli) and dom_rate >= float(args.min_dom))
	if ok and args.min_office is not None:
		off_rate = float(off.get("success_rate", 0.0))
		ok = ok and (off_rate >= float(args.min_office))
	# Optional cost gates if available in KPI
	if ok and (args.max_cli_cost is not None or args.max_dom_cost is not None or args.max_office_cost is not None):
		cli_cost = cli.get("cost_per_success")
		dom_cost = dom.get("cost_per_success")
		off_cost = off.get("cost_per_success")
		if args.max_cli_cost is not None and cli_cost is not None:
			ok = ok and (float(cli_cost) <= float(args.max_cli_cost))
		if args.max_dom_cost is not None and dom_cost is not None:
			ok = ok and (float(dom_cost) <= float(args.max_dom_cost))
		if args.max_office_cost is not None and off_cost is not None:
			ok = ok and (float(off_cost) <= float(args.max_office_cost))
	# Optional over-budget runs thresholds
	if ok and (args.max_cli_over_budget is not None or args.max_dom_over_budget is not None):
		cli_over = int(cli.get("over_budget_total_runs", 0)) if isinstance(cli, dict) else 0
		dom_over = int(dom.get("over_budget_total_runs", 0)) if isinstance(dom, dict) else 0
		if args.max_cli_over_budget is not None:
			ok = ok and (cli_over <= int(args.max_cli_over_budget))
		if args.max_dom_over_budget is not None:
			ok = ok and (dom_over <= int(args.max_dom_over_budget))
	# Optional memory hit rate thresholds
	if ok and (args.min_cli_mem_hit is not None or args.min_dom_mem_hit is not None):
		cli_m = float(cli.get("memory_hit_rate", 0.0)) if isinstance(cli, dict) else 0.0
		dom_m = float(dom.get("memory_hit_rate", 0.0)) if isinstance(dom, dict) else 0.0
		if args.min_cli_mem_hit is not None:
			ok = ok and (cli_m >= float(args.min_cli_mem_hit))
		if args.min_dom_mem_hit is not None:
			ok = ok and (dom_m >= float(args.min_dom_mem_hit))
	print(json.dumps({
		"ok": ok,
		"cli": cli_rate,
		"dom": dom_rate,
		"office": (float(off.get("success_rate", 0.0)) if isinstance(off, dict) else None),
		"budgeted": bool(args.use_budgeted),
		"thresholds": {"cli": float(args.min_cli), "dom": float(args.min_dom), "office": (float(args.min_office) if args.min_office is not None else None)},
		"cost_thresholds": {
			"cli": (float(args.max_cli_cost) if args.max_cli_cost is not None else None),
			"dom": (float(args.max_dom_cost) if args.max_dom_cost is not None else None),
			"office": (float(args.max_office_cost) if args.max_office_cost is not None else None)
		},
		"over_budget_thresholds": {
			"cli": (int(args.max_cli_over_budget) if args.max_cli_over_budget is not None else None),
			"dom": (int(args.max_dom_over_budget) if args.max_dom_over_budget is not None else None)
		},
		"memory_hit_thresholds": {
			"cli": (float(args.min_cli_mem_hit) if args.min_cli_mem_hit is not None else None),
			"dom": (float(args.min_dom_mem_hit) if args.min_dom_mem_hit is not None else None)
		}
	}))
	return 0 if ok else 1

if __name__ == "__main__":
	raise SystemExit(main())


import logging
import json
from pathlib import Path
from typing import Any, Dict
import sys

from agi_dw.core.actuator.service import ActuatorConfig, RouterExtras, select_action

def eval_accuracy(traces_path: Path, il_path: Path) -> float:
	cfg = ActuatorConfig(mode="nn", il_path=str(il_path))
	extra = RouterExtras(domain="cli")
	total = 0
	correct = 0
	for line in traces_path.read_text(encoding="utf-8").splitlines():
		if not line.strip():
			continue
		obj: Dict[str, Any] = json.loads(line)
		obs = obj.get("obs", {})
		plan = obj.get("plan", {})
		gold = obj.get("action", {})
		pred, _ = select_action(obs, plan, cfg, extra)
		if pred == gold:
			correct += 1
		total += 1
	acc = correct / total if total else 0.0
	print(f"accuracy={acc:.3f} ({correct}/{total})")
	return acc

def main() -> int:
	root = Path(__file__).resolve().parents[1]
	traces = Path(sys.argv[1]) if len(sys.argv) > 1 else root / "data" / "traces" / "seed_os_cli.verified.llm.jsonl"
	il = Path(sys.argv[2]) if len(sys.argv) > 2 else root / "data" / "skills" / "actuator_il.jsonl"
	_ = eval_accuracy(traces, il)
	return 0

if __name__ == "__main__":
	raise SystemExit(main())


	cfg = ActuatorConfig(mode="nn", il_path=str(il_path))
	extra = RouterExtras(domain="cli")
	total = 0
	correct = 0
	for line in traces_path.read_text(encoding="utf-8").splitlines():
		if not line.strip():
			continue
		obj: Dict[str, Any] = json.loads(line)
		obs = obj.get("obs", {})
		plan = obj.get("plan", {})
		gold = obj.get("action", {})
		pred, _ = select_action(obs, plan, cfg, extra)
		if pred == gold:
			correct += 1
		total += 1
	acc = correct / total if total else 0.0
	print(f"accuracy={acc:.3f} ({correct}/{total})")
	return acc

def main() -> int:
	root = Path(__file__).resolve().parents[1]
	traces = Path(sys.argv[1]) if len(sys.argv) > 1 else root / "data" / "traces" / "seed_os_cli.verified.llm.jsonl"
	il = Path(sys.argv[2]) if len(sys.argv) > 2 else root / "data" / "skills" / "actuator_il.jsonl"
	_ = eval_accuracy(traces, il)
	return 0

if __name__ == "__main__":
	raise SystemExit(main())


from __future__ import annotations

import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Any, Dict, List

def _write_jsonl(path: Path, obj: Dict[str, Any]) -> None:
	path.parent.mkdir(parents=True, exist_ok=True)
	with path.open("a", encoding="utf-8") as f:
		f.write(json.dumps(obj, ensure_ascii=False) + "\n")

def cmd_log(args: argparse.Namespace) -> int:
	root = Path(__file__).resolve().parents[2]
	out = root / "data" / "traces" / "refactors" / "refactor_events.jsonl"
	meta: Dict[str, Any] = {}
	if args.meta:
		try:
			meta = json.loads(args.meta)
		except Exception:
			meta = {"raw": args.meta}
	row = {"ts": float(args.ts or 0.0) or __import__("time").time(), "event": args.event, "suite": args.suite or "all", "meta": meta}
	_write_jsonl(out, row)
	print(json.dumps({"ok": True, "wrote": str(out), "event": row["event"], "suite": row["suite"]}))
	return 0

def _git(*cmd: str) -> str:
	try:
		p = subprocess.run(["git", *cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8", check=False)
		return p.stdout
	except Exception:
		return ""

def cmd_capture_diff(args: argparse.Namespace) -> int:
	root = Path(__file__).resolve().parents[2]
	out = root / "data" / "traces" / "refactors" / "file_diffs"
	out.mkdir(parents=True, exist_ok=True)
	# diff vs HEAD (or provided base)
	base = args.base or "HEAD"
	diff = _git("diff", base)
	fp = out / ("diff_" + (base.replace("/", "_")) + ".patch")
	fp.write_text(diff, encoding="utf-8")
	print(json.dumps({"ok": True, "wrote": str(fp), "base": base, "bytes": len(diff)}))
	return 0

def _py_ast_summary(path: Path) -> Dict[str, Any]:
	try:
		import ast
		tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
		funcs: List[str] = []
		classes: List[str] = []
		for node in ast.walk(tree):
			if isinstance(node, ast.FunctionDef):
				funcs.append(node.name)
			elif isinstance(node, ast.AsyncFunctionDef):
				funcs.append(node.name)
			elif isinstance(node, ast.ClassDef):
				classes.append(node.name)
		return {"file": str(path), "functions": sorted(set(funcs)), "classes": sorted(set(classes))}
	except Exception as e:
		return {"file": str(path), "error": str(e)}

def cmd_capture_ast(args: argparse.Namespace) -> int:
	root = Path(__file__).resolve().parents[2]
	out = root / "data" / "traces" / "refactors" / "ast_summaries"
	out.mkdir(parents=True, exist_ok=True)
	paths = [Path(p) for p in (args.paths or [])]
	rows: List[Dict[str, Any]] = []
	for p in paths:
		if p.is_dir():
			for fp in p.rglob("*.py"):
				rows.append(_py_ast_summary(fp))
		else:
			rows.append(_py_ast_summary(p))
	res = {"root": str(root), "count": len(rows), "summaries": rows[:200]}  # limit preview size
	fp = out / "ast_summary.json"
	fp.write_text(json.dumps(res, ensure_ascii=False, indent=2), encoding="utf-8")
	print(json.dumps({"ok": True, "wrote": str(fp), "count": len(rows)}))
	return 0

def cmd_env_snapshot(args: argparse.Namespace) -> int:
	"""Capture brief environment summary for WM/P/A/V/U training context."""
	root = Path(__file__).resolve().parents[2]
	out = root / "data" / "traces" / "refactors" / "env_summaries"
	out.mkdir(parents=True, exist_ok=True)
	info: Dict[str, Any] = {"cwd": os.getcwd(), "python": sys.version}
	for mod in ("torch", "transformers", "datasets", "evalplus"):
		try:
			m = __import__(mod)
			info[mod] = getattr(m, "__version__", None)
		except Exception:
			info[mod] = None
	fp = out / "env.json"
	fp.write_text(json.dumps(info, ensure_ascii=False, indent=2), encoding="utf-8")
	print(json.dumps({"ok": True, "wrote": str(fp)}))
	return 0

def main() -> int:
	ap = argparse.ArgumentParser(description="Refactor tracer utilities")
	sub = ap.add_subparsers(dest="cmd", required=True)

	ap_log = sub.add_parser("log", help="Log a refactor event")
	ap_log.add_argument("event")
	ap_log.add_argument("--suite", default="all")
	ap_log.add_argument("--meta", default="")
	ap_log.add_argument("--ts", default="")
	ap_log.set_defaults(func=cmd_log)

	ap_diff = sub.add_parser("capture-diff", help="Capture working diff vs base (HEAD by default)")
	ap_diff.add_argument("--base", default="HEAD")
	ap_diff.set_defaults(func=cmd_capture_diff)

	ap_ast = sub.add_parser("capture-ast", help="Capture AST summaries for given paths")
	ap_ast.add_argument("paths", nargs="+")
	ap_ast.set_defaults(func=cmd_capture_ast)

	ap_env = sub.add_parser("env-snapshot", help="Capture environment summary for WM/P/A/V/U context")
	ap_env.set_defaults(func=cmd_env_snapshot)

	args = ap.parse_args()
	return int(args.func(args))

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


from __future__ import annotations

import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Any, Dict, List

def _write_jsonl(path: Path, obj: Dict[str, Any]) -> None:
	path.parent.mkdir(parents=True, exist_ok=True)
	with path.open("a", encoding="utf-8") as f:
		f.write(json.dumps(obj, ensure_ascii=False) + "\n")

def cmd_log(args: argparse.Namespace) -> int:
	root = Path(__file__).resolve().parents[2]
	out = root / "data" / "traces" / "refactors" / "refactor_events.jsonl"
	meta: Dict[str, Any] = {}
	if args.meta:
		try:
			meta = json.loads(args.meta)
		except Exception:
			meta = {"raw": args.meta}
	row = {"ts": float(args.ts or 0.0) or __import__("time").time(), "event": args.event, "suite": args.suite or "all", "meta": meta}
	_write_jsonl(out, row)
	print(json.dumps({"ok": True, "wrote": str(out), "event": row["event"], "suite": row["suite"]}))
	return 0

def _git(*cmd: str) -> str:
	try:
		p = subprocess.run(["git", *cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8", check=False)


from __future__ import annotations

import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Any, Dict, List

def _write_jsonl(path: Path, obj: Dict[str, Any]) -> None:
	path.parent.mkdir(parents=True, exist_ok=True)
	with path.open("a", encoding="utf-8") as f:
		f.write(json.dumps(obj, ensure_ascii=False) + "\n")

def cmd_log(args: argparse.Namespace) -> int:
	root = Path(__file__).resolve().parents[2]
	out = root / "data" / "traces" / "refactors" / "refactor_events.jsonl"
	meta: Dict[str, Any] = {}
	if args.meta:
		try:
			meta = json.loads(args.meta)
		except Exception:
			meta = {"raw": args.meta}
	row = {"ts": float(args.ts or 0.0) or __import__("time").time(), "event": args.event, "suite": args.suite or "all", "meta": meta}
	_write_jsonl(out, row)
	print(json.dumps({"ok": True, "wrote": str(out), "event": row["event"], "suite": row["suite"]}))
	return 0

def _git(*cmd: str) -> str:
	try:
		p = subprocess.run(["git", *cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8", check=False)
		return p.stdout
	except Exception:
		return ""

def cmd_capture_diff(args: argparse.Namespace) -> int:
	root = Path(__file__).resolve().parents[2]
	out = root / "data" / "traces" / "refactors" / "file_diffs"
	out.mkdir(parents=True, exist_ok=True)
	# diff vs HEAD (or provided base)
	base = args.base or "HEAD"
	diff = _git("diff", base)
	fp = out / ("diff_" + (base.replace("/", "_")) + ".patch")
	fp.write_text(diff, encoding="utf-8")
	print(json.dumps({"ok": True, "wrote": str(fp), "base": base, "bytes": len(diff)}))


	path.parent.mkdir(parents=True, exist_ok=True)
	with path.open("a", encoding="utf-8") as f:
		f.write(json.dumps(obj, ensure_ascii=False) + "\n")

def cmd_log(args: argparse.Namespace) -> int:
	root = Path(__file__).resolve().parents[2]
	out = root / "data" / "traces" / "refactors" / "refactor_events.jsonl"
	meta: Dict[str, Any] = {}
	if args.meta:
		try:
			meta = json.loads(args.meta)
		except Exception:
			meta = {"raw": args.meta}
	row = {"ts": float(args.ts or 0.0) or __import__("time").time(), "event": args.event, "suite": args.suite or "all", "meta": meta}
	_write_jsonl(out, row)
	print(json.dumps({"ok": True, "wrote": str(out), "event": row["event"], "suite": row["suite"]}))
	return 0

def _git(*cmd: str) -> str:
	try:
		p = subprocess.run(["git", *cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8", check=False)
		return p.stdout
	except Exception:
		return ""

def cmd_capture_diff(args: argparse.Namespace) -> int:
	root = Path(__file__).resolve().parents[2]
	out = root / "data" / "traces" / "refactors" / "file_diffs"
	out.mkdir(parents=True, exist_ok=True)
	# diff vs HEAD (or provided base)
	base = args.base or "HEAD"
	diff = _git("diff", base)
	fp = out / ("diff_" + (base.replace("/", "_")) + ".patch")
	fp.write_text(diff, encoding="utf-8")
	print(json.dumps({"ok": True, "wrote": str(fp), "base": base, "bytes": len(diff)}))
	return 0

def _py_ast_summary(path: Path) -> Dict[str, Any]:
	try:
		import ast
		tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
		funcs: List[str] = []


	meta: Dict[str, Any] = {}
	if args.meta:
		try:
			meta = json.loads(args.meta)
		except Exception:
			meta = {"raw": args.meta}
	row = {"ts": float(args.ts or 0.0) or __import__("time").time(), "event": args.event, "suite": args.suite or "all", "meta": meta}
	_write_jsonl(out, row)
	print(json.dumps({"ok": True, "wrote": str(out), "event": row["event"], "suite": row["suite"]}))
	return 0

def _git(*cmd: str) -> str:
	try:
		p = subprocess.run(["git", *cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8", check=False)
		return p.stdout
	except Exception:
		return ""

def cmd_capture_diff(args: argparse.Namespace) -> int:
	root = Path(__file__).resolve().parents[2]
	out = root / "data" / "traces" / "refactors" / "file_diffs"
	out.mkdir(parents=True, exist_ok=True)
	# diff vs HEAD (or provided base)
	base = args.base or "HEAD"
	diff = _git("diff", base)
	fp = out / ("diff_" + (base.replace("/", "_")) + ".patch")
	fp.write_text(diff, encoding="utf-8")
	print(json.dumps({"ok": True, "wrote": str(fp), "base": base, "bytes": len(diff)}))
	return 0

def _py_ast_summary(path: Path) -> Dict[str, Any]:
	try:
		import ast
		tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
		funcs: List[str] = []
		classes: List[str] = []
		for node in ast.walk(tree):
			if isinstance(node, ast.FunctionDef):
				funcs.append(node.name)
			elif isinstance(node, ast.AsyncFunctionDef):
				funcs.append(node.name)
			elif isinstance(node, ast.ClassDef):
				classes.append(node.name)
		return {"file": str(path), "functions": sorted(set(funcs)), "classes": sorted(set(classes))}
	except Exception as e:
		return {"file": str(path), "error": str(e)}


	try:
		p = subprocess.run(["git", *cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8", check=False)
		return p.stdout
	except Exception:
		return ""

def cmd_capture_diff(args: argparse.Namespace) -> int:
	root = Path(__file__).resolve().parents[2]
	out = root / "data" / "traces" / "refactors" / "file_diffs"
	out.mkdir(parents=True, exist_ok=True)
	# diff vs HEAD (or provided base)
	base = args.base or "HEAD"
	diff = _git("diff", base)
	fp = out / ("diff_" + (base.replace("/", "_")) + ".patch")
	fp.write_text(diff, encoding="utf-8")
	print(json.dumps({"ok": True, "wrote": str(fp), "base": base, "bytes": len(diff)}))
	return 0

def _py_ast_summary(path: Path) -> Dict[str, Any]:
	try:
		import ast
		tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
		funcs: List[str] = []
		classes: List[str] = []
		for node in ast.walk(tree):
			if isinstance(node, ast.FunctionDef):
				funcs.append(node.name)
			elif isinstance(node, ast.AsyncFunctionDef):
				funcs.append(node.name)
			elif isinstance(node, ast.ClassDef):
				classes.append(node.name)
		return {"file": str(path), "functions": sorted(set(funcs)), "classes": sorted(set(classes))}
	except Exception as e:
		return {"file": str(path), "error": str(e)}

def cmd_capture_ast(args: argparse.Namespace) -> int:
	root = Path(__file__).resolve().parents[2]
	out = root / "data" / "traces" / "refactors" / "ast_summaries"
	out.mkdir(parents=True, exist_ok=True)
	paths = [Path(p) for p in (args.paths or [])]
	rows: List[Dict[str, Any]] = []
	for p in paths:
		if p.is_dir():
			for fp in p.rglob("*.py"):
				rows.append(_py_ast_summary(fp))
		else:
			rows.append(_py_ast_summary(p))
	res = {"root": str(root), "count": len(rows), "summaries": rows[:200]}  # limit preview size
	fp = out / "ast_summary.json"
	fp.write_text(json.dumps(res, ensure_ascii=False, indent=2), encoding="utf-8")
	print(json.dumps({"ok": True, "wrote": str(fp), "count": len(rows)}))
	return 0


def _py_ast_summary(path: Path) -> Dict[str, Any]:
	try:
		import ast
		tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
		funcs: List[str] = []
		classes: List[str] = []
		for node in ast.walk(tree):
			if isinstance(node, ast.FunctionDef):
				funcs.append(node.name)
			elif isinstance(node, ast.AsyncFunctionDef):
				funcs.append(node.name)
			elif isinstance(node, ast.ClassDef):
				classes.append(node.name)
		return {"file": str(path), "functions": sorted(set(funcs)), "classes": sorted(set(classes))}
	except Exception as e:
		return {"file": str(path), "error": str(e)}

def cmd_capture_ast(args: argparse.Namespace) -> int:
	root = Path(__file__).resolve().parents[2]
	out = root / "data" / "traces" / "refactors" / "ast_summaries"
	out.mkdir(parents=True, exist_ok=True)
	paths = [Path(p) for p in (args.paths or [])]
	rows: List[Dict[str, Any]] = []
	for p in paths:
		if p.is_dir():
			for fp in p.rglob("*.py"):
				rows.append(_py_ast_summary(fp))
		else:
			rows.append(_py_ast_summary(p))
	res = {"root": str(root), "count": len(rows), "summaries": rows[:200]}  # limit preview size
	fp = out / "ast_summary.json"
	fp.write_text(json.dumps(res, ensure_ascii=False, indent=2), encoding="utf-8")
	print(json.dumps({"ok": True, "wrote": str(fp), "count": len(rows)}))
	return 0

def cmd_env_snapshot(args: argparse.Namespace) -> int:
	"""Capture brief environment summary for WM/P/A/V/U training context."""
	root = Path(__file__).resolve().parents[2]
	out = root / "data" / "traces" / "refactors" / "env_summaries"
	out.mkdir(parents=True, exist_ok=True)
	info: Dict[str, Any] = {"cwd": os.getcwd(), "python": sys.version}
	for mod in ("torch", "transformers", "datasets", "evalplus"):
		try:
			m = __import__(mod)
			info[mod] = getattr(m, "__version__", None)
		except Exception:
			info[mod] = None
	fp = out / "env.json"
	fp.write_text(json.dumps(info, ensure_ascii=False, indent=2), encoding="utf-8")
	print(json.dumps({"ok": True, "wrote": str(fp)}))
	return 0


def cmd_capture_ast(args: argparse.Namespace) -> int:
	root = Path(__file__).resolve().parents[2]
	out = root / "data" / "traces" / "refactors" / "ast_summaries"
	out.mkdir(parents=True, exist_ok=True)
	paths = [Path(p) for p in (args.paths or [])]
	rows: List[Dict[str, Any]] = []
	for p in paths:
		if p.is_dir():
			for fp in p.rglob("*.py"):
				rows.append(_py_ast_summary(fp))
		else:
			rows.append(_py_ast_summary(p))
	res = {"root": str(root), "count": len(rows), "summaries": rows[:200]}  # limit preview size
	fp = out / "ast_summary.json"
	fp.write_text(json.dumps(res, ensure_ascii=False, indent=2), encoding="utf-8")
	print(json.dumps({"ok": True, "wrote": str(fp), "count": len(rows)}))
	return 0

def cmd_env_snapshot(args: argparse.Namespace) -> int:
	"""Capture brief environment summary for WM/P/A/V/U training context."""
	root = Path(__file__).resolve().parents[2]
	out = root / "data" / "traces" / "refactors" / "env_summaries"
	out.mkdir(parents=True, exist_ok=True)
	info: Dict[str, Any] = {"cwd": os.getcwd(), "python": sys.version}
	for mod in ("torch", "transformers", "datasets", "evalplus"):
		try:
			m = __import__(mod)
			info[mod] = getattr(m, "__version__", None)
		except Exception:
			info[mod] = None
	fp = out / "env.json"
	fp.write_text(json.dumps(info, ensure_ascii=False, indent=2), encoding="utf-8")
	print(json.dumps({"ok": True, "wrote": str(fp)}))
	return 0

def main() -> int:
	ap = argparse.ArgumentParser(description="Refactor tracer utilities")
	sub = ap.add_subparsers(dest="cmd", required=True)

	ap_log = sub.add_parser("log", help="Log a refactor event")
	ap_log.add_argument("event")
	ap_log.add_argument("--suite", default="all")
	ap_log.add_argument("--meta", default="")
	ap_log.add_argument("--ts", default="")
	ap_log.set_defaults(func=cmd_log)

	ap_diff = sub.add_parser("capture-diff", help="Capture working diff vs base (HEAD by default)")
	ap_diff.add_argument("--base", default="HEAD")
	ap_diff.set_defaults(func=cmd_capture_diff)

	ap_ast = sub.add_parser("capture-ast", help="Capture AST summaries for given paths")
	ap_ast.add_argument("paths", nargs="+")
	ap_ast.set_defaults(func=cmd_capture_ast)


def cmd_env_snapshot(args: argparse.Namespace) -> int:
	"""Capture brief environment summary for WM/P/A/V/U training context."""
	root = Path(__file__).resolve().parents[2]
	out = root / "data" / "traces" / "refactors" / "env_summaries"
	out.mkdir(parents=True, exist_ok=True)
	info: Dict[str, Any] = {"cwd": os.getcwd(), "python": sys.version}
	for mod in ("torch", "transformers", "datasets", "evalplus"):
		try:
			m = __import__(mod)
			info[mod] = getattr(m, "__version__", None)
		except Exception:
			info[mod] = None
	fp = out / "env.json"
	fp.write_text(json.dumps(info, ensure_ascii=False, indent=2), encoding="utf-8")
	print(json.dumps({"ok": True, "wrote": str(fp)}))
	return 0

def main() -> int:
	ap = argparse.ArgumentParser(description="Refactor tracer utilities")
	sub = ap.add_subparsers(dest="cmd", required=True)

	ap_log = sub.add_parser("log", help="Log a refactor event")
	ap_log.add_argument("event")
	ap_log.add_argument("--suite", default="all")
	ap_log.add_argument("--meta", default="")
	ap_log.add_argument("--ts", default="")
	ap_log.set_defaults(func=cmd_log)

	ap_diff = sub.add_parser("capture-diff", help="Capture working diff vs base (HEAD by default)")
	ap_diff.add_argument("--base", default="HEAD")
	ap_diff.set_defaults(func=cmd_capture_diff)

	ap_ast = sub.add_parser("capture-ast", help="Capture AST summaries for given paths")
	ap_ast.add_argument("paths", nargs="+")
	ap_ast.set_defaults(func=cmd_capture_ast)

	ap_env = sub.add_parser("env-snapshot", help="Capture environment summary for WM/P/A/V/U context")
	ap_env.set_defaults(func=cmd_env_snapshot)

	args = ap.parse_args()
	return int(args.func(args))

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


from __future__ import annotations

import sys
import json
from pathlib import Path
from typing import Any, Dict

try:
    import yaml  # type: ignore
except Exception:
    yaml = None

def main() -> int:
    root = Path(__file__).resolve().parents[2]
    reg_path = root / "tools" / "registry.yaml"
    if not reg_path.exists():
        print(json.dumps({"ok": False, "error": "registry_missing", "path": str(reg_path)}))
        return 2
    if yaml is None:
        print(json.dumps({"ok": False, "error": "pyyaml_missing"}))
        return 2
    try:
        obj = yaml.safe_load(reg_path.read_text(encoding="utf-8"))
    except Exception as e:
        print(json.dumps({"ok": False, "error": f"yaml_parse:{e}"}))
        return 2
    # Minimal conformance checks
    errs = []
    if not isinstance(obj, dict):
        errs.append("top_not_object")
    if obj.get("version") != 1:
        errs.append("version_not_1")
    tools = obj.get("tools") if isinstance(obj, dict) else None
    if not isinstance(tools, list) or not tools:
        errs.append("tools_missing_or_empty")
    else:
        seen = set()
        for i, t in enumerate(tools):
            if not isinstance(t, dict):
                errs.append(f"tool_{i}_not_object")
                continue
            name = str(t.get("name", "")).strip()
            if not name:
                errs.append(f"tool_{i}_missing_name")
            if name in seen:
                errs.append(f"tool_{i}_duplicate_name:{name}")
            seen.add(name)
            if str(t.get("version", "")).strip() == "":
                errs.append(f"tool_{i}_missing_version")
            if not isinstance(t.get("inputs"), dict):
                errs.append(f"tool_{i}_inputs_missing")
            if not isinstance(t.get("outputs"), dict):
                errs.append(f"tool_{i}_outputs_missing")
            # Optional quotas/verifiers are lenient
    ok = len(errs) == 0
    print(json.dumps({"ok": ok, "errors": errs}))
    return 0 if ok else 2

if __name__ == "__main__":
    raise SystemExit(main())


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

import argparse
import json
import os
import re
from pathlib import Path
from typing import Iterable, List, Tuple

HEADER_RE = re.compile(r"^\s*(?:try|except\b[^:]*|finally|if\b[^:]*|elif\b[^:]*|else|for\b[^:]*|while\b[^:]*|with\b[^:]*|def\b[^:]*|class\b[^:]*):\s*(?:#.*)?$")

def _is_comment_or_empty(line: str) -> bool:
    stripped = line.strip()
    return stripped == "" or stripped.startswith("#")

def _leading_ws(line: str) -> str:
    i = 0
    while i < len(line) and line[i] in (" ", "\t"):
        i += 1
    return line[:i]

def _detect_indent_unit(lines: List[str]) -> str:
    # Prefer tabs if any leading tabs exist in the file
    for ln in lines:
        if ln.startswith("\t"):
            return "\t"
    # Otherwise infer space width from common indentation sizes
    space_counts: List[int] = []
    for ln in lines:
        ws = _leading_ws(ln)
        if ws and set(ws) == {" "}:
            space_counts.append(len(ws))
    # Heuristic: choose the most common small width divisor (2 or 4), default 4
    candidates = [2, 4, 8]
    best = 4
    if space_counts:
        for c in candidates:
            if any(sc % c == 0 for sc in space_counts):
                best = c
                break
    return " " * best

def _should_fix_block(lines: List[str], idx: int) -> Tuple[int, bool]:
    """Return (target_line_index, needs_fix) for the first non-empty, non-comment line after header idx."""
    n = len(lines)
    header_ws = _leading_ws(lines[idx])
    j = idx + 1
    while j < n and lines[j].strip() == "":
        j += 1
    # Skip comment-only lines, but do not attempt to reindent comments
    k = j
    while k < n and _is_comment_or_empty(lines[k]):
        # If it's a pure comment and at same indent or less, we still only care about the first code line
        k += 1
    if k >= n:
        return (j, False)
    next_ws = _leading_ws(lines[k])
    # Compare indentation width by raw length within the file's own style; safe if styles are consistent per file
    if len(next_ws) <= len(header_ws):
        return (k, True)
    return (k, False)

def fix_file(path: Path) -> Tuple[bool, int]:
    text = path.read_text(encoding="utf-8")
    lines = text.splitlines(keepends=False)
    if not lines:
        return (False, 0)
    indent_unit = _detect_indent_unit(lines)
    changed = False
    fixes = 0
    i = 0
    while i < len(lines):
        line = lines[i]
        if HEADER_RE.match(line or "") is not None:
            target_idx, needs_fix = _should_fix_block(lines, i)
            if needs_fix and 0 <= target_idx < len(lines):
                lines[target_idx] = f"{indent_unit}{lines[target_idx]}"
                changed = True
                fixes += 1
                # Skip over the just-fixed line to avoid double-processing nested headers immediately following
                i = max(i + 1, target_idx)
        i += 1
    if changed:
        path.write_text("\n".join(lines) + ("\n" if text.endswith("\n") else ""), encoding="utf-8")
    return (changed, fixes)

def iter_python_files(root: Path) -> Iterable[Path]:
    ignore_dirs = {".git", "__pycache__", "node_modules", "venv", ".venv", "data", "models"}
    for dirpath, dirnames, filenames in os.walk(root):
        # Prune ignored directories in-place for efficiency
        dirnames[:] = [d for d in dirnames if d not in ignore_dirs and not d.startswith(".")]
        for fn in filenames:
            if fn.endswith(".py"):
                yield Path(dirpath) / fn

def main() -> int:
    ap = argparse.ArgumentParser(description="Auto-fix missing indentation after block headers in Python files")
    ap.add_argument("paths", nargs="*", help="Files or directories to scan. Defaults to repository root.")
    ap.add_argument("--dry-run", action="store_true", help="Do not write changes; only report.")
    ap.add_argument("--verbose", action="store_true", help="Print each fix applied.")
    args = ap.parse_args()

    # Default to repo root (two parents above this script) if no explicit paths
    if args.paths:
        targets: List[Path] = []
        for p in args.paths:
            targets.append(Path(p).resolve())
    else:
        targets = [Path(__file__).resolve().parents[2]]

    files: List[Path] = []
    for t in targets:
        if t.is_file() and t.suffix == ".py":
            files.append(t)
        elif t.is_dir():
            files.extend(iter_python_files(t))

    total_changed = 0
    total_fixes = 0
    changed_files: List[str] = []
    for f in files:
        try:
            text = f.read_text(encoding="utf-8")
        except Exception:
            continue
        # Quick pre-filter: only consider files with suspicious patterns to avoid unnecessary writes
        if ":\n" not in text and ":\r\n" not in text:
            continue
        if args.dry_run:
            # Simulate without writing
            lines = text.splitlines(False)
            indent_unit = _detect_indent_unit(lines)
            i = 0
            file_fixes = 0
            while i < len(lines):
                if HEADER_RE.match(lines[i] or "") is not None:
                    _, needs = _should_fix_block(lines, i)
                    if needs:
                        file_fixes += 1
                i += 1
            if file_fixes:
                total_fixes += file_fixes
                changed_files.append(str(f))
                total_changed += 1
                if args.verbose:
                    print(json.dumps({"file": str(f), "fixes": file_fixes}))
        else:
            changed, fixes = fix_file(f)
            if changed:
                total_changed += 1
                total_fixes += fixes
                changed_files.append(str(f))
                if args.verbose:
                    print(json.dumps({"file": str(f), "fixes": fixes}))

    print(json.dumps({
        "ok": True,
        "changed_files": total_changed,
        "fixes": total_fixes,
        "files": changed_files if args.verbose else None,
    }))
    return 0

if __name__ == "__main__":
    raise SystemExit(main())


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

import argparse
import json
import os
import re
from pathlib import Path
from typing import Iterable, List, Tuple

HEADER_RE = re.compile(r"^\s*(?:try|except\b[^:]*|finally|if\b[^:]*|elif\b[^:]*|else|for\b[^:]*|while\b[^:]*|with\b[^:]*|def\b[^:]*|class\b[^:]*):\s*(?:#.*)?$")

def _is_comment_or_empty(line: str) -> bool:
    stripped = line.strip()
    return stripped == "" or stripped.startswith("#")

def _leading_ws(line: str) -> str:
    i = 0
    while i < len(line) and line[i] in (" ", "\t"):
        i += 1
    return line[:i]

def _detect_indent_unit(lines: List[str]) -> str:
    # Prefer tabs if any leading tabs exist in the file
    for ln in lines:
        if ln.startswith("\t"):
            return "\t"
    # Otherwise infer space width from common indentation sizes
    space_counts: List[int] = []
    for ln in lines:
        ws = _leading_ws(ln)
        if ws and set(ws) == {" "}:
            space_counts.append(len(ws))


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

import argparse
import json
import os
import re
from pathlib import Path
from typing import Iterable, List, Tuple

HEADER_RE = re.compile(r"^\s*(?:try|except\b[^:]*|finally|if\b[^:]*|elif\b[^:]*|else|for\b[^:]*|while\b[^:]*|with\b[^:]*|def\b[^:]*|class\b[^:]*):\s*(?:#.*)?$")

def _is_comment_or_empty(line: str) -> bool:
    stripped = line.strip()
    return stripped == "" or stripped.startswith("#")

def _leading_ws(line: str) -> str:
    i = 0
    while i < len(line) and line[i] in (" ", "\t"):
        i += 1
    return line[:i]

def _detect_indent_unit(lines: List[str]) -> str:
    # Prefer tabs if any leading tabs exist in the file
    for ln in lines:
        if ln.startswith("\t"):
            return "\t"
    # Otherwise infer space width from common indentation sizes
    space_counts: List[int] = []
    for ln in lines:
        ws = _leading_ws(ln)
        if ws and set(ws) == {" "}:
            space_counts.append(len(ws))
    # Heuristic: choose the most common small width divisor (2 or 4), default 4
    candidates = [2, 4, 8]
    best = 4
    if space_counts:
        for c in candidates:
            if any(sc % c == 0 for sc in space_counts):
                best = c


import re
from pathlib import Path
from typing import Iterable, List, Tuple

HEADER_RE = re.compile(r"^\s*(?:try|except\b[^:]*|finally|if\b[^:]*|elif\b[^:]*|else|for\b[^:]*|while\b[^:]*|with\b[^:]*|def\b[^:]*|class\b[^:]*):\s*(?:#.*)?$")

def _is_comment_or_empty(line: str) -> bool:
    stripped = line.strip()
    return stripped == "" or stripped.startswith("#")

def _leading_ws(line: str) -> str:
    i = 0
    while i < len(line) and line[i] in (" ", "\t"):
        i += 1
    return line[:i]

def _detect_indent_unit(lines: List[str]) -> str:
    # Prefer tabs if any leading tabs exist in the file
    for ln in lines:
        if ln.startswith("\t"):
            return "\t"
    # Otherwise infer space width from common indentation sizes
    space_counts: List[int] = []
    for ln in lines:
        ws = _leading_ws(ln)
        if ws and set(ws) == {" "}:
            space_counts.append(len(ws))
    # Heuristic: choose the most common small width divisor (2 or 4), default 4
    candidates = [2, 4, 8]
    best = 4
    if space_counts:
        for c in candidates:
            if any(sc % c == 0 for sc in space_counts):
                best = c
                break
    return " " * best

def _should_fix_block(lines: List[str], idx: int) -> Tuple[int, bool]:
    """Return (target_line_index, needs_fix) for the first non-empty, non-comment line after header idx."""
    n = len(lines)
    header_ws = _leading_ws(lines[idx])
    j = idx + 1
    while j < n and lines[j].strip() == "":
        j += 1
    # Skip comment-only lines, but do not attempt to reindent comments
    k = j
    while k < n and _is_comment_or_empty(lines[k]):
        # If it's a pure comment and at same indent or less, we still only care about the first code line
        k += 1
    if k >= n:
        return (j, False)
    next_ws = _leading_ws(lines[k])
    # Compare indentation width by raw length within the file's own style; safe if styles are consistent per file
    if len(next_ws) <= len(header_ws):
        return (k, True)


    for ln in lines:
        if ln.startswith("\t"):
            return "\t"
    # Otherwise infer space width from common indentation sizes
    space_counts: List[int] = []
    for ln in lines:
        ws = _leading_ws(ln)
        if ws and set(ws) == {" "}:
            space_counts.append(len(ws))
    # Heuristic: choose the most common small width divisor (2 or 4), default 4
    candidates = [2, 4, 8]
    best = 4
    if space_counts:
        for c in candidates:
            if any(sc % c == 0 for sc in space_counts):
                best = c
                break
    return " " * best

def _should_fix_block(lines: List[str], idx: int) -> Tuple[int, bool]:
    """Return (target_line_index, needs_fix) for the first non-empty, non-comment line after header idx."""
    n = len(lines)
    header_ws = _leading_ws(lines[idx])
    j = idx + 1
    while j < n and lines[j].strip() == "":
        j += 1
    # Skip comment-only lines, but do not attempt to reindent comments
    k = j
    while k < n and _is_comment_or_empty(lines[k]):
        # If it's a pure comment and at same indent or less, we still only care about the first code line
        k += 1
    if k >= n:
        return (j, False)
    next_ws = _leading_ws(lines[k])
    # Compare indentation width by raw length within the file's own style; safe if styles are consistent per file
    if len(next_ws) <= len(header_ws):
        return (k, True)
    return (k, False)

def fix_file(path: Path) -> Tuple[bool, int]:
    text = path.read_text(encoding="utf-8")
    lines = text.splitlines(keepends=False)
    if not lines:
        return (False, 0)
    indent_unit = _detect_indent_unit(lines)
    changed = False
    fixes = 0
    i = 0
    while i < len(lines):
        line = lines[i]
        if HEADER_RE.match(line or "") is not None:
            target_idx, needs_fix = _should_fix_block(lines, i)
            if needs_fix and 0 <= target_idx < len(lines):
                lines[target_idx] = f"{indent_unit}{lines[target_idx]}"
                changed = True
                fixes += 1
                # Skip over the just-fixed line to avoid double-processing nested headers immediately following


    """Return (target_line_index, needs_fix) for the first non-empty, non-comment line after header idx."""
    n = len(lines)
    header_ws = _leading_ws(lines[idx])
    j = idx + 1
    while j < n and lines[j].strip() == "":
        j += 1
    # Skip comment-only lines, but do not attempt to reindent comments
    k = j
    while k < n and _is_comment_or_empty(lines[k]):
        # If it's a pure comment and at same indent or less, we still only care about the first code line
        k += 1
    if k >= n:
        return (j, False)
    next_ws = _leading_ws(lines[k])
    # Compare indentation width by raw length within the file's own style; safe if styles are consistent per file
    if len(next_ws) <= len(header_ws):
        return (k, True)
    return (k, False)

def fix_file(path: Path) -> Tuple[bool, int]:
    text = path.read_text(encoding="utf-8")
    lines = text.splitlines(keepends=False)
    if not lines:
        return (False, 0)
    indent_unit = _detect_indent_unit(lines)
    changed = False
    fixes = 0
    i = 0
    while i < len(lines):
        line = lines[i]
        if HEADER_RE.match(line or "") is not None:
            target_idx, needs_fix = _should_fix_block(lines, i)
            if needs_fix and 0 <= target_idx < len(lines):
                lines[target_idx] = f"{indent_unit}{lines[target_idx]}"
                changed = True
                fixes += 1
                # Skip over the just-fixed line to avoid double-processing nested headers immediately following
                i = max(i + 1, target_idx)
        i += 1
    if changed:
        path.write_text("\n".join(lines) + ("\n" if text.endswith("\n") else ""), encoding="utf-8")
    return (changed, fixes)

def iter_python_files(root: Path) -> Iterable[Path]:
    ignore_dirs = {".git", "__pycache__", "node_modules", "venv", ".venv", "data", "models"}
    for dirpath, dirnames, filenames in os.walk(root):
        # Prune ignored directories in-place for efficiency
        dirnames[:] = [d for d in dirnames if d not in ignore_dirs and not d.startswith(".")]
        for fn in filenames:
            if fn.endswith(".py"):
                yield Path(dirpath) / fn

def main() -> int:
    ap = argparse.ArgumentParser(description="Auto-fix missing indentation after block headers in Python files")
    ap.add_argument("paths", nargs="*", help="Files or directories to scan. Defaults to repository root.")
    ap.add_argument("--dry-run", action="store_true", help="Do not write changes; only report.")
    ap.add_argument("--verbose", action="store_true", help="Print each fix applied.")
    args = ap.parse_args()

    # Default to repo root (two parents above this script) if no explicit paths


    indent_unit = _detect_indent_unit(lines)
    changed = False
    fixes = 0
    i = 0
    while i < len(lines):
        line = lines[i]
        if HEADER_RE.match(line or "") is not None:
            target_idx, needs_fix = _should_fix_block(lines, i)
            if needs_fix and 0 <= target_idx < len(lines):
                lines[target_idx] = f"{indent_unit}{lines[target_idx]}"
                changed = True
                fixes += 1
                # Skip over the just-fixed line to avoid double-processing nested headers immediately following
                i = max(i + 1, target_idx)
        i += 1
    if changed:
        path.write_text("\n".join(lines) + ("\n" if text.endswith("\n") else ""), encoding="utf-8")
    return (changed, fixes)

def iter_python_files(root: Path) -> Iterable[Path]:
    ignore_dirs = {".git", "__pycache__", "node_modules", "venv", ".venv", "data", "models"}
    for dirpath, dirnames, filenames in os.walk(root):
        # Prune ignored directories in-place for efficiency
        dirnames[:] = [d for d in dirnames if d not in ignore_dirs and not d.startswith(".")]
        for fn in filenames:
            if fn.endswith(".py"):
                yield Path(dirpath) / fn

def main() -> int:
    ap = argparse.ArgumentParser(description="Auto-fix missing indentation after block headers in Python files")
    ap.add_argument("paths", nargs="*", help="Files or directories to scan. Defaults to repository root.")
    ap.add_argument("--dry-run", action="store_true", help="Do not write changes; only report.")
    ap.add_argument("--verbose", action="store_true", help="Print each fix applied.")
    args = ap.parse_args()

    # Default to repo root (two parents above this script) if no explicit paths
    if args.paths:
        targets: List[Path] = []
        for p in args.paths:
            targets.append(Path(p).resolve())
    else:
        targets = [Path(__file__).resolve().parents[2]]

    files: List[Path] = []
    for t in targets:
        if t.is_file() and t.suffix == ".py":


                changed = True
                fixes += 1
                # Skip over the just-fixed line to avoid double-processing nested headers immediately following
                i = max(i + 1, target_idx)
        i += 1
    if changed:
        path.write_text("\n".join(lines) + ("\n" if text.endswith("\n") else ""), encoding="utf-8")
    return (changed, fixes)

def iter_python_files(root: Path) -> Iterable[Path]:
    ignore_dirs = {".git", "__pycache__", "node_modules", "venv", ".venv", "data", "models"}
    for dirpath, dirnames, filenames in os.walk(root):
        # Prune ignored directories in-place for efficiency
        dirnames[:] = [d for d in dirnames if d not in ignore_dirs and not d.startswith(".")]
        for fn in filenames:
            if fn.endswith(".py"):
                yield Path(dirpath) / fn

def main() -> int:
    ap = argparse.ArgumentParser(description="Auto-fix missing indentation after block headers in Python files")
    ap.add_argument("paths", nargs="*", help="Files or directories to scan. Defaults to repository root.")
    ap.add_argument("--dry-run", action="store_true", help="Do not write changes; only report.")
    ap.add_argument("--verbose", action="store_true", help="Print each fix applied.")
    args = ap.parse_args()

    # Default to repo root (two parents above this script) if no explicit paths
    if args.paths:
        targets: List[Path] = []
        for p in args.paths:
            targets.append(Path(p).resolve())
    else:
        targets = [Path(__file__).resolve().parents[2]]

    files: List[Path] = []
    for t in targets:
        if t.is_file() and t.suffix == ".py":
            files.append(t)
        elif t.is_dir():
            files.extend(iter_python_files(t))

    total_changed = 0
    total_fixes = 0
    changed_files: List[str] = []
    for f in files:
        try:
            text = f.read_text(encoding="utf-8")
        except Exception:
            continue
        # Quick pre-filter: only consider files with suspicious patterns to avoid unnecessary writes
        if ":\n" not in text and ":\r\n" not in text:
            continue
        if args.dry_run:
            # Simulate without writing
            lines = text.splitlines(False)
            indent_unit = _detect_indent_unit(lines)
            i = 0
            file_fixes = 0
            while i < len(lines):
                if HEADER_RE.match(lines[i] or "") is not None:
                    _, needs = _should_fix_block(lines, i)
                    if needs:
                        file_fixes += 1
                i += 1
            if file_fixes:
                total_fixes += file_fixes
                changed_files.append(str(f))
                total_changed += 1
                if args.verbose:
                    print(json.dumps({"file": str(f), "fixes": file_fixes}))
        else:
            changed, fixes = fix_file(f)
            if changed:
                total_changed += 1
                total_fixes += fixes
                changed_files.append(str(f))
                if args.verbose:
                    print(json.dumps({"file": str(f), "fixes": fixes}))

    print(json.dumps({
        "ok": True,
        "changed_files": total_changed,
        "fixes": total_fixes,
        "files": changed_files if args.verbose else None,
    }))
    return 0

if __name__ == "__main__":
    raise SystemExit(main())


from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
from typing import Any, Dict, List, Set

def index_repo(repo_root: Path) -> Dict[str, Any]:
    try:
        from agi_dw.tools.code_index import index_python_repo  # type: ignore
    except Exception as e:  # pragma: no cover
        return {"root": str(repo_root), "functions": {}, "error": f"code_index unavailable: {e}"}
    try:
        return index_python_repo(repo_root)
    except Exception as e:  # pragma: no cover
        return {"root": str(repo_root), "functions": {}, "error": str(e)}

def merge_indexes(indexes: List[Dict[str, Any]]) -> Dict[str, Any]:
    """Merge per-repo indexes into a single multi-repo index.

    Preserves functions/classes/calls/imports maps keyed by absolute file path.
    """
    merged: Dict[str, Any] = {"functions": {}, "classes": {}, "calls": {}, "imports": {}, "roots": []}
    for idx in indexes:
        root = str(idx.get("root", ""))
        if root:
            merged["roots"].append(root)
        for key in ("functions", "classes", "calls"):
            m: Dict[str, List[Dict[str, Any]]] = merged.setdefault(key, {})  # type: ignore
            src: Dict[str, List[Dict[str, Any]]] = (idx.get(key, {}) or {})  # type: ignore
            for fpath, entries in (src.items() if isinstance(src, dict) else []):
                try:
                    lst = m.setdefault(fpath, [])
                    for it in (entries or []):
                        name = str(it.get("name", ""))
                        if not name:
                            continue
                        lst.append({"name": name, **{k: v for k, v in it.items() if k != "name"}})
                except Exception:
                    continue
        # Imports map is str -> List[str]
        imap: Dict[str, List[str]] = merged.setdefault("imports", {})  # type: ignore
        src_imap: Dict[str, List[str]] = (idx.get("imports", {}) or {})  # type: ignore
        for fpath, imps in (src_imap.items() if isinstance(src_imap, dict) else []):
            try:
                acc = imap.setdefault(fpath, [])
                for imp in (imps or []):
                    if imp not in acc:
                        acc.append(imp)
            except Exception:
                continue
    return merged

def _is_repo_root(p: Path) -> bool:
    try:
        if (p / ".git").exists():
            return True
        if (p / "pyproject.toml").exists() or (p / "setup.py").exists():
            return True
        if (p / "package.json").exists():
            return True
        # Fallback: has at least one .py
        for _ in p.glob("*.py"):
            return True
        return False
    except Exception:
        return False

def discover_repos(root: Path, max_repos: int | None = None, max_depth: int = 2) -> List[Path]:
    """Recursively discover repo roots under root up to max_depth.

    Skips heavy/irrelevant directories for speed.
    """
    ignore_names: Set[str] = {".git", "node_modules", ".venv", "venv", "dist", "build", "models", "__pycache__"}
    found: List[Path] = []

    def _walk(dirp: Path, depth: int) -> None:
        nonlocal found
        if max_repos is not None and len(found) >= max_repos:
            return
        if depth > max_depth:
            return
        try:
            # If current dir looks like a repo, record it and do not dive further inside
            if _is_repo_root(dirp):
                found.append(dirp)
                return
            for entry in sorted(dirp.iterdir()):
                if not entry.is_dir():
                    continue
                name = entry.name
                if name in ignore_names or name.startswith('.'):
                    continue
                _walk(entry, depth + 1)
                if max_repos is not None and len(found) >= max_repos:
                    return
        except Exception:
            return

    _walk(root, depth=0)
    return found

def main() -> int:
    ap = argparse.ArgumentParser(description="Index the 'inspiration' directory (multiple repos, recursive) into one code index")
    default_root = Path("/data/agiattempt/inspiration")
    project_root = Path(__file__).resolve().parents[2]
    default_out = project_root / "data" / "sandbox" / "inspiration" / "index.json"
    ap.add_argument("--root", default=str(default_root))
    ap.add_argument("--out", default=str(default_out))
    ap.add_argument("--max-repos", type=int, default=0, help="Limit the number of repos to index (0 = all)")
    ap.add_argument("--max-depth", type=int, default=2, help="Max recursion depth when scanning for nested repos")
    args = ap.parse_args()

    root = Path(str(getattr(args, "root")))
    out = Path(str(getattr(args, "out")))
    out.parent.mkdir(parents=True, exist_ok=True)

    if not root.exists() or not root.is_dir():
        print(json.dumps({"ok": False, "error": f"root not found: {root}"}))
        return 2

    max_repos = int(getattr(args, "max_repos", 0) or 0) or None
    max_depth = int(getattr(args, "max_depth", 2) or 2)
    repos = discover_repos(root, max_repos=max_repos, max_depth=max_depth)
    indexes: List[Dict[str, Any]] = []
    for repo in repos:
        idx = index_repo(repo)
        idx["root"] = str(repo)
        indexes.append(idx)

    merged = merge_indexes(indexes)
    merged["meta"] = {
        "root": str(root),
        "n_repos": len(repos),
    }
    out.write_text(json.dumps(merged, ensure_ascii=False, indent=2), encoding="utf-8")
    print(json.dumps({"ok": True, "out": str(out), "n_repos": len(repos), "n_filesets": len(merged.get("functions", {}))}))
    return 0

if __name__ == "__main__":
    raise SystemExit(main())


from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
from typing import Any, Dict, List, Set

def index_repo(repo_root: Path) -> Dict[str, Any]:
    try:
        from agi_dw.tools.code_index import index_python_repo  # type: ignore
    except Exception as e:  # pragma: no cover
        return {"root": str(repo_root), "functions": {}, "error": f"code_index unavailable: {e}"}
    try:
        return index_python_repo(repo_root)
    except Exception as e:  # pragma: no cover
        return {"root": str(repo_root), "functions": {}, "error": str(e)}

def merge_indexes(indexes: List[Dict[str, Any]]) -> Dict[str, Any]:
    """Merge per-repo indexes into a single multi-repo index.

    Preserves functions/classes/calls/imports maps keyed by absolute file path.
    """
    merged: Dict[str, Any] = {"functions": {}, "classes": {}, "calls": {}, "imports": {}, "roots": []}
    for idx in indexes:
        root = str(idx.get("root", ""))
        if root:
            merged["roots"].append(root)
        for key in ("functions", "classes", "calls"):
            m: Dict[str, List[Dict[str, Any]]] = merged.setdefault(key, {})  # type: ignore
            src: Dict[str, List[Dict[str, Any]]] = (idx.get(key, {}) or {})  # type: ignore
            for fpath, entries in (src.items() if isinstance(src, dict) else []):
                try:
                    lst = m.setdefault(fpath, [])
                    for it in (entries or []):
                        name = str(it.get("name", ""))


from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
from typing import Any, Dict, List, Set

def index_repo(repo_root: Path) -> Dict[str, Any]:
    try:
        from agi_dw.tools.code_index import index_python_repo  # type: ignore
    except Exception as e:  # pragma: no cover
        return {"root": str(repo_root), "functions": {}, "error": f"code_index unavailable: {e}"}
    try:
        return index_python_repo(repo_root)
    except Exception as e:  # pragma: no cover
        return {"root": str(repo_root), "functions": {}, "error": str(e)}

def merge_indexes(indexes: List[Dict[str, Any]]) -> Dict[str, Any]:
    """Merge per-repo indexes into a single multi-repo index.

    Preserves functions/classes/calls/imports maps keyed by absolute file path.
    """
    merged: Dict[str, Any] = {"functions": {}, "classes": {}, "calls": {}, "imports": {}, "roots": []}
    for idx in indexes:
        root = str(idx.get("root", ""))
        if root:
            merged["roots"].append(root)
        for key in ("functions", "classes", "calls"):
            m: Dict[str, List[Dict[str, Any]]] = merged.setdefault(key, {})  # type: ignore
            src: Dict[str, List[Dict[str, Any]]] = (idx.get(key, {}) or {})  # type: ignore
            for fpath, entries in (src.items() if isinstance(src, dict) else []):
                try:
                    lst = m.setdefault(fpath, [])
                    for it in (entries or []):
                        name = str(it.get("name", ""))
                        if not name:
                            continue
                        lst.append({"name": name, **{k: v for k, v in it.items() if k != "name"}})
                except Exception:
                    continue
        # Imports map is str -> List[str]
        imap: Dict[str, List[str]] = merged.setdefault("imports", {})  # type: ignore
        src_imap: Dict[str, List[str]] = (idx.get("imports", {}) or {})  # type: ignore
        for fpath, imps in (src_imap.items() if isinstance(src_imap, dict) else []):
            try:
                acc = imap.setdefault(fpath, [])
                for imp in (imps or []):
                    if imp not in acc:
                        acc.append(imp)
            except Exception:
                continue
    return merged

def _is_repo_root(p: Path) -> bool:
    try:
        if (p / ".git").exists():
            return True
        if (p / "pyproject.toml").exists() or (p / "setup.py").exists():
            return True
        if (p / "package.json").exists():
            return True
        # Fallback: has at least one .py
        for _ in p.glob("*.py"):
            return True
        return False
    except Exception:
        return False

def discover_repos(root: Path, max_repos: int | None = None, max_depth: int = 2) -> List[Path]:
    """Recursively discover repo roots under root up to max_depth.


                        name = str(it.get("name", ""))
                        if not name:
                            continue
                        lst.append({"name": name, **{k: v for k, v in it.items() if k != "name"}})
                except Exception:
                    continue
        # Imports map is str -> List[str]
        imap: Dict[str, List[str]] = merged.setdefault("imports", {})  # type: ignore
        src_imap: Dict[str, List[str]] = (idx.get("imports", {}) or {})  # type: ignore
        for fpath, imps in (src_imap.items() if isinstance(src_imap, dict) else []):
            try:
                acc = imap.setdefault(fpath, [])
                for imp in (imps or []):
                    if imp not in acc:
                        acc.append(imp)
            except Exception:
                continue
    return merged

def _is_repo_root(p: Path) -> bool:
    try:
        if (p / ".git").exists():
            return True
        if (p / "pyproject.toml").exists() or (p / "setup.py").exists():
            return True
        if (p / "package.json").exists():
            return True
        # Fallback: has at least one .py
        for _ in p.glob("*.py"):
            return True
        return False
    except Exception:
        return False

def discover_repos(root: Path, max_repos: int | None = None, max_depth: int = 2) -> List[Path]:
    """Recursively discover repo roots under root up to max_depth.

    Skips heavy/irrelevant directories for speed.
    """
    ignore_names: Set[str] = {".git", "node_modules", ".venv", "venv", "dist", "build", "models", "__pycache__"}
    found: List[Path] = []

    def _walk(dirp: Path, depth: int) -> None:
        nonlocal found
        if max_repos is not None and len(found) >= max_repos:
            return
        if depth > max_depth:
            return
        try:
            # If current dir looks like a repo, record it and do not dive further inside
            if _is_repo_root(dirp):
                found.append(dirp)


                continue
    return merged

def _is_repo_root(p: Path) -> bool:
    try:
        if (p / ".git").exists():
            return True
        if (p / "pyproject.toml").exists() or (p / "setup.py").exists():
            return True
        if (p / "package.json").exists():
            return True
        # Fallback: has at least one .py
        for _ in p.glob("*.py"):
            return True
        return False
    except Exception:
        return False

def discover_repos(root: Path, max_repos: int | None = None, max_depth: int = 2) -> List[Path]:
    """Recursively discover repo roots under root up to max_depth.

    Skips heavy/irrelevant directories for speed.
    """
    ignore_names: Set[str] = {".git", "node_modules", ".venv", "venv", "dist", "build", "models", "__pycache__"}
    found: List[Path] = []

    def _walk(dirp: Path, depth: int) -> None:
        nonlocal found
        if max_repos is not None and len(found) >= max_repos:
            return
        if depth > max_depth:
            return
        try:
            # If current dir looks like a repo, record it and do not dive further inside
            if _is_repo_root(dirp):
                found.append(dirp)
                return
            for entry in sorted(dirp.iterdir()):
                if not entry.is_dir():
                    continue
                name = entry.name
                if name in ignore_names or name.startswith('.'):
                    continue
                _walk(entry, depth + 1)
                if max_repos is not None and len(found) >= max_repos:
                    return
        except Exception:
            return

    _walk(root, depth=0)
    return found

def main() -> int:
    ap = argparse.ArgumentParser(description="Index the 'inspiration' directory (multiple repos, recursive) into one code index")
    default_root = Path("/data/agiattempt/inspiration")
    project_root = Path(__file__).resolve().parents[2]
    default_out = project_root / "data" / "sandbox" / "inspiration" / "index.json"
    ap.add_argument("--root", default=str(default_root))
    ap.add_argument("--out", default=str(default_out))
    ap.add_argument("--max-repos", type=int, default=0, help="Limit the number of repos to index (0 = all)")
    ap.add_argument("--max-depth", type=int, default=2, help="Max recursion depth when scanning for nested repos")
    args = ap.parse_args()

    root = Path(str(getattr(args, "root")))
    out = Path(str(getattr(args, "out")))
    out.parent.mkdir(parents=True, exist_ok=True)

    if not root.exists() or not root.is_dir():
        print(json.dumps({"ok": False, "error": f"root not found: {root}"}))
        return 2


            # If current dir looks like a repo, record it and do not dive further inside
            if _is_repo_root(dirp):
                found.append(dirp)
                return
            for entry in sorted(dirp.iterdir()):
                if not entry.is_dir():
                    continue
                name = entry.name
                if name in ignore_names or name.startswith('.'):
                    continue
                _walk(entry, depth + 1)
                if max_repos is not None and len(found) >= max_repos:
                    return
        except Exception:
            return

    _walk(root, depth=0)
    return found

def main() -> int:
    ap = argparse.ArgumentParser(description="Index the 'inspiration' directory (multiple repos, recursive) into one code index")
    default_root = Path("/data/agiattempt/inspiration")
    project_root = Path(__file__).resolve().parents[2]
    default_out = project_root / "data" / "sandbox" / "inspiration" / "index.json"
    ap.add_argument("--root", default=str(default_root))
    ap.add_argument("--out", default=str(default_out))
    ap.add_argument("--max-repos", type=int, default=0, help="Limit the number of repos to index (0 = all)")
    ap.add_argument("--max-depth", type=int, default=2, help="Max recursion depth when scanning for nested repos")
    args = ap.parse_args()

    root = Path(str(getattr(args, "root")))
    out = Path(str(getattr(args, "out")))
    out.parent.mkdir(parents=True, exist_ok=True)

    if not root.exists() or not root.is_dir():
        print(json.dumps({"ok": False, "error": f"root not found: {root}"}))
        return 2

    max_repos = int(getattr(args, "max_repos", 0) or 0) or None
    max_depth = int(getattr(args, "max_depth", 2) or 2)
    repos = discover_repos(root, max_repos=max_repos, max_depth=max_depth)
    indexes: List[Dict[str, Any]] = []
    for repo in repos:
        idx = index_repo(repo)
        idx["root"] = str(repo)
        indexes.append(idx)

    merged = merge_indexes(indexes)
    merged["meta"] = {
        "root": str(root),
        "n_repos": len(repos),
    }
    out.write_text(json.dumps(merged, ensure_ascii=False, indent=2), encoding="utf-8")
    print(json.dumps({"ok": True, "out": str(out), "n_repos": len(repos), "n_filesets": len(merged.get("functions", {}))}))
    return 0

if __name__ == "__main__":
    raise SystemExit(main())


        if (p / "pyproject.toml").exists() or (p / "setup.py").exists():
            return True
        if (p / "package.json").exists():
            return True
        # Fallback: has at least one .py
        for _ in p.glob("*.py"):
            return True
        return False
    except Exception:
        return False

def discover_repos(root: Path, max_repos: int | None = None, max_depth: int = 2) -> List[Path]:
    """Recursively discover repo roots under root up to max_depth.

    Skips heavy/irrelevant directories for speed.
    """
    ignore_names: Set[str] = {".git", "node_modules", ".venv", "venv", "dist", "build", "models", "__pycache__"}
    found: List[Path] = []

    def _walk(dirp: Path, depth: int) -> None:
        nonlocal found
        if max_repos is not None and len(found) >= max_repos:
            return
        if depth > max_depth:
            return
        try:
            # If current dir looks like a repo, record it and do not dive further inside
            if _is_repo_root(dirp):
                found.append(dirp)
                return
            for entry in sorted(dirp.iterdir()):
                if not entry.is_dir():
                    continue
                name = entry.name
                if name in ignore_names or name.startswith('.'):
                    continue
                _walk(entry, depth + 1)
                if max_repos is not None and len(found) >= max_repos:
                    return
        except Exception:
            return

    _walk(root, depth=0)
    return found

def main() -> int:
    ap = argparse.ArgumentParser(description="Index the 'inspiration' directory (multiple repos, recursive) into one code index")
    default_root = Path("/data/agiattempt/inspiration")
    project_root = Path(__file__).resolve().parents[2]
    default_out = project_root / "data" / "sandbox" / "inspiration" / "index.json"
    ap.add_argument("--root", default=str(default_root))
    ap.add_argument("--out", default=str(default_out))
    ap.add_argument("--max-repos", type=int, default=0, help="Limit the number of repos to index (0 = all)")
    ap.add_argument("--max-depth", type=int, default=2, help="Max recursion depth when scanning for nested repos")
    args = ap.parse_args()

    root = Path(str(getattr(args, "root")))
    out = Path(str(getattr(args, "out")))
    out.parent.mkdir(parents=True, exist_ok=True)


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

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

def _load_json(path: Path) -> Dict[str, Any]:
	if not path.exists():
		return {}
	try:
		return json.loads(path.read_text(encoding="utf-8"))
	except Exception:
		return {}

def _run_cmd(cmd: str, cwd: Path | None = None, timeout: int = 180) -> Tuple[int, str, str]:
	try:
		res = subprocess.run(cmd, cwd=str(cwd) if cwd else None, shell=True, capture_output=True, text=True, timeout=timeout)
		return res.returncode, res.stdout.strip(), res.stderr.strip()
	except Exception as e:
		return 127, "", str(e)

def _git_stats(root: Path) -> Dict[str, Any]:
	code, out, _ = _run_cmd("git rev-parse HEAD", cwd=root)
	commit = out.strip() if code == 0 else ""
	code, out, _ = _run_cmd("git diff --name-only HEAD~1..HEAD", cwd=root)
	files = [l.strip() for l in out.splitlines() if l.strip()] if code == 0 else []
	code, out, _ = _run_cmd("git diff --numstat HEAD~1..HEAD", cwd=root)
	added = 0
	deleted = 0
	if code == 0:
		for line in out.splitlines():
			parts = [p.strip() for p in line.split("\t")]
			if len(parts) >= 3:
				try:
					added += int(parts[0]) if parts[0].isdigit() else 0
					deleted += int(parts[1]) if parts[1].isdigit() else 0
				except Exception:
					pass
	return {"commit": commit, "files": files, "lines_added": added, "lines_deleted": deleted, "files_changed": len(files)}

def _detect_and_run_checks(root: Path) -> Dict[str, Any]:
	from agi_dw.tools.repo_manifest import detect_test_cmds, detect_lint_cmds, detect_type_check_cmds  # type: ignore
	results: Dict[str, Any] = {"tests": {}, "types": {}, "lint": {}}
	# Tests
	test_cmds = detect_test_cmds(root)
	if test_cmds:
		code, out, err = _run_cmd(test_cmds[0], cwd=root)
		results["tests"] = {"cmd": test_cmds[0], "code": code}
	# Type checks
	type_cmds = detect_type_check_cmds(root)
	if type_cmds:
		code, out, err = _run_cmd(type_cmds[0], cwd=root)
		results["types"] = {"cmd": type_cmds[0], "code": code}
	# Lint
	lint_cmds = detect_lint_cmds(root)
	if lint_cmds:
		code, out, err = _run_cmd(lint_cmds[0], cwd=root)
		results["lint"] = {"cmd": lint_cmds[0], "code": code}
	return results

def main() -> int:
	root = Path(__file__).resolve().parents[2]
	art_dir = root / "artifacts"
	art_dir.mkdir(parents=True, exist_ok=True)
	# Load pillar outputs
	base_tmp = root / "data" / "sandbox" / "tmp"
	risk = _load_json(base_tmp / "plan_risk.json")
	api = _load_json(base_tmp / "plan_api_diff.json")
	obs = _load_json(base_tmp / "obs_check.json")
	slo = _load_json(base_tmp / "slo_report.json")
	deps = _load_json(base_tmp / "deps_audit.json")
	deps_up = _load_json(base_tmp / "deps_upgrade.json")
	impact = _load_json(base_tmp / "plan_impact.json")
	# Git stats and best-effort checks
	gitinfo = _git_stats(root)
	checks = _detect_and_run_checks(root)
	# Compose evidence
	evidence = {
		"commit": gitinfo.get("commit", ""),
		"risk": float((risk.get("score") if isinstance(risk.get("score"), (int, float)) else 0.0) or 0.0),
		"files_changed": int(gitinfo.get("files_changed", 0) or 0),
		"tests": {"code": checks.get("tests", {}).get("code", 127)},
		"types": {"code": checks.get("types", {}).get("code", 127)},
		"perf": {"p95_ms_delta": 0.0},
		"api": {"breaking": bool((api.get("api_diff", {}) or {}).get("breaking")) if isinstance(api.get("api_diff"), dict) else False, "diff_report": "data/sandbox/docs/reports/plan_api_diff.md"},
		"security": {"high": len([f for f in (deps.get("findings", []) or []) if str(f).lower().find("high") >= 0])},
		"docs": {"drift_fixed": True},
	}
	# Attach minimal gate statuses and risk fragments for reviewers
	evidence["gates"] = {
		"plan_risk_ok": bool(risk.get("ok", True) if isinstance(risk, dict) else True),
		"slo_ok": bool(slo.get("ok", True) if isinstance(slo, dict) else True),
		"deps_ok": bool(deps.get("ok", True) if isinstance(deps, dict) else True),
		"deps_upgrade_planned": len(deps_up.get("planned", []) if isinstance(deps_up, dict) else [])
	}
	# Include top-3 risk entries and top-3 impact modules for context
	try:
		if isinstance(risk, dict):
			rs = list(risk.get("risk", []) or [])[:3]
			evidence["risk_top"] = rs
	except Exception:
		pass
	try:
		im = (impact.get("impact", {}) or {}).get("top_modules", []) if isinstance(impact, dict) else []
		evidence["impact_top"] = im[:3]
	except Exception:
		pass
	(art_dir / "pr_evidence.json").write_text(json.dumps(evidence, ensure_ascii=False, indent=2), encoding="utf-8")
	# Markdown summary
	summary_lines: List[str] = []
	summary_lines.append(f"Commit: {evidence['commit']}")
	summary_lines.append(f"Risk: {evidence['risk']}")
	summary_lines.append(f"Files changed: {evidence['files_changed']}")
	summary_lines.append(f"Tests exit: {evidence['tests']['code']}")
	summary_lines.append(f"Types exit: {evidence['types']['code']}")
	summary_lines.append(f"API breaking: {evidence['api']['breaking']}")
	summary_lines.append(f"Security high: {evidence['security']['high']}")
	(art_dir / "summary.md").write_text("\n".join(summary_lines) + "\n", encoding="utf-8")
	print(json.dumps({"ok": True, "evidence": str(art_dir / "pr_evidence.json"), "summary": str(art_dir / "summary.md")}, ensure_ascii=False))
	return 0

if __name__ == "__main__":
	raise SystemExit(main())


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

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

def _load_json(path: Path) -> Dict[str, Any]:
	if not path.exists():
		return {}
	try:
		return json.loads(path.read_text(encoding="utf-8"))
	except Exception:
		return {}

def _run_cmd(cmd: str, cwd: Path | None = None, timeout: int = 180) -> Tuple[int, str, str]:
	try:
		res = subprocess.run(cmd, cwd=str(cwd) if cwd else None, shell=True, capture_output=True, text=True, timeout=timeout)
		return res.returncode, res.stdout.strip(), res.stderr.strip()
	except Exception as e:
		return 127, "", str(e)

def _git_stats(root: Path) -> Dict[str, Any]:
	code, out, _ = _run_cmd("git rev-parse HEAD", cwd=root)
	commit = out.strip() if code == 0 else ""
	code, out, _ = _run_cmd("git diff --name-only HEAD~1..HEAD", cwd=root)
	files = [l.strip() for l in out.splitlines() if l.strip()] if code == 0 else []
	code, out, _ = _run_cmd("git diff --numstat HEAD~1..HEAD", cwd=root)
	added = 0
	deleted = 0
	if code == 0:
		for line in out.splitlines():


from __future__ import annotations
import logging

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

def _load_json(path: Path) -> Dict[str, Any]:
	if not path.exists():
		return {}
	try:
		return json.loads(path.read_text(encoding="utf-8"))
	except Exception:
		return {}

def _run_cmd(cmd: str, cwd: Path | None = None, timeout: int = 180) -> Tuple[int, str, str]:
	try:
		res = subprocess.run(cmd, cwd=str(cwd) if cwd else None, shell=True, capture_output=True, text=True, timeout=timeout)
		return res.returncode, res.stdout.strip(), res.stderr.strip()
	except Exception as e:
		return 127, "", str(e)

def _git_stats(root: Path) -> Dict[str, Any]:
	code, out, _ = _run_cmd("git rev-parse HEAD", cwd=root)
	commit = out.strip() if code == 0 else ""
	code, out, _ = _run_cmd("git diff --name-only HEAD~1..HEAD", cwd=root)
	files = [l.strip() for l in out.splitlines() if l.strip()] if code == 0 else []
	code, out, _ = _run_cmd("git diff --numstat HEAD~1..HEAD", cwd=root)
	added = 0
	deleted = 0
	if code == 0:
		for line in out.splitlines():
			parts = [p.strip() for p in line.split("\t")]
			if len(parts) >= 3:
				try:
					added += int(parts[0]) if parts[0].isdigit() else 0
					deleted += int(parts[1]) if parts[1].isdigit() else 0
				except Exception:
					pass
	return {"commit": commit, "files": files, "lines_added": added, "lines_deleted": deleted, "files_changed": len(files)}


from typing import Any, Dict, List, Tuple

def _load_json(path: Path) -> Dict[str, Any]:
	if not path.exists():
		return {}
	try:
		return json.loads(path.read_text(encoding="utf-8"))
	except Exception:
		return {}

def _run_cmd(cmd: str, cwd: Path | None = None, timeout: int = 180) -> Tuple[int, str, str]:
	try:
		res = subprocess.run(cmd, cwd=str(cwd) if cwd else None, shell=True, capture_output=True, text=True, timeout=timeout)
		return res.returncode, res.stdout.strip(), res.stderr.strip()
	except Exception as e:
		return 127, "", str(e)

def _git_stats(root: Path) -> Dict[str, Any]:
	code, out, _ = _run_cmd("git rev-parse HEAD", cwd=root)
	commit = out.strip() if code == 0 else ""
	code, out, _ = _run_cmd("git diff --name-only HEAD~1..HEAD", cwd=root)
	files = [l.strip() for l in out.splitlines() if l.strip()] if code == 0 else []
	code, out, _ = _run_cmd("git diff --numstat HEAD~1..HEAD", cwd=root)
	added = 0
	deleted = 0
	if code == 0:
		for line in out.splitlines():
			parts = [p.strip() for p in line.split("\t")]
			if len(parts) >= 3:
				try:
					added += int(parts[0]) if parts[0].isdigit() else 0
					deleted += int(parts[1]) if parts[1].isdigit() else 0
				except Exception:
					pass
	return {"commit": commit, "files": files, "lines_added": added, "lines_deleted": deleted, "files_changed": len(files)}

def _detect_and_run_checks(root: Path) -> Dict[str, Any]:
	from agi_dw.tools.repo_manifest import detect_test_cmds, detect_lint_cmds, detect_type_check_cmds  # type: ignore
	results: Dict[str, Any] = {"tests": {}, "types": {}, "lint": {}}
	# Tests
	test_cmds = detect_test_cmds(root)
	if test_cmds:
		code, out, err = _run_cmd(test_cmds[0], cwd=root)
		results["tests"] = {"cmd": test_cmds[0], "code": code}
	# Type checks
	type_cmds = detect_type_check_cmds(root)
	if type_cmds:
		code, out, err = _run_cmd(type_cmds[0], cwd=root)
		results["types"] = {"cmd": type_cmds[0], "code": code}
	# Lint
	lint_cmds = detect_lint_cmds(root)
	if lint_cmds:
		code, out, err = _run_cmd(lint_cmds[0], cwd=root)
		results["lint"] = {"cmd": lint_cmds[0], "code": code}


def _git_stats(root: Path) -> Dict[str, Any]:
	code, out, _ = _run_cmd("git rev-parse HEAD", cwd=root)
	commit = out.strip() if code == 0 else ""
	code, out, _ = _run_cmd("git diff --name-only HEAD~1..HEAD", cwd=root)
	files = [l.strip() for l in out.splitlines() if l.strip()] if code == 0 else []
	code, out, _ = _run_cmd("git diff --numstat HEAD~1..HEAD", cwd=root)
	added = 0
	deleted = 0
	if code == 0:
		for line in out.splitlines():
			parts = [p.strip() for p in line.split("\t")]
			if len(parts) >= 3:
				try:
					added += int(parts[0]) if parts[0].isdigit() else 0
					deleted += int(parts[1]) if parts[1].isdigit() else 0
				except Exception:
					pass
	return {"commit": commit, "files": files, "lines_added": added, "lines_deleted": deleted, "files_changed": len(files)}

def _detect_and_run_checks(root: Path) -> Dict[str, Any]:
	from agi_dw.tools.repo_manifest import detect_test_cmds, detect_lint_cmds, detect_type_check_cmds  # type: ignore
	results: Dict[str, Any] = {"tests": {}, "types": {}, "lint": {}}
	# Tests
	test_cmds = detect_test_cmds(root)
	if test_cmds:
		code, out, err = _run_cmd(test_cmds[0], cwd=root)
		results["tests"] = {"cmd": test_cmds[0], "code": code}
	# Type checks
	type_cmds = detect_type_check_cmds(root)
	if type_cmds:
		code, out, err = _run_cmd(type_cmds[0], cwd=root)
		results["types"] = {"cmd": type_cmds[0], "code": code}
	# Lint
	lint_cmds = detect_lint_cmds(root)
	if lint_cmds:
		code, out, err = _run_cmd(lint_cmds[0], cwd=root)
		results["lint"] = {"cmd": lint_cmds[0], "code": code}
	return results

def main() -> int:
	root = Path(__file__).resolve().parents[2]
	art_dir = root / "artifacts"
	art_dir.mkdir(parents=True, exist_ok=True)
	# Load pillar outputs
	base_tmp = root / "data" / "sandbox" / "tmp"
	risk = _load_json(base_tmp / "plan_risk.json")
	api = _load_json(base_tmp / "plan_api_diff.json")
	obs = _load_json(base_tmp / "obs_check.json")
	slo = _load_json(base_tmp / "slo_report.json")
	deps = _load_json(base_tmp / "deps_audit.json")
	deps_up = _load_json(base_tmp / "deps_upgrade.json")
	impact = _load_json(base_tmp / "plan_impact.json")
	# Git stats and best-effort checks
	gitinfo = _git_stats(root)
	checks = _detect_and_run_checks(root)
	# Compose evidence
	evidence = {


	from agi_dw.tools.repo_manifest import detect_test_cmds, detect_lint_cmds, detect_type_check_cmds  # type: ignore
	results: Dict[str, Any] = {"tests": {}, "types": {}, "lint": {}}
	# Tests
	test_cmds = detect_test_cmds(root)
	if test_cmds:
		code, out, err = _run_cmd(test_cmds[0], cwd=root)
		results["tests"] = {"cmd": test_cmds[0], "code": code}
	# Type checks
	type_cmds = detect_type_check_cmds(root)
	if type_cmds:
		code, out, err = _run_cmd(type_cmds[0], cwd=root)
		results["types"] = {"cmd": type_cmds[0], "code": code}
	# Lint
	lint_cmds = detect_lint_cmds(root)
	if lint_cmds:
		code, out, err = _run_cmd(lint_cmds[0], cwd=root)
		results["lint"] = {"cmd": lint_cmds[0], "code": code}
	return results

def main() -> int:
	root = Path(__file__).resolve().parents[2]
	art_dir = root / "artifacts"
	art_dir.mkdir(parents=True, exist_ok=True)
	# Load pillar outputs
	base_tmp = root / "data" / "sandbox" / "tmp"
	risk = _load_json(base_tmp / "plan_risk.json")
	api = _load_json(base_tmp / "plan_api_diff.json")
	obs = _load_json(base_tmp / "obs_check.json")
	slo = _load_json(base_tmp / "slo_report.json")
	deps = _load_json(base_tmp / "deps_audit.json")
	deps_up = _load_json(base_tmp / "deps_upgrade.json")
	impact = _load_json(base_tmp / "plan_impact.json")
	# Git stats and best-effort checks
	gitinfo = _git_stats(root)
	checks = _detect_and_run_checks(root)
	# Compose evidence
	evidence = {
		"commit": gitinfo.get("commit", ""),
		"risk": float((risk.get("score") if isinstance(risk.get("score"), (int, float)) else 0.0) or 0.0),
		"files_changed": int(gitinfo.get("files_changed", 0) or 0),
		"tests": {"code": checks.get("tests", {}).get("code", 127)},
		"types": {"code": checks.get("types", {}).get("code", 127)},
		"perf": {"p95_ms_delta": 0.0},
		"api": {"breaking": bool((api.get("api_diff", {}) or {}).get("breaking")) if isinstance(api.get("api_diff"), dict) else False, "diff_report": "data/sandbox/docs/reports/plan_api_diff.md"},
		"security": {"high": len([f for f in (deps.get("findings", []) or []) if str(f).lower().find("high") >= 0])},
		"docs": {"drift_fixed": True},
	}
	# Attach minimal gate statuses and risk fragments for reviewers
	evidence["gates"] = {
		"plan_risk_ok": bool(risk.get("ok", True) if isinstance(risk, dict) else True),
		"slo_ok": bool(slo.get("ok", True) if isinstance(slo, dict) else True),
		"deps_ok": bool(deps.get("ok", True) if isinstance(deps, dict) else True),
		"deps_upgrade_planned": len(deps_up.get("planned", []) if isinstance(deps_up, dict) else [])
	}
	# Include top-3 risk entries and top-3 impact modules for context
	try:
		if isinstance(risk, dict):
			rs = list(risk.get("risk", []) or [])[:3]
			evidence["risk_top"] = rs
	except Exception:
		pass
	try:
		im = (impact.get("impact", {}) or {}).get("top_modules", []) if isinstance(impact, dict) else []
		evidence["impact_top"] = im[:3]
	except Exception:
		pass
	(art_dir / "pr_evidence.json").write_text(json.dumps(evidence, ensure_ascii=False, indent=2), encoding="utf-8")
	# Markdown summary
	summary_lines: List[str] = []
	summary_lines.append(f"Commit: {evidence['commit']}")
	summary_lines.append(f"Risk: {evidence['risk']}")
	summary_lines.append(f"Files changed: {evidence['files_changed']}")
	summary_lines.append(f"Tests exit: {evidence['tests']['code']}")
	summary_lines.append(f"Types exit: {evidence['types']['code']}")
	summary_lines.append(f"API breaking: {evidence['api']['breaking']}")
	summary_lines.append(f"Security high: {evidence['security']['high']}")
	(art_dir / "summary.md").write_text("\n".join(summary_lines) + "\n", encoding="utf-8")
	print(json.dumps({"ok": True, "evidence": str(art_dir / "pr_evidence.json"), "summary": str(art_dir / "summary.md")}, ensure_ascii=False))
	return 0

if __name__ == "__main__":
	raise SystemExit(main())


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

import json
import subprocess
from pathlib import Path

def main() -> int:
    root = Path(__file__).resolve().parents[2]
    art = root / "artifacts"
    art.mkdir(parents=True, exist_ok=True)
    # Run SBOM and audit pillars
    try:
        subprocess.check_call(["python", str(root / "scripts" / "pillars" / "deps_sbom.py")])
    except Exception:
        pass
    try:
        subprocess.check_call(["python", str(root / "scripts" / "pillars" / "deps_audit.py")])
    except Exception:
        pass
    # Propose upgrades via deps_upgrade pillar
    try:
        subprocess.check_call(["python", str(root / "scripts" / "pillars" / "deps_upgrade.py")])
    except Exception:
        pass
    # Write a short summary
    out = art / "deps_upgrade_summary.md"
    lines = ["# Nightly Dependencies Report", "", "- SBOM, audit, and upgrade plan executed."]
    out.write_text("\n".join(lines) + "\n", encoding="utf-8")
    print(json.dumps({"ok": True, "summary": str(out)}))
    return 0

if __name__ == "__main__":
    raise SystemExit(main())


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

from pathlib import Path

def main() -> int:
	root = Path(__file__).resolve().parents[2]
	summary = root / "artifacts" / "summary.md"
	if summary.exists():
		print(summary.read_text(encoding="utf-8"))
		return 0
	print("No summary found at artifacts/summary.md")
	return 1

if __name__ == "__main__":
	raise SystemExit(main())