""" AirDbg mode — V2 调试器。 V2 改进:7步工作流强制追踪(L1 代码级),修复前自动 git snapshot 回滚。 """ from __future__ import annotations import subprocess from datetime import datetime, timezone from pathlib import Path from air_runtime.io import atomic_json_write, safe_json_load from air_runtime.paths import airplan_root, event_log_path from air_runtime.events import EventLog, DEBUG_SESSION from air_runtime.utils import now_iso, session_stamp DBG_STEPS = [ "confirm_symptoms", "load_context", "reproduce", "locate_root_cause", "fix", "verify", "close_out", ] class EvidenceFirstGate: """先读后写门控:未执行任何取证行为前,禁止代码修改。""" EVIDENCE_TYPES = [ "screenshot", "packet_capture", "static_analysis", "log_analysis", "code_trace", "reproduction", ] def __init__(self, session_id: str): self._session_id = session_id self._collected_evidence: list[str] = [] def record_evidence(self, evidence_type: str, detail: str = "") -> None: if evidence_type not in self.EVIDENCE_TYPES: raise ValueError(f"unknown evidence type: {evidence_type!r}") self._collected_evidence.append(evidence_type) def can_modify_code(self) -> bool: return len(self._collected_evidence) > 0 def gate_check(self) -> None: if not self.can_modify_code(): raise WorkflowViolation( "未执行任何取证行为,禁止修改代码。" "请先至少完成以下一项:截图、抓包、静态分析、日志分析、代码追踪、复现步骤。" ) class WorkflowViolation(Exception): """调试工作流违规。""" def _paths(project_root: Path) -> dict[str, Path]: root = airplan_root(project_root) / "state" / "airdbg" return { "root": root, "state": root / "state.json", "sessions_dir": root / "sessions", "snapshots_dir": root / "snapshots", } def _ensure_dirs(paths: dict[str, Path]) -> None: for key in ("sessions_dir", "snapshots_dir"): paths[key].mkdir(parents=True, exist_ok=True) def start_session(project_root: Path, task_id: str) -> dict: paths = _paths(project_root) _ensure_dirs(paths) session_id = f"{task_id}-{session_stamp()}" session_state = { "sessionId": session_id, "taskId": task_id, "currentStep": "confirm_symptoms", "startedAt": now_iso(), "stepsCompleted": [], "collectedEvidence": [], "evidence": {}, "result": None, } session_path = paths["sessions_dir"] / f"{session_id}.json" atomic_json_write(session_path, session_state) log = EventLog(event_log_path(project_root)) log.emit(DEBUG_SESSION, {"sessionId": session_id, "taskId": task_id, "action": "started"}) return { "sessionId": session_id, "sessionPath": str(session_path), "currentStep": "confirm_symptoms", "steps": DBG_STEPS, } def get_step(session_path: Path) -> str: session = safe_json_load(session_path) if not session or not isinstance(session, dict): return "confirm_symptoms" return session.get("currentStep", "confirm_symptoms") def advance_step(session_path: Path, evidence: dict) -> str: session = safe_json_load(session_path) if not session or not isinstance(session, dict): raise ValueError("invalid session") current = session.get("currentStep", "confirm_symptoms") current_idx = DBG_STEPS.index(current) if current in DBG_STEPS else 0 # 验证当前步骤需要的证据 required_evidence = _required_evidence_for_step(current) if required_evidence: missing = [k for k in required_evidence if k not in evidence] if missing: raise ValueError(f"step '{current}' requires evidence: {missing}") # 先读后写门控:fix 步骤前必须已有取证记录 if current == "fix": collected = session.get("collectedEvidence", []) if not collected: raise WorkflowViolation( "未执行任何取证行为,禁止修改代码。" "请先至少完成以下一项:截图、抓包、静态分析、日志分析、代码追踪、复现步骤。" ) session["stepsCompleted"].append({"step": current, "evidence": evidence, "completedAt": now_iso()}) # 累积取证记录(confirm_symptoms, load_context, reproduce, locate_root_cause 都是取证步骤) evidence_steps = {"confirm_symptoms", "load_context", "reproduce", "locate_root_cause"} if current in evidence_steps: session.setdefault("collectedEvidence", []).append(current) next_idx = current_idx + 1 if next_idx < len(DBG_STEPS): session["currentStep"] = DBG_STEPS[next_idx] atomic_json_write(session_path, session) return session["currentStep"] def skip_reproduce(session_path: Path, reason: str) -> str: session = safe_json_load(session_path) if not session or not isinstance(session, dict): raise ValueError("invalid session") if session.get("currentStep") != "reproduce": raise ValueError("can only skip from reproduce step") session["currentStep"] = "locate_root_cause" session["stepsCompleted"].append({"step": "reproduce", "evidence": {"skipped": True, "reason": reason}}) atomic_json_write(session_path, session) return "locate_root_cause" def pre_fix_snapshot(project_root: Path, task_id: str) -> str: """ 修复前创建 git tag 作为回滚点。 V2 改进:只提交当前 task 写集范围内的文件(在 result.filesChanged 中声明)。 """ # 1. 读取 worker result 获取 filesChanged result_path = project_root / "AirPlan" / "state" / "airdo" / "tasks" / task_id / "result.json" if not result_path.exists(): # 无 result 文件,回退到全量提交(但加 warning) return _snapshot_full(project_root, task_id) result = safe_json_load(result_path) or {} files_changed = result.get("filesChanged", []) if not files_changed: # 无写集声明,回退到当前工作目录中的已跟踪文件 files_changed = None # 2. 只 add 这些文件,然后 commit return _snapshot_selective(project_root, task_id, files_changed) def _snapshot_selective(project_root: Path, task_id: str, files: list[str] | None) -> str: """只提交指定的文件列表""" ref = f"airdbg-prefix-{task_id}-{session_stamp()}" try: # git add if files: for f in files: fp = project_root / f if fp.exists(): subprocess.run(["git", "-C", str(project_root), "add", str(fp)], check=True, capture_output=True, timeout=10) # 如果有 staging 的内容则 commit,否则跳过(避免空 commit) result = subprocess.run( ["git", "-C", str(project_root), "commit", "-m", f"AirDbg pre-fix snapshot: {task_id}", "--allow-empty"], capture_output=True, text=True, timeout=30, ) if result.returncode == 0: subprocess.run( ["git", "-C", str(project_root), "tag", ref], check=True, capture_output=True, text=True, timeout=10, ) return ref else: # 没有 staged 内容或 commit 失败 return "" except subprocess.CalledProcessError: return "" def _snapshot_full(project_root: Path, task_id: str) -> str: """全量提交(仅在无 filesChanged 信息时的 fallback,加 warning)""" import logging logger = logging.getLogger(__name__) logger.warning("pre_fix_snapshot: no filesChanged info, falling back to git commit -am") # 这里保留原逻辑但加 comment 说明这是 fallback return _do_git_commit_am(project_root, task_id) def _do_git_commit_am(project_root: Path, task_id: str) -> str: """原始实现,保留用于 fallback""" ref = f"airdbg-prefix-{task_id}-{session_stamp()}" try: subprocess.run( ["git", "-C", str(project_root), "commit", "-am", f"AirDbg pre-fix snapshot: {task_id}", "--allow-empty"], check=True, capture_output=True, text=True, timeout=30, ) subprocess.run( ["git", "-C", str(project_root), "tag", ref], check=True, capture_output=True, text=True, timeout=10, ) except subprocess.CalledProcessError: return "" return ref def _required_evidence_for_step(step: str) -> list[str]: evidence_map = { "confirm_symptoms": ["symptom", "expected", "actual"], "load_context": [], "reproduce": ["reproduction_steps"], "locate_root_cause": ["root_cause_analysis"], "fix": ["fix_description", "files_changed"], "verify": ["validation_result"], "close_out": ["residual_risk", "adr_updates"], } return evidence_map.get(step, []) def main(args) -> None: project_root = Path(args.project).expanduser().resolve() sub = args.sub or "status" paths = _paths(project_root) _ensure_dirs(paths) if sub == "start": result = start_session(project_root, args.task_id) print("airplan_mode=dbg") print(f"session_id={result['sessionId']}") print(f"current_step={result['currentStep']}") print(f"steps={','.join(result['steps'])}") elif sub == "snapshot": ref = pre_fix_snapshot(project_root, args.task_id) print("airplan_mode=dbg") print(f"snapshot_ref={ref}") else: state = safe_json_load(paths["state"]) or {} print("airplan_mode=dbg") print(f"enabled={state.get('enabled', False)}")