    def to_dict(self) -> Dict[str, Any]:
        return {
            "id": self.id,
            "task_id": self.task_id,
            "timestamp": self.timestamp.isoformat(),
            "name": self.name,
            "value": self.value,
            "metadata": self.meta_data  # Using metadata in API for consistency, but meta_data in DB
        }

class SystemMetric(Base):
    __tablename__ = "system_metrics"

    id = Column(Integer, primary_key=True, autoincrement=True)
    timestamp = Column(DateTime, nullable=False, default=datetime.utcnow)
    cpu_percent = Column(Float, nullable=True)
    memory_percent = Column(Float, nullable=True)
    active_tasks = Column(Integer, nullable=True)
    gpu_metrics = Column(JSON, nullable=True)

    def to_dict(self) -> Dict[str, Any]:
        return {
            "id": self.id,
            "timestamp": self.timestamp.isoformat(),
            "cpu_percent": self.cpu_percent,
            "memory_percent": self.memory_percent,
            "active_tasks": self.active_tasks,
            "gpu_metrics": self.gpu_metrics
        }

# Create database session
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

def get_db():
    """Get database session"""
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

def init_db():
    """Initialize database"""
    Base.metadata.create_all(bind=engine)

def get_task_by_id(db: SessionLocal, task_id: str) -> Optional[Task]:
    """Get task by ID"""
    return db.query(Task).filter(Task.id == task_id).first()


from __future__ import annotations
import logging

import subprocess
from pathlib import Path
from typing import Dict, Any, List, Optional

def _exec(cmd: List[str], cwd: str | Path) -> tuple[int, str]:
	p = subprocess.Popen(cmd, cwd=str(cwd), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
	out, _ = p.communicate()
	return p.returncode, out or ""

def run_flake8(repo_dir: str | Path, paths: Optional[List[str]] = None) -> Dict[str, Any]:
	repo = Path(repo_dir)
	cmd = ["flake8"] + (paths or ["."])
	code, out = _exec(cmd, repo)
	issues: List[Dict[str, Any]] = []
	for line in (out or "").splitlines():
		# format: path:line:col: code message
		try:
			path_part, rest = line.split(":", 1)
			line_part, rest2 = rest.split(":", 1)
			col_part, rest3 = rest2.split(":", 1)
			code_msg = rest3.strip().split(" ", 1)
			code = code_msg[0] if code_msg else ""
			msg = code_msg[1] if len(code_msg) > 1 else ""
			issues.append({"file": path_part.strip(), "line": int(line_part), "col": int(col_part), "code": code, "message": msg})
		except Exception:
			continue
	return {"tool": "flake8", "cmd": " ".join(cmd), "code": code, "issues": issues}

def run_mypy(repo_dir: str | Path, paths: Optional[List[str]] = None, strict: bool = False) -> Dict[str, Any]:
	repo = Path(repo_dir)
	cmd = ["mypy", "--show-error-codes"] + (["--strict"] if strict else []) + (paths or ["."])
	code, out = _exec(cmd, repo)
	errors: List[Dict[str, Any]] = []
	for line in (out or "").splitlines():
		# typical: path:line: col: error: message  [code]
		if ": error:" in line:
			try:
				p1, msg = line.split(": error:", 1)
				file_part, line_part, col_part = p1.split(":")[:3]
				msg = msg.strip()
				errors.append({"file": file_part.strip(), "line": int(line_part), "col": int(col_part), "message": msg})
			except Exception:
				continue
	return {"tool": "mypy", "cmd": " ".join(cmd), "code": code, "errors": errors}


from __future__ import annotations
import logging

import subprocess
from pathlib import Path
from typing import Dict, Any, List, Optional

def _exec(cmd: List[str], cwd: str | Path) -> tuple[int, str]:
	p = subprocess.Popen(cmd, cwd=str(cwd), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
	out, _ = p.communicate()
	return p.returncode, out or ""

def run_flake8(repo_dir: str | Path, paths: Optional[List[str]] = None) -> Dict[str, Any]:
	repo = Path(repo_dir)
	cmd = ["flake8"] + (paths or ["."])
	code, out = _exec(cmd, repo)
	issues: List[Dict[str, Any]] = []
	for line in (out or "").splitlines():
		# format: path:line:col: code message
		try:
			path_part, rest = line.split(":", 1)
			line_part, rest2 = rest.split(":", 1)
			col_part, rest3 = rest2.split(":", 1)
			code_msg = rest3.strip().split(" ", 1)
			code = code_msg[0] if code_msg else ""
			msg = code_msg[1] if len(code_msg) > 1 else ""
			issues.append({"file": path_part.strip(), "line": int(line_part), "col": int(col_part), "code": code, "message": msg})
		except Exception:
			continue
	return {"tool": "flake8", "cmd": " ".join(cmd), "code": code, "issues": issues}


def run_flake8(repo_dir: str | Path, paths: Optional[List[str]] = None) -> Dict[str, Any]:
	repo = Path(repo_dir)
	cmd = ["flake8"] + (paths or ["."])
	code, out = _exec(cmd, repo)
	issues: List[Dict[str, Any]] = []
	for line in (out or "").splitlines():
		# format: path:line:col: code message
		try:
			path_part, rest = line.split(":", 1)
			line_part, rest2 = rest.split(":", 1)
			col_part, rest3 = rest2.split(":", 1)
			code_msg = rest3.strip().split(" ", 1)
			code = code_msg[0] if code_msg else ""
			msg = code_msg[1] if len(code_msg) > 1 else ""
			issues.append({"file": path_part.strip(), "line": int(line_part), "col": int(col_part), "code": code, "message": msg})
		except Exception:
			continue
	return {"tool": "flake8", "cmd": " ".join(cmd), "code": code, "issues": issues}

def run_mypy(repo_dir: str | Path, paths: Optional[List[str]] = None, strict: bool = False) -> Dict[str, Any]:
	repo = Path(repo_dir)
	cmd = ["mypy", "--show-error-codes"] + (["--strict"] if strict else []) + (paths or ["."])
	code, out = _exec(cmd, repo)
	errors: List[Dict[str, Any]] = []
	for line in (out or "").splitlines():
		# typical: path:line: col: error: message  [code]
		if ": error:" in line:
			try:
				p1, msg = line.split(": error:", 1)
				file_part, line_part, col_part = p1.split(":")[:3]
				msg = msg.strip()
				errors.append({"file": file_part.strip(), "line": int(line_part), "col": int(col_part), "message": msg})
			except Exception:
				continue
	return {"tool": "mypy", "cmd": " ".join(cmd), "code": code, "errors": errors}


from __future__ import annotations
import logging

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

def run(cmd: List[str], cwd: Path) -> Dict[str, Any]:
	p = subprocess.run(cmd, cwd=str(cwd), capture_output=True, text=True)
	return {
		"cmd": " ".join(cmd),
		"rc": p.returncode,
		"out": p.stdout,
		"err": p.stderr,
	}

def main(argv: List[str] | None = None) -> int:
	parser = argparse.ArgumentParser(description="Collect static analysis summary")
	parser.add_argument("--root", default="/data/agiattempt/agi_dw")
	parser.add_argument("--out", default="/data/agiattempt/agi_dw/data/sandbox/tmp/static_summary.json")
	args = parser.parse_args(argv)
	root = Path(args.root)
	results: Dict[str, Any] = {"root": str(root), "tools": {}}
	if shutil.which("flake8"):
		results["tools"]["flake8"] = run(["flake8", "."], root)
	else:
		results["tools"]["flake8"] = {"missing": True}
	if shutil.which("mypy"):
		results["tools"]["mypy"] = run(["mypy", "--ignore-missing-imports", "."], root)
	else:
		results["tools"]["mypy"] = {"missing": True}
	out = Path(args.out)
	out.parent.mkdir(parents=True, exist_ok=True)
	out.write_text(json.dumps(results, ensure_ascii=False, indent=2), encoding="utf-8")
	print(str(out))
	return 0

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


run

from __future__ import annotations
import logging

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

def run(cmd: List[str], cwd: Path) -> Dict[str, Any]:
	p = subprocess.run(cmd, cwd=str(cwd), capture_output=True, text=True)
	return {
		"cmd": " ".join(cmd),
		"rc": p.returncode,
		"out": p.stdout,
		"err": p.stderr,
	}

def main(argv: List[str] | None = None) -> int:
	parser = argparse.ArgumentParser(description="Collect static analysis summary")
	parser.add_argument("--root", default="/data/agiattempt/agi_dw")
	parser.add_argument("--out", default="/data/agiattempt/agi_dw/data/sandbox/tmp/static_summary.json")
	args = parser.parse_args(argv)
	root = Path(args.root)
	results: Dict[str, Any] = {"root": str(root), "tools": {}}
	if shutil.which("flake8"):
		results["tools"]["flake8"] = run(["flake8", "."], root)
	else:
		results["tools"]["flake8"] = {"missing": True}
	if shutil.which("mypy"):
		results["tools"]["mypy"] = run(["mypy", "--ignore-missing-imports", "."], root)
	else:
		results["tools"]["mypy"] = {"missing": True}
	out = Path(args.out)
	out.parent.mkdir(parents=True, exist_ok=True)
	out.write_text(json.dumps(results, ensure_ascii=False, indent=2), encoding="utf-8")


main

import logging

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

def run(cmd: List[str], cwd: Path) -> Dict[str, Any]:
	p = subprocess.run(cmd, cwd=str(cwd), capture_output=True, text=True)
	return {
		"cmd": " ".join(cmd),
		"rc": p.returncode,
		"out": p.stdout,
		"err": p.stderr,
	}

def main(argv: List[str] | None = None) -> int:
	parser = argparse.ArgumentParser(description="Collect static analysis summary")
	parser.add_argument("--root", default="/data/agiattempt/agi_dw")
	parser.add_argument("--out", default="/data/agiattempt/agi_dw/data/sandbox/tmp/static_summary.json")
	args = parser.parse_args(argv)
	root = Path(args.root)
	results: Dict[str, Any] = {"root": str(root), "tools": {}}
	if shutil.which("flake8"):
		results["tools"]["flake8"] = run(["flake8", "."], root)
	else:
		results["tools"]["flake8"] = {"missing": True}
	if shutil.which("mypy"):
		results["tools"]["mypy"] = run(["mypy", "--ignore-missing-imports", "."], root)
	else:
		results["tools"]["mypy"] = {"missing": True}
	out = Path(args.out)
	out.parent.mkdir(parents=True, exist_ok=True)
	out.write_text(json.dumps(results, ensure_ascii=False, indent=2), encoding="utf-8")
	print(str(out))
	return 0

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


from __future__ import annotations
import logging

import argparse
import ast
import json
from pathlib import Path
from typing import Any, Dict, List, Tuple

def scan_py(path: Path) -> Dict[str, Any]:
	text = path.read_text(encoding="utf-8", errors="ignore")
	try:
		tree = ast.parse(text, filename=str(path))
		defs: List[Dict[str, Any]] = []
		for node in ast.walk(tree):
			if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
				defs.append({
					"name": node.name,
					"kind": node.__class__.__name__,
					"lineno": getattr(node, "lineno", None),
				})
		return {"file": str(path), "defs": defs}
	except SyntaxError as e:
		# Record parse errors but do not fail the entire index build
		return {"file": str(path), "error": f"SyntaxError: {e}"}

def build_index(root: Path) -> Dict[str, Any]:
	entries: List[Dict[str, Any]] = []
	for p in root.rglob("*.py"):
		if "/.venv/" in str(p):
			continue
		entries.append(scan_py(p))
	# Attach richer code graph for downstream tools
	graph = index_python_repo(root)
	return {"root": str(root), "entries": entries, "graph": graph}

def _extract_imports_and_calls(tree: ast.AST) -> Tuple[List[str], List[str]]:
    imports: List[str] = []
    calls: List[str] = []
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for n in node.names:
                imports.append(n.name)
        elif isinstance(node, ast.ImportFrom):
            mod = node.module or ""
            for n in node.names:
                name = f"{mod}.{n.name}" if mod else n.name
                imports.append(name)
        elif isinstance(node, ast.Call):
            fn = node.func
            name = None
            if isinstance(fn, ast.Name):
                name = fn.id
            elif isinstance(fn, ast.Attribute):
                # attempt to reconstruct dotted attr
                parts: List[str] = []
                cur: Any = fn
                while isinstance(cur, ast.Attribute):
                    parts.append(cur.attr)
                    cur = cur.value
                if isinstance(cur, ast.Name):
                    parts.append(cur.id)
                parts.reverse()
                name = ".".join(parts)
            if name:
                calls.append(name)
    return imports, calls

def index_python_repo(root: str | Path) -> Dict[str, Dict[str, List[Dict[str, Any]]]]:
	"""Build a lightweight symbol index compatible with planner expectations.

	Returns a dict with keys: functions, classes, calls; each maps absolute file paths
	to lists of objects (at minimum, with a name field).
	"""
	root_path = Path(root).resolve()
	functions: Dict[str, List[Dict[str, Any]]] = {}
	classes: Dict[str, List[Dict[str, Any]]] = {}
	calls: Dict[str, List[Dict[str, Any]]] = {}
	imports_map: Dict[str, List[str]] = {}
	for p in root_path.rglob("*.py"):
		if "/.venv/" in str(p):
			continue
		try:
			text = p.read_text(encoding="utf-8", errors="ignore")
			try:
				tree = ast.parse(text, filename=str(p))
			except SyntaxError:
				continue
			f_list: List[Dict[str, Any]] = []
			c_list: List[Dict[str, Any]] = []
			for node in ast.walk(tree):
				if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
					f_list.append({"name": node.name, "lineno": getattr(node, "lineno", None)})
				elif isinstance(node, ast.ClassDef):
					c_list.append({"name": node.name, "lineno": getattr(node, "lineno", None)})
			abs_path = str(p.resolve())
			if f_list:
				functions[abs_path] = f_list
			if c_list:
				classes[abs_path] = c_list
			imps, call_names = _extract_imports_and_calls(tree)
			if call_names:
				calls[abs_path] = [{"name": n} for n in call_names]
			else:
				calls.setdefault(abs_path, [])
			if imps:
				imports_map[abs_path] = imps
		except Exception:
			continue
	return {"functions": functions, "classes": classes, "calls": calls, "imports": imports_map}

def main(argv: List[str] | None = None) -> int:
	parser = argparse.ArgumentParser(description="Build a simple code index")
	parser.add_argument("--root", default="/data/agiattempt/agi_dw", help="Root to index")
	parser.add_argument("--out", default="/data/agiattempt/agi_dw/data/sandbox/tmp/index.json", help="Where to write index JSON")
	args = parser.parse_args(argv)
	idx = build_index(Path(args.root))
	out = Path(args.out)
	out.parent.mkdir(parents=True, exist_ok=True)
	out.write_text(json.dumps(idx, ensure_ascii=False, indent=2), encoding="utf-8")
	print(str(out))
	return 0

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


from __future__ import annotations
import logging

import argparse
import ast
import json
from pathlib import Path
from typing import Any, Dict, List, Tuple

def scan_py(path: Path) -> Dict[str, Any]:
	text = path.read_text(encoding="utf-8", errors="ignore")
	try:
		tree = ast.parse(text, filename=str(path))
		defs: List[Dict[str, Any]] = []
		for node in ast.walk(tree):
			if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
				defs.append({
					"name": node.name,
					"kind": node.__class__.__name__,
					"lineno": getattr(node, "lineno", None),
				})
		return {"file": str(path), "defs": defs}
	except SyntaxError as e:
		# Record parse errors but do not fail the entire index build
		return {"file": str(path), "error": f"SyntaxError: {e}"}

def build_index(root: Path) -> Dict[str, Any]:
	entries: List[Dict[str, Any]] = []
	for p in root.rglob("*.py"):
		if "/.venv/" in str(p):
			continue
		entries.append(scan_py(p))
	# Attach richer code graph for downstream tools
	graph = index_python_repo(root)
	return {"root": str(root), "entries": entries, "graph": graph}

def _extract_imports_and_calls(tree: ast.AST) -> Tuple[List[str], List[str]]:
    imports: List[str] = []
    calls: List[str] = []
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for n in node.names:
                imports.append(n.name)


def scan_py(path: Path) -> Dict[str, Any]:
	text = path.read_text(encoding="utf-8", errors="ignore")
	try:
		tree = ast.parse(text, filename=str(path))
		defs: List[Dict[str, Any]] = []
		for node in ast.walk(tree):
			if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
				defs.append({
					"name": node.name,
					"kind": node.__class__.__name__,
					"lineno": getattr(node, "lineno", None),
				})
		return {"file": str(path), "defs": defs}
	except SyntaxError as e:
		# Record parse errors but do not fail the entire index build
		return {"file": str(path), "error": f"SyntaxError: {e}"}

def build_index(root: Path) -> Dict[str, Any]:
	entries: List[Dict[str, Any]] = []
	for p in root.rglob("*.py"):
		if "/.venv/" in str(p):
			continue
		entries.append(scan_py(p))
	# Attach richer code graph for downstream tools
	graph = index_python_repo(root)
	return {"root": str(root), "entries": entries, "graph": graph}

def _extract_imports_and_calls(tree: ast.AST) -> Tuple[List[str], List[str]]:
    imports: List[str] = []
    calls: List[str] = []
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for n in node.names:
                imports.append(n.name)
        elif isinstance(node, ast.ImportFrom):
            mod = node.module or ""
            for n in node.names:
                name = f"{mod}.{n.name}" if mod else n.name
                imports.append(name)
        elif isinstance(node, ast.Call):
            fn = node.func
            name = None
            if isinstance(fn, ast.Name):
                name = fn.id
            elif isinstance(fn, ast.Attribute):


					"kind": node.__class__.__name__,
					"lineno": getattr(node, "lineno", None),
				})
		return {"file": str(path), "defs": defs}
	except SyntaxError as e:
		# Record parse errors but do not fail the entire index build
		return {"file": str(path), "error": f"SyntaxError: {e}"}

def build_index(root: Path) -> Dict[str, Any]:
	entries: List[Dict[str, Any]] = []
	for p in root.rglob("*.py"):
		if "/.venv/" in str(p):
			continue
		entries.append(scan_py(p))
	# Attach richer code graph for downstream tools
	graph = index_python_repo(root)
	return {"root": str(root), "entries": entries, "graph": graph}

def _extract_imports_and_calls(tree: ast.AST) -> Tuple[List[str], List[str]]:
    imports: List[str] = []
    calls: List[str] = []
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for n in node.names:
                imports.append(n.name)
        elif isinstance(node, ast.ImportFrom):
            mod = node.module or ""
            for n in node.names:
                name = f"{mod}.{n.name}" if mod else n.name
                imports.append(name)
        elif isinstance(node, ast.Call):
            fn = node.func
            name = None
            if isinstance(fn, ast.Name):
                name = fn.id
            elif isinstance(fn, ast.Attribute):
                # attempt to reconstruct dotted attr
                parts: List[str] = []
                cur: Any = fn
                while isinstance(cur, ast.Attribute):
                    parts.append(cur.attr)
                    cur = cur.value
                if isinstance(cur, ast.Name):
                    parts.append(cur.id)
                parts.reverse()
                name = ".".join(parts)
            if name:
                calls.append(name)
    return imports, calls

def index_python_repo(root: str | Path) -> Dict[str, Dict[str, List[Dict[str, Any]]]]:
	"""Build a lightweight symbol index compatible with planner expectations.

	Returns a dict with keys: functions, classes, calls; each maps absolute file paths
	to lists of objects (at minimum, with a name field).
	"""
	root_path = Path(root).resolve()
	functions: Dict[str, List[Dict[str, Any]]] = {}
	classes: Dict[str, List[Dict[str, Any]]] = {}
	calls: Dict[str, List[Dict[str, Any]]] = {}
	imports_map: Dict[str, List[str]] = {}
	for p in root_path.rglob("*.py"):
		if "/.venv/" in str(p):
			continue
		try:
			text = p.read_text(encoding="utf-8", errors="ignore")
			try:
				tree = ast.parse(text, filename=str(p))


            fn = node.func
            name = None
            if isinstance(fn, ast.Name):
                name = fn.id
            elif isinstance(fn, ast.Attribute):
                # attempt to reconstruct dotted attr
                parts: List[str] = []
                cur: Any = fn
                while isinstance(cur, ast.Attribute):
                    parts.append(cur.attr)
                    cur = cur.value
                if isinstance(cur, ast.Name):
                    parts.append(cur.id)
                parts.reverse()
                name = ".".join(parts)
            if name:
                calls.append(name)
    return imports, calls

def index_python_repo(root: str | Path) -> Dict[str, Dict[str, List[Dict[str, Any]]]]:
	"""Build a lightweight symbol index compatible with planner expectations.

	Returns a dict with keys: functions, classes, calls; each maps absolute file paths
	to lists of objects (at minimum, with a name field).
	"""
	root_path = Path(root).resolve()
	functions: Dict[str, List[Dict[str, Any]]] = {}
	classes: Dict[str, List[Dict[str, Any]]] = {}
	calls: Dict[str, List[Dict[str, Any]]] = {}
	imports_map: Dict[str, List[str]] = {}
	for p in root_path.rglob("*.py"):
		if "/.venv/" in str(p):
			continue
		try:
			text = p.read_text(encoding="utf-8", errors="ignore")
			try:
				tree = ast.parse(text, filename=str(p))
			except SyntaxError:
				continue
			f_list: List[Dict[str, Any]] = []
			c_list: List[Dict[str, Any]] = []
			for node in ast.walk(tree):
				if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
					f_list.append({"name": node.name, "lineno": getattr(node, "lineno", None)})
				elif isinstance(node, ast.ClassDef):
					c_list.append({"name": node.name, "lineno": getattr(node, "lineno", None)})
			abs_path = str(p.resolve())
			if f_list:
				functions[abs_path] = f_list
			if c_list:
				classes[abs_path] = c_list
			imps, call_names = _extract_imports_and_calls(tree)
			if call_names:
				calls[abs_path] = [{"name": n} for n in call_names]
			else:
				calls.setdefault(abs_path, [])
			if imps:
				imports_map[abs_path] = imps
		except Exception:
			continue
	return {"functions": functions, "classes": classes, "calls": calls, "imports": imports_map}

def main(argv: List[str] | None = None) -> int:
	parser = argparse.ArgumentParser(description="Build a simple code index")
	parser.add_argument("--root", default="/data/agiattempt/agi_dw", help="Root to index")
	parser.add_argument("--out", default="/data/agiattempt/agi_dw/data/sandbox/tmp/index.json", help="Where to write index JSON")
	args = parser.parse_args(argv)
	idx = build_index(Path(args.root))
	out = Path(args.out)
	out.parent.mkdir(parents=True, exist_ok=True)
	out.write_text(json.dumps(idx, ensure_ascii=False, indent=2), encoding="utf-8")
	print(str(out))
	return 0

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


					f_list.append({"name": node.name, "lineno": getattr(node, "lineno", None)})
				elif isinstance(node, ast.ClassDef):
					c_list.append({"name": node.name, "lineno": getattr(node, "lineno", None)})
			abs_path = str(p.resolve())
			if f_list:
				functions[abs_path] = f_list
			if c_list:
				classes[abs_path] = c_list
			imps, call_names = _extract_imports_and_calls(tree)
			if call_names:
				calls[abs_path] = [{"name": n} for n in call_names]
			else:
				calls.setdefault(abs_path, [])
			if imps:
				imports_map[abs_path] = imps
		except Exception:
			continue
	return {"functions": functions, "classes": classes, "calls": calls, "imports": imports_map}

def main(argv: List[str] | None = None) -> int:
	parser = argparse.ArgumentParser(description="Build a simple code index")
	parser.add_argument("--root", default="/data/agiattempt/agi_dw", help="Root to index")
	parser.add_argument("--out", default="/data/agiattempt/agi_dw/data/sandbox/tmp/index.json", help="Where to write index JSON")
	args = parser.parse_args(argv)
	idx = build_index(Path(args.root))
	out = Path(args.out)
	out.parent.mkdir(parents=True, exist_ok=True)
	out.write_text(json.dumps(idx, ensure_ascii=False, indent=2), encoding="utf-8")
	print(str(out))
	return 0

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


from __future__ import annotations
import logging

import subprocess
from pathlib import Path
from typing import Dict, List, Optional

class LinterTool:
	"""Run flake8 and mypy if available; return structured results.

	Skips gracefully if tools are not installed.
	"""

	def __init__(self, cwd: str) -> None:
		self.cwd = Path(cwd)

	def _run(self, cmd: List[str], timeout: int = 300) -> subprocess.CompletedProcess:
		return subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout)

	def run_flake8(self, args: Optional[List[str]] = None, timeout: int = 300) -> Dict:
		try:
			cp = self._run(["flake8", *(args or ["."])], timeout=timeout)
		except FileNotFoundError:
			return {"available": False, "tool": "flake8", "ok": True, "issues": []}
		issues: List[Dict[str, str]] = []
		for line in (cp.stdout + cp.stderr).splitlines():
			# Format: path:line:col: code message
			parts = line.split(":", 3)
			if len(parts) == 4:
				issues.append({
					"path": parts[0].strip(),
					"line": parts[1].strip(),
					"col": parts[2].strip(),
					"msg": parts[3].strip(),
				})
		return {"available": True, "tool": "flake8", "ok": cp.returncode == 0, "issues": issues}

	def run_mypy(self, args: Optional[List[str]] = None, timeout: int = 600) -> Dict:
		try:
			cp = self._run(["mypy", *(args or ["."])], timeout=timeout)
		except FileNotFoundError:
			return {"available": False, "tool": "mypy", "ok": True, "issues": []}
		issues: List[Dict[str, str]] = []
		for line in (cp.stdout + cp.stderr).splitlines():
			# Format: path:line: col: message  (varies by config)
			if ":" in line:
				parts = line.split(":", 3)
				if len(parts) >= 3:
					path = parts[0].strip()
					line_no = parts[1].strip()
					msg = parts[-1].strip()
					issues.append({"path": path, "line": line_no, "msg": msg})
		return {"available": True, "tool": "mypy", "ok": cp.returncode == 0, "issues": issues}


from __future__ import annotations
import logging

import subprocess
from pathlib import Path
from typing import Dict, List, Optional

class LinterTool:
	"""Run flake8 and mypy if available; return structured results.

	Skips gracefully if tools are not installed.
	"""

	def __init__(self, cwd: str) -> None:
		self.cwd = Path(cwd)

	def _run(self, cmd: List[str], timeout: int = 300) -> subprocess.CompletedProcess:
		return subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout)

	def run_flake8(self, args: Optional[List[str]] = None, timeout: int = 300) -> Dict:
		try:
			cp = self._run(["flake8", *(args or ["."])], timeout=timeout)
		except FileNotFoundError:
			return {"available": False, "tool": "flake8", "ok": True, "issues": []}
		issues: List[Dict[str, str]] = []
		for line in (cp.stdout + cp.stderr).splitlines():
			# Format: path:line:col: code message
			parts = line.split(":", 3)
			if len(parts) == 4:
				issues.append({
					"path": parts[0].strip(),
					"line": parts[1].strip(),
					"col": parts[2].strip(),
					"msg": parts[3].strip(),
				})


from __future__ import annotations
import logging

import subprocess
from pathlib import Path
from typing import Dict, List, Optional

class LinterTool:
	"""Run flake8 and mypy if available; return structured results.

	Skips gracefully if tools are not installed.
	"""

	def __init__(self, cwd: str) -> None:
		self.cwd = Path(cwd)

	def _run(self, cmd: List[str], timeout: int = 300) -> subprocess.CompletedProcess:
		return subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout)

	def run_flake8(self, args: Optional[List[str]] = None, timeout: int = 300) -> Dict:
		try:
			cp = self._run(["flake8", *(args or ["."])], timeout=timeout)
		except FileNotFoundError:
			return {"available": False, "tool": "flake8", "ok": True, "issues": []}
		issues: List[Dict[str, str]] = []
		for line in (cp.stdout + cp.stderr).splitlines():
			# Format: path:line:col: code message
			parts = line.split(":", 3)
			if len(parts) == 4:
				issues.append({
					"path": parts[0].strip(),
					"line": parts[1].strip(),
					"col": parts[2].strip(),
					"msg": parts[3].strip(),
				})
		return {"available": True, "tool": "flake8", "ok": cp.returncode == 0, "issues": issues}

	def run_mypy(self, args: Optional[List[str]] = None, timeout: int = 600) -> Dict:


		return subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout)

	def run_flake8(self, args: Optional[List[str]] = None, timeout: int = 300) -> Dict:
		try:
			cp = self._run(["flake8", *(args or ["."])], timeout=timeout)
		except FileNotFoundError:
			return {"available": False, "tool": "flake8", "ok": True, "issues": []}
		issues: List[Dict[str, str]] = []
		for line in (cp.stdout + cp.stderr).splitlines():
			# Format: path:line:col: code message
			parts = line.split(":", 3)
			if len(parts) == 4:
				issues.append({
					"path": parts[0].strip(),
					"line": parts[1].strip(),
					"col": parts[2].strip(),
					"msg": parts[3].strip(),
				})
		return {"available": True, "tool": "flake8", "ok": cp.returncode == 0, "issues": issues}

	def run_mypy(self, args: Optional[List[str]] = None, timeout: int = 600) -> Dict:
		try:
			cp = self._run(["mypy", *(args or ["."])], timeout=timeout)
		except FileNotFoundError:
			return {"available": False, "tool": "mypy", "ok": True, "issues": []}
		issues: List[Dict[str, str]] = []
		for line in (cp.stdout + cp.stderr).splitlines():
			# Format: path:line: col: message  (varies by config)
			if ":" in line:
				parts = line.split(":", 3)
				if len(parts) >= 3:
					path = parts[0].strip()
					line_no = parts[1].strip()
					msg = parts[-1].strip()
					issues.append({"path": path, "line": line_no, "msg": msg})
		return {"available": True, "tool": "mypy", "ok": cp.returncode == 0, "issues": issues}


from __future__ import annotations
import logging

import re
from typing import Iterable

DEFAULT_PATTERNS: list[tuple[re.Pattern[str], str]] = [
	# API keys / tokens
	(re.compile(r"(?i)(api[_-]?key|token|secret)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{16,}['\"]?"), r"\1: [REDACTED]"),
	# JWTs (very rough)
	(re.compile(r"eyJ[0-9A-Za-z_-]+\.[0-9A-Za-z_-]+\.[0-9A-Za-z_-]+"), "[REDACTED_JWT]"),
	# Email addresses
	(re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}"), "[REDACTED_EMAIL]"),
	# IPv4
	(re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b"), "[REDACTED_IP]"),
]

def redact_text(text: str, patterns: Iterable[tuple[re.Pattern[str], str]] | None = None) -> str:
	out = text or ""
	for pat, repl in (list(patterns) if patterns is not None else DEFAULT_PATTERNS):
		try:
			out = pat.sub(repl, out)
		except Exception:
			continue
	return out


from __future__ import annotations
import logging

import re
from typing import Dict, Any, List

def classify_failures(stdout: str, stderr: str) -> Dict[str, Any]:
	text = (stdout or "") + "\n" + (stderr or "")
	categories: List[str] = []
	advice: List[str] = []

	def hit(rx: str) -> bool:
		return re.search(rx, text, re.IGNORECASE | re.MULTILINE) is not None

	# Import & module errors
	if hit(r"ModuleNotFoundError|No module named|ImportError: cannot import name"):
		categories.append("import_error")
		advice.append("Check PYTHONPATH and package installs; consider rewrite-imports to fix relative imports.")
	# Device / CUDA
	if hit(r"CUDA out of memory|device-side assert|RuntimeError: CUDA|torch\.cuda"):
		categories.append("device_error")
		advice.append("Switch to CPU or smaller batch; guard device availability in tests.")
	# Test assertion
	if hit(r"AssertionError|assert .* == .*"):
		categories.append("test_assertion")
		advice.append("Verify expected vs actual; minimal fix may be adjusting off-by-one or output normalization.")
	# Shape mismatches
	if hit(r"ValueError: .*shape|mismatch|expected .* got .*shape"):
		categories.append("shape_mismatch")
		advice.append("Validate tensor/array shapes; add reshape/permute or adjust model config.")
	# Type errors / argument mismatch
	if hit(r"TypeError: .*expected|got|missing .* positional argument|argument .* of type|Unsupported operand type"):
		categories.append("type_error")
		advice.append("Review function signatures; ensure correct argument names and defaults.")
	# Common flake8 codes (subset)
	if hit(r"F401 .*imported but unused|F841 local variable .* is assigned to but never used"):
		categories.append("flake8_unused")
		advice.append("Remove unused imports/variables or prefix with _ if intentional.")
	if hit(r"W291 trailing whitespace|W293 blank line contains whitespace|W292 no newline at end of file"):
		categories.append("flake8_whitespace")
		advice.append("Strip trailing whitespace and ensure newline at EOF.")
	if hit(r"E231 missing whitespace after ','|E302 expected 2 blank lines|E303 too many blank lines"):
		categories.append("flake8_format")
		advice.append("Normalize spacing/blank lines to comply with PEP8.")
	# Common mypy patterns
	if hit(r"error: Argument .* has incompatible type|error: Incompatible types in assignment|error: Missing positional argument"):
		categories.append("mypy_type")
		advice.append("Adjust type annotations or casts; ensure call signatures match.")
	if hit(r"error: Name .* is not defined"):
		categories.append("mypy_name")
		advice.append("Import or define missing names; check module paths.")
	if not categories:
		categories.append("unknown")
		advice.append("Inspect failing test output and logs for details.")
	return {"categories": categories, "advice": advice[:3]}


hit

from __future__ import annotations
import logging

import re
from typing import Dict, Any, List

def classify_failures(stdout: str, stderr: str) -> Dict[str, Any]:
	text = (stdout or "") + "\n" + (stderr or "")
	categories: List[str] = []
	advice: List[str] = []

	def hit(rx: str) -> bool:
		return re.search(rx, text, re.IGNORECASE | re.MULTILINE) is not None

	# Import & module errors
	if hit(r"ModuleNotFoundError|No module named|ImportError: cannot import name"):
		categories.append("import_error")
		advice.append("Check PYTHONPATH and package installs; consider rewrite-imports to fix relative imports.")
	# Device / CUDA
	if hit(r"CUDA out of memory|device-side assert|RuntimeError: CUDA|torch\.cuda"):
		categories.append("device_error")
		advice.append("Switch to CPU or smaller batch; guard device availability in tests.")
	# Test assertion
	if hit(r"AssertionError|assert .* == .*"):
		categories.append("test_assertion")
		advice.append("Verify expected vs actual; minimal fix may be adjusting off-by-one or output normalization.")
	# Shape mismatches
	if hit(r"ValueError: .*shape|mismatch|expected .* got .*shape"):
		categories.append("shape_mismatch")
		advice.append("Validate tensor/array shapes; add reshape/permute or adjust model config.")
	# Type errors / argument mismatch
	if hit(r"TypeError: .*expected|got|missing .* positional argument|argument .* of type|Unsupported operand type"):
		categories.append("type_error")


from __future__ import annotations
import logging

from pathlib import Path
from typing import List, Tuple, Set
import ast

def validate_unified_diff_schema(diff_text: str) -> Tuple[bool, str]:
	try:
		lines = diff_text.splitlines()
		if not any(ln.startswith("diff --git ") for ln in lines):
			return False, "missing diff header"
		if not any(ln.startswith("--- a/") for ln in lines) or not any(ln.startswith("+++ b/") for ln in lines):
			return False, "missing file headers"
		if not any(ln.startswith("@@ ") for ln in lines):
			return False, "missing hunk headers"
		# Block binary diffs and mode/rename flags
		for ln in lines:
			l = ln.strip().lower()
			if l.startswith("binary files ") or l.startswith("gitattributes"):
				return False, "binary diff not allowed"
			if l.startswith("new mode ") or l.startswith("old mode ") or l.startswith("rename from ") or l.startswith("rename to "):
				return False, "mode/rename not allowed"
		return True, "ok"
	except Exception as e:
		return False, str(e)

def check_python_syntax(paths: List[str]) -> Tuple[bool, List[str]]:
	bad: List[str] = []
	for p in paths:
		try:
			fp = Path(p)
			if fp.suffix != ".py" or (not fp.exists()):
				continue
			code = fp.read_text(encoding="utf-8")
			ast.parse(code)
		except Exception:
			bad.append(str(p))
	return (len(bad) == 0), bad

def count_files_in_diff(diff_text: str) -> Set[str]:
	files: Set[str] = set()
	try:
		for ln in diff_text.splitlines():
			if ln.startswith("+++ b/"):
				files.add(ln[6:].strip())
	except Exception:
		return files
	return files


from __future__ import annotations
import logging

from pathlib import Path
from typing import List, Tuple, Set
import ast

def validate_unified_diff_schema(diff_text: str) -> Tuple[bool, str]:
	try:
		lines = diff_text.splitlines()
		if not any(ln.startswith("diff --git ") for ln in lines):
			return False, "missing diff header"
		if not any(ln.startswith("--- a/") for ln in lines) or not any(ln.startswith("+++ b/") for ln in lines):
			return False, "missing file headers"
		if not any(ln.startswith("@@ ") for ln in lines):
			return False, "missing hunk headers"
		# Block binary diffs and mode/rename flags
		for ln in lines:
			l = ln.strip().lower()
			if l.startswith("binary files ") or l.startswith("gitattributes"):
				return False, "binary diff not allowed"
			if l.startswith("new mode ") or l.startswith("old mode ") or l.startswith("rename from ") or l.startswith("rename to "):
				return False, "mode/rename not allowed"
		return True, "ok"
	except Exception as e:
		return False, str(e)

def check_python_syntax(paths: List[str]) -> Tuple[bool, List[str]]:
	bad: List[str] = []
	for p in paths:
		try:
			fp = Path(p)
			if fp.suffix != ".py" or (not fp.exists()):
				continue
			code = fp.read_text(encoding="utf-8")
			ast.parse(code)
		except Exception:
			bad.append(str(p))
	return (len(bad) == 0), bad

def count_files_in_diff(diff_text: str) -> Set[str]:
	files: Set[str] = set()
	try:
		for ln in diff_text.splitlines():


	try:
		lines = diff_text.splitlines()
		if not any(ln.startswith("diff --git ") for ln in lines):
			return False, "missing diff header"
		if not any(ln.startswith("--- a/") for ln in lines) or not any(ln.startswith("+++ b/") for ln in lines):
			return False, "missing file headers"
		if not any(ln.startswith("@@ ") for ln in lines):
			return False, "missing hunk headers"
		# Block binary diffs and mode/rename flags
		for ln in lines:
			l = ln.strip().lower()
			if l.startswith("binary files ") or l.startswith("gitattributes"):
				return False, "binary diff not allowed"
			if l.startswith("new mode ") or l.startswith("old mode ") or l.startswith("rename from ") or l.startswith("rename to "):
				return False, "mode/rename not allowed"
		return True, "ok"
	except Exception as e:
		return False, str(e)

def check_python_syntax(paths: List[str]) -> Tuple[bool, List[str]]:
	bad: List[str] = []
	for p in paths:
		try:
			fp = Path(p)
			if fp.suffix != ".py" or (not fp.exists()):
				continue
			code = fp.read_text(encoding="utf-8")
			ast.parse(code)
		except Exception:
			bad.append(str(p))
	return (len(bad) == 0), bad

def count_files_in_diff(diff_text: str) -> Set[str]:
	files: Set[str] = set()
	try:
		for ln in diff_text.splitlines():
			if ln.startswith("+++ b/"):
				files.add(ln[6:].strip())
	except Exception:
		return files
	return files


				return False, "mode/rename not allowed"
		return True, "ok"
	except Exception as e:
		return False, str(e)

def check_python_syntax(paths: List[str]) -> Tuple[bool, List[str]]:
	bad: List[str] = []
	for p in paths:
		try:
			fp = Path(p)
			if fp.suffix != ".py" or (not fp.exists()):
				continue
			code = fp.read_text(encoding="utf-8")
			ast.parse(code)
		except Exception:
			bad.append(str(p))
	return (len(bad) == 0), bad

def count_files_in_diff(diff_text: str) -> Set[str]:
	files: Set[str] = set()
	try:
		for ln in diff_text.splitlines():
			if ln.startswith("+++ b/"):
				files.add(ln[6:].strip())
	except Exception:
		return files
	return files


from __future__ import annotations
import logging

from pathlib import Path
from typing import Dict, Any, List, Tuple
import os
import re
import fnmatch

def load_env_limits(strict_default: bool = True) -> Dict[str, int | bool | str]:
	strict = os.environ.get("AGI_PATCH_STRICT", "1" if strict_default else "0").strip() not in ("0", "false", "False")
	max_files = int(os.environ.get("AGI_PATCH_MAX_FILES", "10" if strict else "20") or ("10" if strict else "20"))
	max_added = int(os.environ.get("AGI_PATCH_MAX_ADDED", "400" if strict else "1000") or ("400" if strict else "1000"))
	max_deleted = int(os.environ.get("AGI_PATCH_MAX_DELETED", "200" if strict else "500") or ("200" if strict else "500"))
	allow_raw = os.environ.get("AGI_PATCH_ALLOW", "").strip()
	block_raw = os.environ.get("AGI_PATCH_BLOCK", "").strip()
	allow_patterns = [p.strip() for p in allow_raw.split(",") if p.strip()]
	block_patterns = [p.strip() for p in block_raw.split(",") if p.strip()]
	allow_mode = os.environ.get("AGI_PATCH_ALLOW_FILE_MODES", "0").strip() in ("1", "true", "True")
	allow_rename = os.environ.get("AGI_PATCH_ALLOW_RENAMES", "0").strip() in ("1", "true", "True")
	return {
		"strict": strict,
		"max_files": max_files,
		"max_added": max_added,
		"max_deleted": max_deleted,
		"allow_patterns": allow_patterns,
		"block_patterns": block_patterns,
		"allow_mode": allow_mode,
		"allow_rename": allow_rename,
	}

def validate_unified_diff(text: str, limits: Dict[str, Any]) -> Tuple[bool, str | None, Dict[str, Any]]:
	# Binary patch
	if "GIT binary patch" in text:
		return False, "binary_patch_not_allowed", {}
	allow_mode = bool(limits.get("allow_mode"))
	allow_rename = bool(limits.get("allow_rename"))
	if not allow_mode:
		for line in text.splitlines():
			l = line.strip().lower()
			if l.startswith("new file mode ") or l.startswith("deleted file mode ") or l.startswith("old mode ") or l.startswith("new mode "):
				return False, "file_mode_change_not_allowed", {}
	if not allow_rename:
		for line in text.splitlines():
			l = line.strip().lower()
			if l.startswith("rename from ") or l.startswith("rename to "):
				return False, "rename_not_allowed", {}
	# Gather files from headers
	files: List[str] = []
	for line in text.splitlines():
		if line.startswith("+++ ") or line.startswith("--- "):
			m = re.match(r"[+-]{3} \w/(.+)$", line)
			if m:
				p = m.group(1).strip()
				if p and p != "/dev/null":
					files.append(p)
	unique = sorted(set(files))
	# Limits
	if len(unique) > int(limits.get("max_files", 10)):
		return False, "too_many_files", {"files": unique}
	# Path checks and allow/block
	allowed_ext = {".py", ".md", ".txt", ".json", ".yml", ".yaml", ".toml", ".ini", ".cfg"} if bool(limits.get("strict")) else None
	for p in unique:
		if p.startswith("/") or ".." in p.split("/"):
			return False, "unsafe_path", {"path": p}
		parts = Path(p).parts
		if parts:
			head = parts[0]
			if bool(limits.get("strict")) and head in {".github", "node_modules", "dist", "build", "models", "data"}:
				return False, f"risky_dir:{head}", {"path": p}
		pp = Path(p).as_posix()
		blockp = limits.get("block_patterns") or []
		allowp = limits.get("allow_patterns") or []
		if blockp and any(fnmatch.fnmatch(pp, pat) for pat in blockp):
			return False, f"blocked_path:{p}", {}
		if allowp:
			if not any(fnmatch.fnmatch(pp, pat) for pat in allowp):
				return False, f"not_in_allowlist:{p}", {}
		elif allowed_ext is not None:
			ext = Path(p).suffix.lower()
			if ext and ext not in allowed_ext:
				return False, f"disallowed_extension:{ext}", {}
	# Churn limits
	added = 0; deleted = 0
	for line in text.splitlines():
		if line.startswith("+") and not line.startswith("+++ "):
			added += 1
		elif line.startswith("-") and not line.startswith("--- "):
			deleted += 1
	if added > int(limits.get("max_added", 400)) or deleted > int(limits.get("max_deleted", 200)):
		return False, "too_much_churn", {"added": added, "deleted": deleted}
	return True, None, {"files": unique, "added": added, "deleted": deleted}


from __future__ import annotations
import logging

from pathlib import Path
from typing import Dict, Any, List, Tuple
import os
import re
import fnmatch

def load_env_limits(strict_default: bool = True) -> Dict[str, int | bool | str]:
	strict = os.environ.get("AGI_PATCH_STRICT", "1" if strict_default else "0").strip() not in ("0", "false", "False")
	max_files = int(os.environ.get("AGI_PATCH_MAX_FILES", "10" if strict else "20") or ("10" if strict else "20"))
	max_added = int(os.environ.get("AGI_PATCH_MAX_ADDED", "400" if strict else "1000") or ("400" if strict else "1000"))
	max_deleted = int(os.environ.get("AGI_PATCH_MAX_DELETED", "200" if strict else "500") or ("200" if strict else "500"))
	allow_raw = os.environ.get("AGI_PATCH_ALLOW", "").strip()
	block_raw = os.environ.get("AGI_PATCH_BLOCK", "").strip()
	allow_patterns = [p.strip() for p in allow_raw.split(",") if p.strip()]
	block_patterns = [p.strip() for p in block_raw.split(",") if p.strip()]
	allow_mode = os.environ.get("AGI_PATCH_ALLOW_FILE_MODES", "0").strip() in ("1", "true", "True")
	allow_rename = os.environ.get("AGI_PATCH_ALLOW_RENAMES", "0").strip() in ("1", "true", "True")
	return {
		"strict": strict,
		"max_files": max_files,
		"max_added": max_added,
		"max_deleted": max_deleted,
		"allow_patterns": allow_patterns,
		"block_patterns": block_patterns,
		"allow_mode": allow_mode,
		"allow_rename": allow_rename,
	}

def validate_unified_diff(text: str, limits: Dict[str, Any]) -> Tuple[bool, str | None, Dict[str, Any]]:
	# Binary patch
	if "GIT binary patch" in text:
		return False, "binary_patch_not_allowed", {}
	allow_mode = bool(limits.get("allow_mode"))
	allow_rename = bool(limits.get("allow_rename"))
	if not allow_mode:
		for line in text.splitlines():
			l = line.strip().lower()
			if l.startswith("new file mode ") or l.startswith("deleted file mode ") or l.startswith("old mode ") or l.startswith("new mode "):
				return False, "file_mode_change_not_allowed", {}
	if not allow_rename:
		for line in text.splitlines():
			l = line.strip().lower()
			if l.startswith("rename from ") or l.startswith("rename to "):
				return False, "rename_not_allowed", {}
	# Gather files from headers
	files: List[str] = []


	max_added = int(os.environ.get("AGI_PATCH_MAX_ADDED", "400" if strict else "1000") or ("400" if strict else "1000"))
	max_deleted = int(os.environ.get("AGI_PATCH_MAX_DELETED", "200" if strict else "500") or ("200" if strict else "500"))
	allow_raw = os.environ.get("AGI_PATCH_ALLOW", "").strip()
	block_raw = os.environ.get("AGI_PATCH_BLOCK", "").strip()
	allow_patterns = [p.strip() for p in allow_raw.split(",") if p.strip()]
	block_patterns = [p.strip() for p in block_raw.split(",") if p.strip()]
	allow_mode = os.environ.get("AGI_PATCH_ALLOW_FILE_MODES", "0").strip() in ("1", "true", "True")
	allow_rename = os.environ.get("AGI_PATCH_ALLOW_RENAMES", "0").strip() in ("1", "true", "True")
	return {
		"strict": strict,
		"max_files": max_files,
		"max_added": max_added,
		"max_deleted": max_deleted,
		"allow_patterns": allow_patterns,
		"block_patterns": block_patterns,
		"allow_mode": allow_mode,
		"allow_rename": allow_rename,
	}

def validate_unified_diff(text: str, limits: Dict[str, Any]) -> Tuple[bool, str | None, Dict[str, Any]]:
	# Binary patch
	if "GIT binary patch" in text:
		return False, "binary_patch_not_allowed", {}
	allow_mode = bool(limits.get("allow_mode"))
	allow_rename = bool(limits.get("allow_rename"))
	if not allow_mode:
		for line in text.splitlines():
			l = line.strip().lower()
			if l.startswith("new file mode ") or l.startswith("deleted file mode ") or l.startswith("old mode ") or l.startswith("new mode "):
				return False, "file_mode_change_not_allowed", {}
	if not allow_rename:
		for line in text.splitlines():
			l = line.strip().lower()
			if l.startswith("rename from ") or l.startswith("rename to "):
				return False, "rename_not_allowed", {}
	# Gather files from headers
	files: List[str] = []
	for line in text.splitlines():
		if line.startswith("+++ ") or line.startswith("--- "):
			m = re.match(r"[+-]{3} \w/(.+)$", line)
			if m:
				p = m.group(1).strip()
				if p and p != "/dev/null":
					files.append(p)
	unique = sorted(set(files))
	# Limits
	if len(unique) > int(limits.get("max_files", 10)):
		return False, "too_many_files", {"files": unique}
	# Path checks and allow/block
	allowed_ext = {".py", ".md", ".txt", ".json", ".yml", ".yaml", ".toml", ".ini", ".cfg"} if bool(limits.get("strict")) else None
	for p in unique:
		if p.startswith("/") or ".." in p.split("/"):
			return False, "unsafe_path", {"path": p}
		parts = Path(p).parts
		if parts:
			head = parts[0]
			if bool(limits.get("strict")) and head in {".github", "node_modules", "dist", "build", "models", "data"}:
				return False, f"risky_dir:{head}", {"path": p}
		pp = Path(p).as_posix()
		blockp = limits.get("block_patterns") or []
		allowp = limits.get("allow_patterns") or []
		if blockp and any(fnmatch.fnmatch(pp, pat) for pat in blockp):
			return False, f"blocked_path:{p}", {}
		if allowp:
			if not any(fnmatch.fnmatch(pp, pat) for pat in allowp):
				return False, f"not_in_allowlist:{p}", {}
		elif allowed_ext is not None:
			ext = Path(p).suffix.lower()
			if ext and ext not in allowed_ext:
				return False, f"disallowed_extension:{ext}", {}
	# Churn limits
	added = 0; deleted = 0
	for line in text.splitlines():
		if line.startswith("+") and not line.startswith("+++ "):
			added += 1
		elif line.startswith("-") and not line.startswith("--- "):
			deleted += 1
	if added > int(limits.get("max_added", 400)) or deleted > int(limits.get("max_deleted", 200)):
		return False, "too_much_churn", {"added": added, "deleted": deleted}
	return True, None, {"files": unique, "added": added, "deleted": deleted}


from __future__ import annotations
import logging

import json
from pathlib import Path
from typing import Any, Dict, Iterable, List, Tuple

def read_jsonl(path: str | Path) -> Iterable[Dict[str, Any]]:
	p = Path(path)
	if not p.exists():
		return []
	with p.open("r", encoding="utf-8") as f:
		for line in f:
			s = line.strip()
			if not s:
				continue
			try:
				yield json.loads(s)
			except Exception:
				continue

def iter_rows(paths: List[str]) -> Tuple[int, List[Dict[str, Any]]]:
	rows: List[Dict[str, Any]] = []
	count = 0
	for p in (paths or []):
		for obj in read_jsonl(p):
			rows.append(obj)
			count += 1
	return count, rows

def write_json(path: str | Path, obj: Dict[str, Any]) -> None:
	p = Path(path)
	p.parent.mkdir(parents=True, exist_ok=True)
	p.write_text(json.dumps(obj, ensure_ascii=False, indent=2), encoding="utf-8")

def safe_float(x: Any, default: float = 0.0) -> float:
	try:
		if x is None:
			return float(default)
		return float(x)
	except Exception:
		return float(default)

def safe_int(x: Any, default: int = 0) -> int:
	try:
		if x is None:
			return int(default)
		return int(x)
	except Exception:
		return int(default)


from __future__ import annotations
import logging

import json
from pathlib import Path
from typing import Any, Dict, Iterable, List, Tuple

def read_jsonl(path: str | Path) -> Iterable[Dict[str, Any]]:
	p = Path(path)
	if not p.exists():
		return []
	with p.open("r", encoding="utf-8") as f:
		for line in f:
			s = line.strip()
			if not s:
				continue
			try:
				yield json.loads(s)
			except Exception:
				continue

def iter_rows(paths: List[str]) -> Tuple[int, List[Dict[str, Any]]]:
	rows: List[Dict[str, Any]] = []
	count = 0
	for p in (paths or []):
		for obj in read_jsonl(p):
			rows.append(obj)
			count += 1
	return count, rows

def write_json(path: str | Path, obj: Dict[str, Any]) -> None:
	p = Path(path)
	p.parent.mkdir(parents=True, exist_ok=True)
	p.write_text(json.dumps(obj, ensure_ascii=False, indent=2), encoding="utf-8")

def safe_float(x: Any, default: float = 0.0) -> float:
	try:


import json
from pathlib import Path
from typing import Any, Dict, Iterable, List, Tuple

def read_jsonl(path: str | Path) -> Iterable[Dict[str, Any]]:
	p = Path(path)
	if not p.exists():
		return []
	with p.open("r", encoding="utf-8") as f:
		for line in f:
			s = line.strip()
			if not s:
				continue
			try:
				yield json.loads(s)
			except Exception:
				continue

def iter_rows(paths: List[str]) -> Tuple[int, List[Dict[str, Any]]]:
	rows: List[Dict[str, Any]] = []
	count = 0
	for p in (paths or []):
		for obj in read_jsonl(p):
			rows.append(obj)
			count += 1
	return count, rows

def write_json(path: str | Path, obj: Dict[str, Any]) -> None:
	p = Path(path)
	p.parent.mkdir(parents=True, exist_ok=True)
	p.write_text(json.dumps(obj, ensure_ascii=False, indent=2), encoding="utf-8")

def safe_float(x: Any, default: float = 0.0) -> float:
	try:
		if x is None:
			return float(default)
		return float(x)
	except Exception:
		return float(default)

def safe_int(x: Any, default: int = 0) -> int:
	try:
		if x is None:


		for line in f:
			s = line.strip()
			if not s:
				continue
			try:
				yield json.loads(s)
			except Exception:
				continue

def iter_rows(paths: List[str]) -> Tuple[int, List[Dict[str, Any]]]:
	rows: List[Dict[str, Any]] = []
	count = 0
	for p in (paths or []):
		for obj in read_jsonl(p):
			rows.append(obj)
			count += 1
	return count, rows

def write_json(path: str | Path, obj: Dict[str, Any]) -> None:
	p = Path(path)
	p.parent.mkdir(parents=True, exist_ok=True)
	p.write_text(json.dumps(obj, ensure_ascii=False, indent=2), encoding="utf-8")

def safe_float(x: Any, default: float = 0.0) -> float:
	try:
		if x is None:
			return float(default)
		return float(x)
	except Exception:
		return float(default)

def safe_int(x: Any, default: int = 0) -> int:
	try:
		if x is None:
			return int(default)
		return int(x)
	except Exception:
		return int(default)


			except Exception:
				continue

def iter_rows(paths: List[str]) -> Tuple[int, List[Dict[str, Any]]]:
	rows: List[Dict[str, Any]] = []
	count = 0
	for p in (paths or []):
		for obj in read_jsonl(p):
			rows.append(obj)
			count += 1
	return count, rows

def write_json(path: str | Path, obj: Dict[str, Any]) -> None:
	p = Path(path)
	p.parent.mkdir(parents=True, exist_ok=True)
	p.write_text(json.dumps(obj, ensure_ascii=False, indent=2), encoding="utf-8")

def safe_float(x: Any, default: float = 0.0) -> float:
	try:
		if x is None:
			return float(default)
		return float(x)
	except Exception:
		return float(default)

def safe_int(x: Any, default: int = 0) -> int:
	try:
		if x is None:
			return int(default)
		return int(x)
	except Exception:
		return int(default)


			rows.append(obj)
			count += 1
	return count, rows

def write_json(path: str | Path, obj: Dict[str, Any]) -> None:
	p = Path(path)
	p.parent.mkdir(parents=True, exist_ok=True)
	p.write_text(json.dumps(obj, ensure_ascii=False, indent=2), encoding="utf-8")

def safe_float(x: Any, default: float = 0.0) -> float:
	try:
		if x is None:
			return float(default)
		return float(x)
	except Exception:
		return float(default)

def safe_int(x: Any, default: int = 0) -> int:
	try:
		if x is None:
			return int(default)
		return int(x)
	except Exception:
		return int(default)


from __future__ import annotations
import logging

import subprocess
import shlex
import time
from pathlib import Path
from typing import Dict, Any, List, Optional

def _run_cmd(cmd: List[str], cwd: str | Path) -> tuple[int, str]:
	start = time.time()
	try:
		p = subprocess.Popen(cmd, cwd=str(cwd), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
		out, _ = p.communicate()
		elapsed = max(0.0, time.time() - start)
		return p.returncode, out or ""
	except Exception as e:
		elapsed = max(0.0, time.time() - start)
		return 127, f"[test-runner] failed to execute: {' '.join(cmd)}\n{e}"

def run_pytest(repo_dir: str | Path, args: Optional[List[str]] = None) -> Dict[str, Any]:
	repo = Path(repo_dir)
	cmd = ["pytest", "-q"] + (args or [])
	code, out = _run_cmd(cmd, repo)
	# Very light summary parse
	summary = {"passed": 0, "failed": 0, "errors": 0, "skipped": 0, "total": 0}
	for line in (out or "").splitlines():
		l = line.strip().lower()
		if l.endswith(" in") or ("seconds" in l and ("passed" in l or "failed" in l)):
			# Example: 12 passed, 1 failed, 2 skipped in 1.23s
			parts = [p.strip() for p in l.replace("=", " ").replace(",", " ").split()]
			for i, p in enumerate(parts):
				try:
					n = int(p)
					if i + 1 < len(parts):
						k = parts[i + 1]
						if k.startswith("passed"):
							summary["passed"] = n
						elif k.startswith("failed"):
							summary["failed"] = n
						elif k.startswith("error"):
							summary["errors"] = n
						elif k.startswith("skipped"):
							summary["skipped"] = n
				except Exception:
					continue
			break
	summary["total"] = int(summary["passed"]) + int(summary["failed"]) + int(summary["errors"]) + int(summary["skipped"])
	return {
		"tool": "pytest",
		"cmd": " ".join(shlex.quote(c) for c in cmd),
		"code": code,
		"output": out,
		"summary": summary,
	}

def run_npm_jest(repo_dir: str | Path, args: Optional[List[str]] = None) -> Dict[str, Any]:
	repo = Path(repo_dir)
	# Prefer npx jest if available; fallback to npm test
	cmd = ["npx", "jest", "--runInBand"] + (args or [])
	code, out = _run_cmd(cmd, repo)
	if code == 127:
		cmd = ["npm", "test", "--silent"] + (args or [])
		code, out = _run_cmd(cmd, repo)
	# Light summary
	summary = {"passed": 0, "failed": 0, "total": 0}
	for line in (out or "").splitlines():
		l = line.strip().lower()
		# Example: Tests: 10 passed, 2 failed, 12 total
		if l.startswith("tests:") and "total" in l:
			try:
				parts = l.replace(",", "").split()
				for i, p in enumerate(parts):
					if p == "passed":
						summary["passed"] = int(parts[i - 1]) if i > 0 else 0
					elif p == "failed":
						summary["failed"] = int(parts[i - 1]) if i > 0 else 0
					elif p == "total":
						summary["total"] = int(parts[i - 1]) if i > 0 else 0
			except Exception:
				pass
			break
	return {
		"tool": "jest",
		"cmd": " ".join(shlex.quote(c) for c in cmd),
		"code": code,
		"output": out,
		"summary": summary,
	}

class TestRunner:
	"""Minimal test runner for pytest or npm test with structured results."""

	def __init__(self, cwd: str) -> None:
		self.cwd = Path(cwd)

	def run_pytest(self, args: Optional[list[str]] = None, timeout: int = 600, env: Optional[dict] = None) -> Dict:
		# Include -q for concise output and -rA to ensure failure summaries are present
		cmd = ["pytest", "-q", "-rA"] + (args or [])
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		# Fallback: some repos add unsupported options (e.g., --cov) via pytest.ini addopts.
		# If we detect an "unrecognized arguments" error, rerun with a clean config (-c /dev/null)
		# to bypass repository-level pytest.ini.
		try:
			stderr_lower = (p.stderr or "").lower()
			stdout_lower = (p.stdout or "").lower()
			if p.returncode != 0 and ("unrecognized arguments" in stderr_lower or "unrecognized arguments" in stdout_lower):
				clean_cmd = ["pytest", "-q", "-rA", "-c", "/dev/null"]
				p = subprocess.run(clean_cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		except Exception:
			pass
		res = {
			"returncode": p.returncode,
			"stdout": p.stdout,
			"stderr": p.stderr,
			"ok": p.returncode == 0,
		}
		# Optional flake hint: detect reruns or flaky markers without changing behavior
		try:
			stdout_text = (p.stdout or "")
			text_lc = (stdout_text + "\n" + (p.stderr or "")).lower()
			# Count lines that look like pytest-rerunfailures summary entries (e.g., 'RERUN test_mod::test_name')
			reruns = 0
			for line in stdout_text.splitlines():
				ls = line.strip()
				if ls.upper().startswith("RERUN "):
					reruns += 1
			if reruns > 0 or any(tok in text_lc for tok in ("flaky", "rerunfailures", "rerun failure", "intermittent")):
				res["flake_hint"] = True
				if reruns > 0:
					res["flake_reruns"] = int(reruns)
		except Exception:
			pass
		# Optional coverage summary extraction if present in output (e.g., coverage.py TOTAL line)
		try:
			import re as _re  # type: ignore
			cov = None
			for line in (p.stdout or "").splitlines():
				l = line.strip()
				# Common coverage table row: 'TOTAL    123     4    97%'
				m = _re.search(r"^TOTAL\s+\d+\s+\d+\s+(\d+)%$", l, flags=_re.IGNORECASE)
				if m:
					try:
						pct = float(m.group(1))
						cov = {"percent": float(round(pct, 3))}
						break
					except Exception:
						continue
			if cov is not None:
				res["coverage"] = cov
		except Exception:
			pass
		# Best-effort failure extraction from summary lines like: "FAILED path/to/test_file.py::test_name - Traceback..."
		try:
			fails = []
			for line in (p.stdout or "").splitlines():
				line = line.strip()
				if line.startswith("FAILED ") and "::" in line:
					part = line[len("FAILED "):].split(" - ")[0]
					path, test = part.split("::", 1)
					fails.append({"path": path.strip(), "test": test.strip()})
			if fails:
				res["failures"] = fails
		except Exception:
			pass
		return res

	def run_npm_test(self, script: str = "test", timeout: int = 900, env: Optional[dict] = None) -> Dict:
		p = subprocess.run(["npm", "run", script, "--silent"], cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		return {
			"returncode": p.returncode,
			"stdout": p.stdout,
			"stderr": p.stderr,
			"ok": p.returncode == 0,
		}

	def run_nose(self, args: Optional[list[str]] = None, timeout: int = 600, env: Optional[dict] = None) -> Dict:
		cmd = ["nosetests", "-q"] + (args or [])
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		return {
			"returncode": p.returncode,
			"stdout": p.stdout,
			"stderr": p.stderr,
			"ok": p.returncode == 0,
		}

	def run_go_test(self, pkgs: Optional[list[str]] = None, timeout: int = 900, env: Optional[dict] = None) -> Dict:
		cmd = ["go", "test", "./..."] if not pkgs else ["go", "test", *pkgs]
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		return {
			"returncode": p.returncode,
			"stdout": p.stdout,
			"stderr": p.stderr,
			"ok": p.returncode == 0,
		}

	def run_eslint(self, files: Optional[list[str]] = None, timeout: int = 600, env: Optional[dict] = None) -> Dict:
		cmd = ["npx", "eslint", "--format", "json", *(files or ["."])]
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		issues: list[dict] = []
		try:
			import json as _json  # type: ignore
			parsed = _json.loads(p.stdout or "[]")
			for file_res in parsed:
				for m in file_res.get("messages", []) or []:
					issues.append({
						"path": file_res.get("filePath", ""),
						"ruleId": m.get("ruleId"),
						"severity": m.get("severity"),
						"line": m.get("line"),
						"msg": m.get("message"),
					})
		except Exception:
			issues = []
		return {"returncode": p.returncode, "ok": p.returncode == 0, "issues": issues}

	def run_tsc(self, project: Optional[str] = None, timeout: int = 900, env: Optional[dict] = None) -> Dict:
		cmd = ["npx", "tsc", "-p", (project or ".")]
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		return {"returncode": p.returncode, "ok": p.returncode == 0, "stdout": p.stdout, "stderr": p.stderr}


from __future__ import annotations
import logging

import subprocess
import shlex
import time
from pathlib import Path
from typing import Dict, Any, List, Optional

def _run_cmd(cmd: List[str], cwd: str | Path) -> tuple[int, str]:
	start = time.time()
	try:
		p = subprocess.Popen(cmd, cwd=str(cwd), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
		out, _ = p.communicate()
		elapsed = max(0.0, time.time() - start)
		return p.returncode, out or ""
	except Exception as e:
		elapsed = max(0.0, time.time() - start)
		return 127, f"[test-runner] failed to execute: {' '.join(cmd)}\n{e}"

def run_pytest(repo_dir: str | Path, args: Optional[List[str]] = None) -> Dict[str, Any]:
	repo = Path(repo_dir)
	cmd = ["pytest", "-q"] + (args or [])
	code, out = _run_cmd(cmd, repo)
	# Very light summary parse
	summary = {"passed": 0, "failed": 0, "errors": 0, "skipped": 0, "total": 0}
	for line in (out or "").splitlines():
		l = line.strip().lower()
		if l.endswith(" in") or ("seconds" in l and ("passed" in l or "failed" in l)):
			# Example: 12 passed, 1 failed, 2 skipped in 1.23s
			parts = [p.strip() for p in l.replace("=", " ").replace(",", " ").split()]
			for i, p in enumerate(parts):
				try:
					n = int(p)
					if i + 1 < len(parts):
						k = parts[i + 1]
						if k.startswith("passed"):
							summary["passed"] = n


					elif p == "total":
						summary["total"] = int(parts[i - 1]) if i > 0 else 0
			except Exception:
				pass
			break
	return {
		"tool": "jest",
		"cmd": " ".join(shlex.quote(c) for c in cmd),
		"code": code,
		"output": out,
		"summary": summary,
	}

class TestRunner:
	"""Minimal test runner for pytest or npm test with structured results."""

	def __init__(self, cwd: str) -> None:
		self.cwd = Path(cwd)

	def run_pytest(self, args: Optional[list[str]] = None, timeout: int = 600, env: Optional[dict] = None) -> Dict:
		# Include -q for concise output and -rA to ensure failure summaries are present
		cmd = ["pytest", "-q", "-rA"] + (args or [])
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		# Fallback: some repos add unsupported options (e.g., --cov) via pytest.ini addopts.
		# If we detect an "unrecognized arguments" error, rerun with a clean config (-c /dev/null)
		# to bypass repository-level pytest.ini.
		try:
			stderr_lower = (p.stderr or "").lower()
			stdout_lower = (p.stdout or "").lower()
			if p.returncode != 0 and ("unrecognized arguments" in stderr_lower or "unrecognized arguments" in stdout_lower):
				clean_cmd = ["pytest", "-q", "-rA", "-c", "/dev/null"]
				p = subprocess.run(clean_cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		except Exception:
			pass
		res = {
			"returncode": p.returncode,
			"stdout": p.stdout,
			"stderr": p.stderr,
			"ok": p.returncode == 0,
		}
		# Optional flake hint: detect reruns or flaky markers without changing behavior
		try:
			stdout_text = (p.stdout or "")
			text_lc = (stdout_text + "\n" + (p.stderr or "")).lower()
			# Count lines that look like pytest-rerunfailures summary entries (e.g., 'RERUN test_mod::test_name')
			reruns = 0
			for line in stdout_text.splitlines():
				ls = line.strip()
				if ls.upper().startswith("RERUN "):
					reruns += 1
			if reruns > 0 or any(tok in text_lc for tok in ("flaky", "rerunfailures", "rerun failure", "intermittent")):
				res["flake_hint"] = True
				if reruns > 0:
					res["flake_reruns"] = int(reruns)
		except Exception:
			pass
		# Optional coverage summary extraction if present in output (e.g., coverage.py TOTAL line)
		try:
			import re as _re  # type: ignore
			cov = None
			for line in (p.stdout or "").splitlines():
				l = line.strip()
				# Common coverage table row: 'TOTAL    123     4    97%'
				m = _re.search(r"^TOTAL\s+\d+\s+\d+\s+(\d+)%$", l, flags=_re.IGNORECASE)
				if m:
					try:
						pct = float(m.group(1))
						cov = {"percent": float(round(pct, 3))}
						break
					except Exception:
						continue
			if cov is not None:
				res["coverage"] = cov
		except Exception:
			pass
		# Best-effort failure extraction from summary lines like: "FAILED path/to/test_file.py::test_name - Traceback..."
		try:
			fails = []
			for line in (p.stdout or "").splitlines():
				line = line.strip()
				if line.startswith("FAILED ") and "::" in line:
					part = line[len("FAILED "):].split(" - ")[0]
					path, test = part.split("::", 1)
					fails.append({"path": path.strip(), "test": test.strip()})
			if fails:
				res["failures"] = fails
		except Exception:
			pass
		return res

	def run_npm_test(self, script: str = "test", timeout: int = 900, env: Optional[dict] = None) -> Dict:
		p = subprocess.run(["npm", "run", script, "--silent"], cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		return {
			"returncode": p.returncode,
			"stdout": p.stdout,
			"stderr": p.stderr,
			"ok": p.returncode == 0,
		}

	def run_nose(self, args: Optional[list[str]] = None, timeout: int = 600, env: Optional[dict] = None) -> Dict:
		cmd = ["nosetests", "-q"] + (args or [])
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		return {
			"returncode": p.returncode,
			"stdout": p.stdout,
			"stderr": p.stderr,
			"ok": p.returncode == 0,
		}


							summary["passed"] = n
						elif k.startswith("failed"):
							summary["failed"] = n
						elif k.startswith("error"):
							summary["errors"] = n
						elif k.startswith("skipped"):
							summary["skipped"] = n
				except Exception:
					continue
			break
	summary["total"] = int(summary["passed"]) + int(summary["failed"]) + int(summary["errors"]) + int(summary["skipped"])
	return {
		"tool": "pytest",
		"cmd": " ".join(shlex.quote(c) for c in cmd),
		"code": code,
		"output": out,
		"summary": summary,
	}

def run_npm_jest(repo_dir: str | Path, args: Optional[List[str]] = None) -> Dict[str, Any]:
	repo = Path(repo_dir)
	# Prefer npx jest if available; fallback to npm test
	cmd = ["npx", "jest", "--runInBand"] + (args or [])
	code, out = _run_cmd(cmd, repo)
	if code == 127:
		cmd = ["npm", "test", "--silent"] + (args or [])
		code, out = _run_cmd(cmd, repo)
	# Light summary
	summary = {"passed": 0, "failed": 0, "total": 0}
	for line in (out or "").splitlines():
		l = line.strip().lower()
		# Example: Tests: 10 passed, 2 failed, 12 total
		if l.startswith("tests:") and "total" in l:
			try:
				parts = l.replace(",", "").split()
				for i, p in enumerate(parts):
					if p == "passed":
						summary["passed"] = int(parts[i - 1]) if i > 0 else 0
					elif p == "failed":
						summary["failed"] = int(parts[i - 1]) if i > 0 else 0
					elif p == "total":
						summary["total"] = int(parts[i - 1]) if i > 0 else 0
			except Exception:
				pass
			break
	return {
		"tool": "jest",
		"cmd": " ".join(shlex.quote(c) for c in cmd),
		"code": code,
		"output": out,
		"summary": summary,
	}

class TestRunner:
	"""Minimal test runner for pytest or npm test with structured results."""

	def __init__(self, cwd: str) -> None:
		self.cwd = Path(cwd)

	def run_pytest(self, args: Optional[list[str]] = None, timeout: int = 600, env: Optional[dict] = None) -> Dict:
		# Include -q for concise output and -rA to ensure failure summaries are present
		cmd = ["pytest", "-q", "-rA"] + (args or [])
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		# Fallback: some repos add unsupported options (e.g., --cov) via pytest.ini addopts.
		# If we detect an "unrecognized arguments" error, rerun with a clean config (-c /dev/null)
		# to bypass repository-level pytest.ini.
		try:
			stderr_lower = (p.stderr or "").lower()
			stdout_lower = (p.stdout or "").lower()
			if p.returncode != 0 and ("unrecognized arguments" in stderr_lower or "unrecognized arguments" in stdout_lower):
				clean_cmd = ["pytest", "-q", "-rA", "-c", "/dev/null"]


TestRunner

				parts = l.replace(",", "").split()
				for i, p in enumerate(parts):
					if p == "passed":
						summary["passed"] = int(parts[i - 1]) if i > 0 else 0
					elif p == "failed":
						summary["failed"] = int(parts[i - 1]) if i > 0 else 0
					elif p == "total":
						summary["total"] = int(parts[i - 1]) if i > 0 else 0
			except Exception:
				pass
			break
	return {
		"tool": "jest",
		"cmd": " ".join(shlex.quote(c) for c in cmd),
		"code": code,
		"output": out,
		"summary": summary,
	}

class TestRunner:
	"""Minimal test runner for pytest or npm test with structured results."""

	def __init__(self, cwd: str) -> None:
		self.cwd = Path(cwd)

	def run_pytest(self, args: Optional[list[str]] = None, timeout: int = 600, env: Optional[dict] = None) -> Dict:
		# Include -q for concise output and -rA to ensure failure summaries are present
		cmd = ["pytest", "-q", "-rA"] + (args or [])
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		# Fallback: some repos add unsupported options (e.g., --cov) via pytest.ini addopts.
		# If we detect an "unrecognized arguments" error, rerun with a clean config (-c /dev/null)
		# to bypass repository-level pytest.ini.
		try:
			stderr_lower = (p.stderr or "").lower()
			stdout_lower = (p.stdout or "").lower()
			if p.returncode != 0 and ("unrecognized arguments" in stderr_lower or "unrecognized arguments" in stdout_lower):
				clean_cmd = ["pytest", "-q", "-rA", "-c", "/dev/null"]
				p = subprocess.run(clean_cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		except Exception:
			pass
		res = {
			"returncode": p.returncode,
			"stdout": p.stdout,
			"stderr": p.stderr,
			"ok": p.returncode == 0,
		}
		# Optional flake hint: detect reruns or flaky markers without changing behavior
		try:
			stdout_text = (p.stdout or "")
			text_lc = (stdout_text + "\n" + (p.stderr or "")).lower()
			# Count lines that look like pytest-rerunfailures summary entries (e.g., 'RERUN test_mod::test_name')
			reruns = 0
			for line in stdout_text.splitlines():
				ls = line.strip()
				if ls.upper().startswith("RERUN "):
					reruns += 1
			if reruns > 0 or any(tok in text_lc for tok in ("flaky", "rerunfailures", "rerun failure", "intermittent")):
				res["flake_hint"] = True
				if reruns > 0:
					res["flake_reruns"] = int(reruns)
		except Exception:
			pass
		# Optional coverage summary extraction if present in output (e.g., coverage.py TOTAL line)
		try:
			import re as _re  # type: ignore
			cov = None
			for line in (p.stdout or "").splitlines():
				l = line.strip()
				# Common coverage table row: 'TOTAL    123     4    97%'
				m = _re.search(r"^TOTAL\s+\d+\s+\d+\s+(\d+)%$", l, flags=_re.IGNORECASE)
				if m:
					try:
						pct = float(m.group(1))
						cov = {"percent": float(round(pct, 3))}
						break
					except Exception:
						continue
			if cov is not None:
				res["coverage"] = cov
		except Exception:
			pass
		# Best-effort failure extraction from summary lines like: "FAILED path/to/test_file.py::test_name - Traceback..."
		try:
			fails = []
			for line in (p.stdout or "").splitlines():
				line = line.strip()
				if line.startswith("FAILED ") and "::" in line:
					part = line[len("FAILED "):].split(" - ")[0]
					path, test = part.split("::", 1)
					fails.append({"path": path.strip(), "test": test.strip()})
			if fails:
				res["failures"] = fails
		except Exception:
			pass
		return res

	def run_npm_test(self, script: str = "test", timeout: int = 900, env: Optional[dict] = None) -> Dict:
		p = subprocess.run(["npm", "run", script, "--silent"], cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		return {
			"returncode": p.returncode,
			"stdout": p.stdout,
			"stderr": p.stderr,
			"ok": p.returncode == 0,
		}

	def run_nose(self, args: Optional[list[str]] = None, timeout: int = 600, env: Optional[dict] = None) -> Dict:
		cmd = ["nosetests", "-q"] + (args or [])
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		return {
			"returncode": p.returncode,
			"stdout": p.stdout,
			"stderr": p.stderr,
			"ok": p.returncode == 0,
		}

	def run_go_test(self, pkgs: Optional[list[str]] = None, timeout: int = 900, env: Optional[dict] = None) -> Dict:
		cmd = ["go", "test", "./..."] if not pkgs else ["go", "test", *pkgs]
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		return {
			"returncode": p.returncode,
			"stdout": p.stdout,
			"stderr": p.stderr,
			"ok": p.returncode == 0,
		}

	def run_eslint(self, files: Optional[list[str]] = None, timeout: int = 600, env: Optional[dict] = None) -> Dict:
		cmd = ["npx", "eslint", "--format", "json", *(files or ["."])]
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		issues: list[dict] = []
		try:
			import json as _json  # type: ignore
			parsed = _json.loads(p.stdout or "[]")
			for file_res in parsed:
				for m in file_res.get("messages", []) or []:
					issues.append({
						"path": file_res.get("filePath", ""),
						"ruleId": m.get("ruleId"),
						"severity": m.get("severity"),
						"line": m.get("line"),
						"msg": m.get("message"),
					})
		except Exception:
			issues = []
		return {"returncode": p.returncode, "ok": p.returncode == 0, "issues": issues}

	def run_tsc(self, project: Optional[str] = None, timeout: int = 900, env: Optional[dict] = None) -> Dict:
		cmd = ["npx", "tsc", "-p", (project or ".")]
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		return {"returncode": p.returncode, "ok": p.returncode == 0, "stdout": p.stdout, "stderr": p.stderr}


						summary["passed"] = int(parts[i - 1]) if i > 0 else 0
					elif p == "failed":
						summary["failed"] = int(parts[i - 1]) if i > 0 else 0
					elif p == "total":
						summary["total"] = int(parts[i - 1]) if i > 0 else 0
			except Exception:
				pass
			break
	return {
		"tool": "jest",
		"cmd": " ".join(shlex.quote(c) for c in cmd),
		"code": code,
		"output": out,
		"summary": summary,
	}

class TestRunner:
	"""Minimal test runner for pytest or npm test with structured results."""

	def __init__(self, cwd: str) -> None:
		self.cwd = Path(cwd)

	def run_pytest(self, args: Optional[list[str]] = None, timeout: int = 600, env: Optional[dict] = None) -> Dict:
		# Include -q for concise output and -rA to ensure failure summaries are present
		cmd = ["pytest", "-q", "-rA"] + (args or [])
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		# Fallback: some repos add unsupported options (e.g., --cov) via pytest.ini addopts.
		# If we detect an "unrecognized arguments" error, rerun with a clean config (-c /dev/null)
		# to bypass repository-level pytest.ini.
		try:
			stderr_lower = (p.stderr or "").lower()
			stdout_lower = (p.stdout or "").lower()
			if p.returncode != 0 and ("unrecognized arguments" in stderr_lower or "unrecognized arguments" in stdout_lower):
				clean_cmd = ["pytest", "-q", "-rA", "-c", "/dev/null"]
				p = subprocess.run(clean_cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		except Exception:
			pass
		res = {
			"returncode": p.returncode,
			"stdout": p.stdout,
			"stderr": p.stderr,


						continue
			if cov is not None:
				res["coverage"] = cov
		except Exception:
			pass
		# Best-effort failure extraction from summary lines like: "FAILED path/to/test_file.py::test_name - Traceback..."
		try:
			fails = []
			for line in (p.stdout or "").splitlines():
				line = line.strip()
				if line.startswith("FAILED ") and "::" in line:
					part = line[len("FAILED "):].split(" - ")[0]
					path, test = part.split("::", 1)
					fails.append({"path": path.strip(), "test": test.strip()})
			if fails:
				res["failures"] = fails
		except Exception:
			pass
		return res

	def run_npm_test(self, script: str = "test", timeout: int = 900, env: Optional[dict] = None) -> Dict:
		p = subprocess.run(["npm", "run", script, "--silent"], cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		return {
			"returncode": p.returncode,
			"stdout": p.stdout,
			"stderr": p.stderr,
			"ok": p.returncode == 0,
		}

	def run_nose(self, args: Optional[list[str]] = None, timeout: int = 600, env: Optional[dict] = None) -> Dict:
		cmd = ["nosetests", "-q"] + (args or [])
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		return {
			"returncode": p.returncode,
			"stdout": p.stdout,
			"stderr": p.stderr,
			"ok": p.returncode == 0,
		}

	def run_go_test(self, pkgs: Optional[list[str]] = None, timeout: int = 900, env: Optional[dict] = None) -> Dict:
		cmd = ["go", "test", "./..."] if not pkgs else ["go", "test", *pkgs]
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		return {
			"returncode": p.returncode,
			"stdout": p.stdout,
			"stderr": p.stderr,
			"ok": p.returncode == 0,
		}


				line = line.strip()
				if line.startswith("FAILED ") and "::" in line:
					part = line[len("FAILED "):].split(" - ")[0]
					path, test = part.split("::", 1)
					fails.append({"path": path.strip(), "test": test.strip()})
			if fails:
				res["failures"] = fails
		except Exception:
			pass
		return res

	def run_npm_test(self, script: str = "test", timeout: int = 900, env: Optional[dict] = None) -> Dict:
		p = subprocess.run(["npm", "run", script, "--silent"], cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		return {
			"returncode": p.returncode,
			"stdout": p.stdout,
			"stderr": p.stderr,
			"ok": p.returncode == 0,
		}

	def run_nose(self, args: Optional[list[str]] = None, timeout: int = 600, env: Optional[dict] = None) -> Dict:
		cmd = ["nosetests", "-q"] + (args or [])
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		return {
			"returncode": p.returncode,
			"stdout": p.stdout,
			"stderr": p.stderr,
			"ok": p.returncode == 0,
		}

	def run_go_test(self, pkgs: Optional[list[str]] = None, timeout: int = 900, env: Optional[dict] = None) -> Dict:
		cmd = ["go", "test", "./..."] if not pkgs else ["go", "test", *pkgs]
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		return {
			"returncode": p.returncode,
			"stdout": p.stdout,
			"stderr": p.stderr,
			"ok": p.returncode == 0,
		}

	def run_eslint(self, files: Optional[list[str]] = None, timeout: int = 600, env: Optional[dict] = None) -> Dict:
		cmd = ["npx", "eslint", "--format", "json", *(files or ["."])]
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		issues: list[dict] = []
		try:
			import json as _json  # type: ignore
			parsed = _json.loads(p.stdout or "[]")
			for file_res in parsed:
				for m in file_res.get("messages", []) or []:


	def run_npm_test(self, script: str = "test", timeout: int = 900, env: Optional[dict] = None) -> Dict:
		p = subprocess.run(["npm", "run", script, "--silent"], cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		return {
			"returncode": p.returncode,
			"stdout": p.stdout,
			"stderr": p.stderr,
			"ok": p.returncode == 0,
		}

	def run_nose(self, args: Optional[list[str]] = None, timeout: int = 600, env: Optional[dict] = None) -> Dict:
		cmd = ["nosetests", "-q"] + (args or [])
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		return {
			"returncode": p.returncode,
			"stdout": p.stdout,
			"stderr": p.stderr,
			"ok": p.returncode == 0,
		}

	def run_go_test(self, pkgs: Optional[list[str]] = None, timeout: int = 900, env: Optional[dict] = None) -> Dict:
		cmd = ["go", "test", "./..."] if not pkgs else ["go", "test", *pkgs]
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		return {
			"returncode": p.returncode,
			"stdout": p.stdout,
			"stderr": p.stderr,
			"ok": p.returncode == 0,
		}

	def run_eslint(self, files: Optional[list[str]] = None, timeout: int = 600, env: Optional[dict] = None) -> Dict:
		cmd = ["npx", "eslint", "--format", "json", *(files or ["."])]
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		issues: list[dict] = []
		try:
			import json as _json  # type: ignore
			parsed = _json.loads(p.stdout or "[]")
			for file_res in parsed:
				for m in file_res.get("messages", []) or []:
					issues.append({
						"path": file_res.get("filePath", ""),
						"ruleId": m.get("ruleId"),
						"severity": m.get("severity"),
						"line": m.get("line"),
						"msg": m.get("message"),
					})
		except Exception:
			issues = []
		return {"returncode": p.returncode, "ok": p.returncode == 0, "issues": issues}


	def run_nose(self, args: Optional[list[str]] = None, timeout: int = 600, env: Optional[dict] = None) -> Dict:
		cmd = ["nosetests", "-q"] + (args or [])
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		return {
			"returncode": p.returncode,
			"stdout": p.stdout,
			"stderr": p.stderr,
			"ok": p.returncode == 0,
		}

	def run_go_test(self, pkgs: Optional[list[str]] = None, timeout: int = 900, env: Optional[dict] = None) -> Dict:
		cmd = ["go", "test", "./..."] if not pkgs else ["go", "test", *pkgs]
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		return {
			"returncode": p.returncode,
			"stdout": p.stdout,
			"stderr": p.stderr,
			"ok": p.returncode == 0,
		}

	def run_eslint(self, files: Optional[list[str]] = None, timeout: int = 600, env: Optional[dict] = None) -> Dict:
		cmd = ["npx", "eslint", "--format", "json", *(files or ["."])]
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		issues: list[dict] = []
		try:
			import json as _json  # type: ignore
			parsed = _json.loads(p.stdout or "[]")
			for file_res in parsed:
				for m in file_res.get("messages", []) or []:
					issues.append({
						"path": file_res.get("filePath", ""),
						"ruleId": m.get("ruleId"),
						"severity": m.get("severity"),
						"line": m.get("line"),
						"msg": m.get("message"),
					})
		except Exception:
			issues = []
		return {"returncode": p.returncode, "ok": p.returncode == 0, "issues": issues}

	def run_tsc(self, project: Optional[str] = None, timeout: int = 900, env: Optional[dict] = None) -> Dict:
		cmd = ["npx", "tsc", "-p", (project or ".")]
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		return {"returncode": p.returncode, "ok": p.returncode == 0, "stdout": p.stdout, "stderr": p.stderr}


	def run_eslint(self, files: Optional[list[str]] = None, timeout: int = 600, env: Optional[dict] = None) -> Dict:
		cmd = ["npx", "eslint", "--format", "json", *(files or ["."])]
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		issues: list[dict] = []
		try:
			import json as _json  # type: ignore
			parsed = _json.loads(p.stdout or "[]")
			for file_res in parsed:
				for m in file_res.get("messages", []) or []:
					issues.append({
						"path": file_res.get("filePath", ""),
						"ruleId": m.get("ruleId"),
						"severity": m.get("severity"),
						"line": m.get("line"),
						"msg": m.get("message"),
					})
		except Exception:
			issues = []
		return {"returncode": p.returncode, "ok": p.returncode == 0, "issues": issues}

	def run_tsc(self, project: Optional[str] = None, timeout: int = 900, env: Optional[dict] = None) -> Dict:
		cmd = ["npx", "tsc", "-p", (project or ".")]
		p = subprocess.run(cmd, cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout, env=env)
		return {"returncode": p.returncode, "ok": p.returncode == 0, "stdout": p.stdout, "stderr": p.stderr}


from __future__ import annotations
import logging

from dataclasses import dataclass
from pathlib import Path
from typing import List, Dict, Any, Optional
import csv

@dataclass
class Sheet:
	name: str
	columns: List[str]
	rows: List[Dict[str, Any]]

def read_csv_sheet(path: str, name: Optional[str] = None) -> Sheet:
	p = Path(path)
	with p.open("r", encoding="utf-8") as f:
		reader = csv.DictReader(f)
		rows = [dict(r) for r in reader]
		columns = list(reader.fieldnames or [])
	return Sheet(name=name or p.stem, columns=columns, rows=rows)

def write_csv_sheet(sheet: Sheet, path: str) -> None:
	p = Path(path)
	p.parent.mkdir(parents=True, exist_ok=True)
	with p.open("w", newline="", encoding="utf-8") as f:
		writer = csv.DictWriter(f, fieldnames=sheet.columns)
		writer.writeheader()
		for r in sheet.rows:
			writer.writerow({k: r.get(k, "") for k in sheet.columns})

def evaluate_formula(cell: str, sheet: Sheet) -> Optional[float]:
	"""Very small evaluator for SUM/AVG over a single column.
	Syntax: SUM(column) or AVG(column)
	"""
	s = (cell or "").strip()
	if not s:
		return None
	def _col_values(col: str) -> List[float]:
		vals: List[float] = []
		for r in sheet.rows:
			try:
				v = float(str(r.get(col, "")).strip())
			except Exception:
				continue
			vals.append(v)
		return vals
	if s.upper().startswith("SUM(") and s.endswith(")"):
		col = s[4:-1].strip()
		vals = _col_values(col)
		return float(sum(vals)) if vals else 0.0
	if s.upper().startswith("AVG(") and s.endswith(")"):
		col = s[4:-1].strip()
		vals = _col_values(col)
		return float(sum(vals) / len(vals)) if vals else 0.0
	return None


Sheet

from __future__ import annotations
import logging

from dataclasses import dataclass
from pathlib import Path
from typing import List, Dict, Any, Optional
import csv

@dataclass
class Sheet:
	name: str
	columns: List[str]
	rows: List[Dict[str, Any]]

def read_csv_sheet(path: str, name: Optional[str] = None) -> Sheet:
	p = Path(path)
	with p.open("r", encoding="utf-8") as f:
		reader = csv.DictReader(f)
		rows = [dict(r) for r in reader]
		columns = list(reader.fieldnames or [])
	return Sheet(name=name or p.stem, columns=columns, rows=rows)

def write_csv_sheet(sheet: Sheet, path: str) -> None:
	p = Path(path)
	p.parent.mkdir(parents=True, exist_ok=True)
	with p.open("w", newline="", encoding="utf-8") as f:
		writer = csv.DictWriter(f, fieldnames=sheet.columns)
		writer.writeheader()
		for r in sheet.rows:
			writer.writerow({k: r.get(k, "") for k in sheet.columns})


from __future__ import annotations
import logging

from dataclasses import dataclass
from pathlib import Path
from typing import List, Dict, Any, Optional
import csv

@dataclass
class Sheet:
	name: str
	columns: List[str]
	rows: List[Dict[str, Any]]

def read_csv_sheet(path: str, name: Optional[str] = None) -> Sheet:
	p = Path(path)
	with p.open("r", encoding="utf-8") as f:
		reader = csv.DictReader(f)
		rows = [dict(r) for r in reader]
		columns = list(reader.fieldnames or [])
	return Sheet(name=name or p.stem, columns=columns, rows=rows)

def write_csv_sheet(sheet: Sheet, path: str) -> None:
	p = Path(path)
	p.parent.mkdir(parents=True, exist_ok=True)
	with p.open("w", newline="", encoding="utf-8") as f:
		writer = csv.DictWriter(f, fieldnames=sheet.columns)
		writer.writeheader()
		for r in sheet.rows:
			writer.writerow({k: r.get(k, "") for k in sheet.columns})

def evaluate_formula(cell: str, sheet: Sheet) -> Optional[float]:
	"""Very small evaluator for SUM/AVG over a single column.
	Syntax: SUM(column) or AVG(column)
	"""
	s = (cell or "").strip()
	if not s:
		return None
	def _col_values(col: str) -> List[float]:


from typing import List, Dict, Any, Optional
import csv

@dataclass
class Sheet:
	name: str
	columns: List[str]
	rows: List[Dict[str, Any]]

def read_csv_sheet(path: str, name: Optional[str] = None) -> Sheet:
	p = Path(path)
	with p.open("r", encoding="utf-8") as f:
		reader = csv.DictReader(f)
		rows = [dict(r) for r in reader]
		columns = list(reader.fieldnames or [])
	return Sheet(name=name or p.stem, columns=columns, rows=rows)

def write_csv_sheet(sheet: Sheet, path: str) -> None:
	p = Path(path)
	p.parent.mkdir(parents=True, exist_ok=True)
	with p.open("w", newline="", encoding="utf-8") as f:
		writer = csv.DictWriter(f, fieldnames=sheet.columns)
		writer.writeheader()
		for r in sheet.rows:
			writer.writerow({k: r.get(k, "") for k in sheet.columns})

def evaluate_formula(cell: str, sheet: Sheet) -> Optional[float]:
	"""Very small evaluator for SUM/AVG over a single column.
	Syntax: SUM(column) or AVG(column)
	"""
	s = (cell or "").strip()
	if not s:
		return None
	def _col_values(col: str) -> List[float]:
		vals: List[float] = []
		for r in sheet.rows:
			try:
				v = float(str(r.get(col, "")).strip())
			except Exception:
				continue
			vals.append(v)
		return vals
	if s.upper().startswith("SUM(") and s.endswith(")"):
		col = s[4:-1].strip()


def read_csv_sheet(path: str, name: Optional[str] = None) -> Sheet:
	p = Path(path)
	with p.open("r", encoding="utf-8") as f:
		reader = csv.DictReader(f)
		rows = [dict(r) for r in reader]
		columns = list(reader.fieldnames or [])
	return Sheet(name=name or p.stem, columns=columns, rows=rows)

def write_csv_sheet(sheet: Sheet, path: str) -> None:
	p = Path(path)
	p.parent.mkdir(parents=True, exist_ok=True)
	with p.open("w", newline="", encoding="utf-8") as f:
		writer = csv.DictWriter(f, fieldnames=sheet.columns)
		writer.writeheader()
		for r in sheet.rows:
			writer.writerow({k: r.get(k, "") for k in sheet.columns})

def evaluate_formula(cell: str, sheet: Sheet) -> Optional[float]:
	"""Very small evaluator for SUM/AVG over a single column.
	Syntax: SUM(column) or AVG(column)
	"""
	s = (cell or "").strip()
	if not s:
		return None
	def _col_values(col: str) -> List[float]:
		vals: List[float] = []
		for r in sheet.rows:
			try:
				v = float(str(r.get(col, "")).strip())
			except Exception:
				continue
			vals.append(v)
		return vals
	if s.upper().startswith("SUM(") and s.endswith(")"):
		col = s[4:-1].strip()
		vals = _col_values(col)
		return float(sum(vals)) if vals else 0.0
	if s.upper().startswith("AVG(") and s.endswith(")"):
		col = s[4:-1].strip()
		vals = _col_values(col)
		return float(sum(vals) / len(vals)) if vals else 0.0
	return None


	return Sheet(name=name or p.stem, columns=columns, rows=rows)

def write_csv_sheet(sheet: Sheet, path: str) -> None:
	p = Path(path)
	p.parent.mkdir(parents=True, exist_ok=True)
	with p.open("w", newline="", encoding="utf-8") as f:
		writer = csv.DictWriter(f, fieldnames=sheet.columns)
		writer.writeheader()
		for r in sheet.rows:
			writer.writerow({k: r.get(k, "") for k in sheet.columns})

def evaluate_formula(cell: str, sheet: Sheet) -> Optional[float]:
	"""Very small evaluator for SUM/AVG over a single column.
	Syntax: SUM(column) or AVG(column)
	"""
	s = (cell or "").strip()
	if not s:
		return None
	def _col_values(col: str) -> List[float]:
		vals: List[float] = []
		for r in sheet.rows:
			try:
				v = float(str(r.get(col, "")).strip())
			except Exception:
				continue
			vals.append(v)
		return vals
	if s.upper().startswith("SUM(") and s.endswith(")"):
		col = s[4:-1].strip()
		vals = _col_values(col)
		return float(sum(vals)) if vals else 0.0
	if s.upper().startswith("AVG(") and s.endswith(")"):
		col = s[4:-1].strip()
		vals = _col_values(col)
		return float(sum(vals) / len(vals)) if vals else 0.0
	return None