""" 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", []), )