Files
AirPlan-V2/lib/air_runtime/review_runtime.py
AirLongDian e73a4da354 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>
2026-06-10 17:06:48 +08:00

196 lines
8.8 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.
"""
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
}
@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", []),
)