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:
AirLongDian
2026-06-10 17:06:48 +08:00
commit e73a4da354
54 changed files with 6054 additions and 0 deletions

View File

@@ -0,0 +1,195 @@
"""
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", []),
)