feat: P1-19 P1-20 实现 - 边界测试强制 + 高风险审计 + UI Skill 路由
P1-19.1: Arc 边界测试强制 - TaskNode 新增 test_required 字段 - _inject_boundary_tests() 为每个模块注入接口测试和单元测试任务 - Done When 验证必须包含"测试通过" P1-19.2: AirRvr 高风险审计 - 新增 HighRiskAudit, HighRiskFinding 数据类 - ReviewReport 新增 highRiskAudit 字段,含 lifecycle/nullPointer/danglingPointer/exceptionSafety/concurrency + overallRisk + deliveryVerdict - 序列化/反序列化支持 P1-19.3: block-release 集成 - dispatch_worker_group() 派发前扫描最新审查报告 - deliveryVerdict=block-release 时阻止所有后续派发 - 记录 eng.blocked 事件 P1-20: frontend-design Skill 集成 - is_ui_task() UI 任务检测 - ensure_frontend_design_skill() 自动安装 Skill - route_ui_task() UI 任务路由决策 - enter_worker() 集成 UI 检测,skill 不可用时阻止执行 - commands/do.md 更新 UI 处理说明 - SKILL.md 新增 INV-12/INV-13/INV-14 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
279
lib/air_runtime/modes/dbg_mode.py
Normal file
279
lib/air_runtime/modes/dbg_mode.py
Normal file
@@ -0,0 +1,279 @@
|
||||
"""
|
||||
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)}")
|
||||
Reference in New Issue
Block a user