feat: AirPlan V2 — 全专家插件强制路由 + 事件系统规范化
P0-8 扩大: do_mode.py finish_worker 全专家插件强制路由 - GUI→XDB, network→NDB, C/C++→SDB, done→Rvr, blocked/failed→Dbg - 证据去重: 已有 xdbSessions/ndbSessions/sdbReports/rvrReviewed 则跳过 P1-GAP17: 事件 emit 规范化 - 新增 7 个事件常量 (TASK_ENTERED, TASK_FINISHED, ENGINE_ENTERED 等) - 全部 emit 调用替换字符串字面量为常量,零残留 - 30 个事件类型常量全部定义且唯一 P1-GAP18: 事件日志原子轮转 - emit 计数器每 128 次检查轮转,避免每次 emit 读文件 - 清除未使用的 _emit_with_completion/_pending_merge_complete - 原子轮转: tempfile+os.replace 保证不损坏 eng 极端接管: 强制调用全部专家插件 (Dbg/XDB/NDB/SDB/Rvr) commands/do.md: 更新为全专家插件路由文档 全量测试: 69 通过, 0 失败 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
214
lib/air_runtime/review_runtime.py
Executable file
214
lib/air_runtime/review_runtime.py
Executable file
@@ -0,0 +1,214 @@
|
||||
"""
|
||||
AirRvr 需求审查运行时 — V2 新增组件。
|
||||
基于原始需求文档对已完成任务进行独立审查,验证交付物与需求的一致性。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from air_runtime.io import atomic_json_write
|
||||
from air_runtime.paths import rvr_state_path
|
||||
from air_runtime.utils import session_stamp
|
||||
|
||||
|
||||
@dataclass
|
||||
class RequirementCoverage:
|
||||
requirement: str
|
||||
status: str # covered | partial | missing
|
||||
evidence: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class CodeToDesignItem:
|
||||
design_item: str
|
||||
implementation_status: str # aligned | divergent | missing
|
||||
code_location: str = ""
|
||||
design_location: str = ""
|
||||
divergence_detail: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class HighRiskFinding:
|
||||
"""P1-19.2: 高风险审计发现项。"""
|
||||
file: str
|
||||
line: int
|
||||
severity: str # critical | high | medium | low
|
||||
issue: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class HighRiskAudit:
|
||||
"""P1-19.2: 高风险审计报告结构。"""
|
||||
lifecycle: list[HighRiskFinding] = field(default_factory=list)
|
||||
nullPointer: list[HighRiskFinding] = field(default_factory=list)
|
||||
danglingPointer: list[HighRiskFinding] = field(default_factory=list)
|
||||
exceptionSafety: list[HighRiskFinding] = field(default_factory=list)
|
||||
concurrency: list[HighRiskFinding] = field(default_factory=list)
|
||||
overallRisk: str = "low" # critical | high | medium | low
|
||||
deliveryVerdict: str = "safe-to-ship" # safe-to-ship | needs-fix | block-release
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReviewReport:
|
||||
task_id: str
|
||||
verdict: str # pass | conditional-pass | fail
|
||||
coverage: list[RequirementCoverage] = field(default_factory=list)
|
||||
intent_alignment: str = "aligned" # aligned | divergent
|
||||
divergence_notes: str = ""
|
||||
regression_risk: str = "none" # none | low | medium | high
|
||||
code_quality: dict = field(default_factory=lambda: {
|
||||
"complexity": "low", "readability": "good", "duplication": "none", "error_handling": "complete",
|
||||
})
|
||||
lifecycle_health: dict = field(default_factory=lambda: {
|
||||
"resource_leak": "none", "connection_management": "proper",
|
||||
"timeout_strategy": "present", "retry_strategy": "present",
|
||||
})
|
||||
runtime_stability: dict = field(default_factory=lambda: {
|
||||
"crash_risk": "none", "race_condition": "none",
|
||||
"memory_leak": "none", "user_impact": "none",
|
||||
})
|
||||
code_to_design_table: list[CodeToDesignItem] = field(default_factory=list)
|
||||
logging_checks: dict = field(default_factory=lambda: {
|
||||
"spdlog_integrated": False,
|
||||
"non_standard_logging": [],
|
||||
"debug_release_switch": False,
|
||||
"critical_path_logging": False,
|
||||
"unified_format": False,
|
||||
})
|
||||
high_risk_audit: HighRiskAudit = field(default_factory=HighRiskAudit) # P1-19.2
|
||||
recommendations: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
class ReviewRuntime:
|
||||
"""AirRvr 审查运行时 — 管理审查会话和报告持久化。"""
|
||||
|
||||
REVIEW_MODES = ["per-task", "per-wave", "per-milestone"]
|
||||
|
||||
def __init__(self, project_root: Path):
|
||||
self._project_root = project_root
|
||||
self._state_dir = rvr_state_path(project_root).parent
|
||||
self._reviews_dir = self._state_dir / "reviews"
|
||||
self._reviews_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def save_report(self, report: ReviewReport) -> Path:
|
||||
report_path = self._reviews_dir / f"{report.task_id}-{session_stamp()}.json"
|
||||
atomic_json_write(report_path, self._report_to_dict(report))
|
||||
return report_path
|
||||
|
||||
def load_report(self, task_id: str, timestamp: str) -> ReviewReport | None:
|
||||
from air_runtime.io import safe_json_load
|
||||
report_path = self._reviews_dir / f"{task_id}-{timestamp}.json"
|
||||
data = safe_json_load(report_path)
|
||||
if data:
|
||||
return self._dict_to_report(data)
|
||||
return None
|
||||
|
||||
def get_integration_verdict(self, report: ReviewReport) -> str:
|
||||
"""与 AirEng 集成:pass → 允许合并,conditional-pass → 合并但记录遗留项,fail → 阻止合并。"""
|
||||
return report.verdict
|
||||
|
||||
def get_verdict_for_task(self, task_id: str) -> dict:
|
||||
"""从持久化的 review report 读 verdict,返回 dict 含 verdict/residual/reportPath。
|
||||
没有 report 时返回 {"verdict": "pass", "reportPath": ""}(默认放行)。"""
|
||||
from air_runtime.io import safe_json_load
|
||||
# reports/ 是 AirEng 约定的存放路径(验证脚本和 eng_mode 期望的位置)
|
||||
reports_dir = self._state_dir / "reports"
|
||||
report_path = reports_dir / f"{task_id}.json"
|
||||
if not report_path.exists():
|
||||
# 兼容旧路径 reviews/ 下的 {task_id}-{ts}.json,找最新一份
|
||||
alt = self._reviews_dir
|
||||
if alt.exists():
|
||||
candidates = sorted(alt.glob(f"{task_id}-*.json"), key=lambda p: p.stat().st_mtime, reverse=True)
|
||||
if candidates:
|
||||
report_path = candidates[0]
|
||||
if not report_path.exists():
|
||||
return {"verdict": "pass", "reportPath": "", "residual": [], "deliveryVerdict": "safe-to-ship"}
|
||||
report = safe_json_load(report_path)
|
||||
if not report or not isinstance(report, dict):
|
||||
return {"verdict": "pass", "reportPath": str(report_path), "residual": [], "deliveryVerdict": "safe-to-ship"}
|
||||
return {
|
||||
"verdict": report.get("verdict", "pass"),
|
||||
"residual": report.get("residual", []),
|
||||
"reportPath": str(report_path),
|
||||
"summary": report.get("summary", ""),
|
||||
"deliveryVerdict": report.get("highRiskAudit", {}).get("deliveryVerdict", "safe-to-ship"), # P1-19.2
|
||||
}
|
||||
|
||||
def check_invalidated_cleanup(self, invalidated_task_ids: list[str]) -> dict:
|
||||
"""P1-21: 检查 INVALIDATED 任务的代码是否已清理(无残留)。"""
|
||||
from air_runtime.io import safe_json_load
|
||||
residual = []
|
||||
for tid in invalidated_task_ids:
|
||||
# 检查是否有残留的 result 文件(说明旧代码未被 revert)
|
||||
result_dir = self._state_dir.parent / "airdo" / "tasks" / tid
|
||||
if result_dir.exists():
|
||||
result_file = result_dir / "result.json"
|
||||
if result_file.exists():
|
||||
data = safe_json_load(result_file)
|
||||
if data and data.get("status") == "done":
|
||||
residual.append({"taskId": tid, "reason": "done result still exists — code may not be reverted"})
|
||||
return {
|
||||
"cleaned": len(residual) == 0,
|
||||
"residualCount": len(residual),
|
||||
"residualDetails": residual,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _report_to_dict(report: ReviewReport) -> dict:
|
||||
# P1-19.2: highRiskAudit 序列化
|
||||
hra = report.high_risk_audit
|
||||
high_risk_audit_dict = {
|
||||
"lifecycle": [{"file": f.file, "line": f.line, "severity": f.severity, "issue": f.issue} for f in hra.lifecycle],
|
||||
"nullPointer": [{"file": f.file, "line": f.line, "severity": f.severity, "issue": f.issue} for f in hra.nullPointer],
|
||||
"danglingPointer": [{"file": f.file, "line": f.line, "severity": f.severity, "issue": f.issue} for f in hra.danglingPointer],
|
||||
"exceptionSafety": [{"file": f.file, "line": f.line, "severity": f.severity, "issue": f.issue} for f in hra.exceptionSafety],
|
||||
"concurrency": [{"file": f.file, "line": f.line, "severity": f.severity, "issue": f.issue} for f in hra.concurrency],
|
||||
"overallRisk": hra.overallRisk,
|
||||
"deliveryVerdict": hra.deliveryVerdict,
|
||||
}
|
||||
return {
|
||||
"taskId": report.task_id,
|
||||
"verdict": report.verdict,
|
||||
"coverage": [c.__dict__ for c in report.coverage],
|
||||
"intentAlignment": report.intent_alignment,
|
||||
"divergenceNotes": report.divergence_notes,
|
||||
"regressionRisk": report.regression_risk,
|
||||
"codeQuality": report.code_quality,
|
||||
"lifecycleHealth": report.lifecycle_health,
|
||||
"runtimeStability": report.runtime_stability,
|
||||
"codeToDesignTable": [c.__dict__ for c in report.code_to_design_table],
|
||||
"loggingChecks": report.logging_checks,
|
||||
"highRiskAudit": high_risk_audit_dict,
|
||||
"recommendations": report.recommendations,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _dict_to_report(data: dict) -> ReviewReport:
|
||||
# P1-19.2: highRiskAudit 反序列化
|
||||
hra_data = data.get("highRiskAudit", {})
|
||||
high_risk_audit = HighRiskAudit(
|
||||
lifecycle=[HighRiskFinding(**f) for f in hra_data.get("lifecycle", [])],
|
||||
nullPointer=[HighRiskFinding(**f) for f in hra_data.get("nullPointer", [])],
|
||||
danglingPointer=[HighRiskFinding(**f) for f in hra_data.get("danglingPointer", [])],
|
||||
exceptionSafety=[HighRiskFinding(**f) for f in hra_data.get("exceptionSafety", [])],
|
||||
concurrency=[HighRiskFinding(**f) for f in hra_data.get("concurrency", [])],
|
||||
overallRisk=hra_data.get("overallRisk", "low"),
|
||||
deliveryVerdict=hra_data.get("deliveryVerdict", "safe-to-ship"),
|
||||
)
|
||||
return ReviewReport(
|
||||
task_id=data.get("taskId", ""),
|
||||
verdict=data.get("verdict", "fail"),
|
||||
coverage=[RequirementCoverage(**c) for c in data.get("coverage", [])],
|
||||
intent_alignment=data.get("intentAlignment", "aligned"),
|
||||
divergence_notes=data.get("divergenceNotes", ""),
|
||||
regression_risk=data.get("regressionRisk", "none"),
|
||||
code_quality=data.get("codeQuality", {}),
|
||||
lifecycle_health=data.get("lifecycleHealth", {}),
|
||||
runtime_stability=data.get("runtimeStability", {}),
|
||||
code_to_design_table=[CodeToDesignItem(**c) for c in data.get("codeToDesignTable", [])],
|
||||
logging_checks=data.get("loggingChecks", {}),
|
||||
high_risk_audit=high_risk_audit,
|
||||
recommendations=data.get("recommendations", []),
|
||||
)
|
||||
Reference in New Issue
Block a user