Files
AirPlan-V2/lib/air_runtime/modes/dbg_mode.py
AirPlan Team 2c4b3340bf AirPlan V2 initial release — unified scheduler with 12 sub-modes
Merges 8 V1 plugins into 1 unified plugin, adds 4 new components (dep, tst, sec, rvr).
12 sub-modes: arc, eng, do, dbg, xdb, sdb, ndb, ctx, dep, tst, sec, rvr.
L1 code-level guarantees: atomic writes, file locks, evidence gating, forced debug routing,
3-phase arc gate, evidence-first debug gate, Chinese language lock, scheduler boundary.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 16:24:26 +08:00

280 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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 <files>
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)}")