""" 并行审查模块 — V2 从 V1 迁移。 分析任务依赖、写集冲突、产出并行组和串行点。 包含三层审查放行标准 (P1-24/P1-25) 和弱模型任务描述优化 (3.2.18)。 """ from __future__ import annotations import re from dataclasses import dataclass, field from pathlib import Path from air_runtime.todo_parser import parse_tasks # 3.2.18: 弱模型优化 — 歧义动词检测 AMBIGUOUS_VERBS = { "清理": "歧义——可能是删除、重构、或移除依赖", "优化": "歧义——可能是性能优化、代码重构、或简化逻辑", "整理": "歧义——可能是格式化、重命名、或删除", "更新": "歧义——可能是修改现有代码、或替换为新实现", } SAFE_VERBS = { "重构": "修改实现但保持外部接口不变", "新增": "添加新功能,不修改现有代码", "删除": "移除指定文件或函数(必须列出具体目标)", "修改": "修改指定文件的具体部分(必须指明改什么)", "保留": "明确标记为不可修改的文件/目录", } # 3.2.9b: 禁止使用的降级语言模式 FORBIDDEN_DEGRADATION_PATTERNS = [ "兜底方案", "fallback", "先这样做", "先这样跑通", "先这样实现", "以后再删", "以后再补", "以后再改", "以后再优化", "先回退", "临时方案", "temporary workaround", "hack 一下", "quick fix", "MVP 先上", "先 ship 再迭代", ] # AirRvr 审查放行标准 — 禁止的表面理由 FORBIDDEN_PASS_REASONS = [ "测试 pass", "测试全绿", "all tests passed", "实现存在", "函数存在", "文件已创建", "编译通过", "无报错", "能跑通", "功能可用", ] @dataclass class ParallelGroup: name: str task_ids: list[str] reason: str = "" def to_dict(self) -> dict: return {"name": self.name, "task_ids": self.task_ids, "reason": self.reason} @dataclass class Conflict: task_a: str task_b: str reason: str = "" def to_dict(self) -> dict: return {"task_a": self.task_a, "task_b": self.task_b, "reason": self.reason} @dataclass class ReviewResult: parallel_groups: list[ParallelGroup] = field(default_factory=list) conflicts: list[Conflict] = field(default_factory=list) serialization_points: list[dict] = field(default_factory=list) edges: list[dict] = field(default_factory=list) def to_dict(self) -> dict: return { "parallelGroups": [{"name": g.name, "task_ids": g.task_ids, "reason": g.reason} for g in self.parallel_groups], "conflicts": [{"task_a": c.task_a, "task_b": c.task_b, "reason": c.reason} for c in self.conflicts], "serializationPoints": self.serialization_points, "edges": self.edges, } @classmethod def from_dict(cls, data: dict) -> ReviewResult: return cls( parallel_groups=[ParallelGroup(**g) for g in data.get("parallelGroups", [])], conflicts=[Conflict(**c) for c in data.get("conflicts", [])], serialization_points=data.get("serializationPoints", []), edges=data.get("edges", []), ) def build_parallel_review(todo_path: Path) -> ReviewResult: """分析 todo.md,产出并行组和冲突。""" tasks = parse_tasks(todo_path) result = ReviewResult() # 解析依赖:task 文本中的 "依赖 T-xxx" 或 Done When 中的引用 edges = [] for t in tasks: deps = re.findall(r"T-\d+[a-z]*", t.done_when) deps.extend(re.findall(r"依赖\s+(T-\d+[a-z]*)", t.task)) for dep in deps: if dep != t.task_id: edges.append({"source": dep, "target": t.task_id, "kind": "dependency"}) result.edges.append({"source": dep, "target": t.task_id, "kind": "dependency"}) # 写集冲突检测 file_map: dict[str, list[str]] = {} for t in tasks: if t.files_dirs: files = [f.strip() for f in t.files_dirs.split(",")] for f in files: file_map.setdefault(f, []).append(t.task_id) conflicts = [] for fpath, tid_list in file_map.items(): for i, tid_a in enumerate(tid_list): for tid_b in tid_list[i + 1:]: conflicts.append(Conflict(tid_a, tid_b, f"shared file: {fpath}")) result.conflicts = conflicts # 串行点:同文件不同任务的依赖链 for fpath, tid_list in file_map.items(): if len(tid_list) > 1: for tid in tid_list[1:]: result.serialization_points.append({ "taskId": tid, "reasons": [f"serialized with {tid_list[0]} due to shared file: {fpath}"], }) # 并行组:入度为 0 的任务 target_count = {e["target"] for e in edges} ready = [t.task_id for t in tasks if t.task_id not in target_count and t.status == "TODO"] if ready: result.parallel_groups.append(ParallelGroup( name="wave-1", task_ids=ready, reason="no dependencies on other TODO tasks", )) return result def render_review_markdown(review: ReviewResult) -> str: lines = ["# AirArc Parallel Review", ""] lines.append(f"## Summary") lines.append(f"- Parallel groups: {len(review.parallel_groups)}") lines.append(f"- Conflicts: {len(review.conflicts)}") lines.append(f"- Serialization points: {len(review.serialization_points)}") lines.append("") lines.append("## Parallel Groups") for g in review.parallel_groups: lines.append(f"### {g.name}") lines.append(f"Reason: {g.reason}") lines.append(f"Tasks: {', '.join(g.task_ids)}") lines.append("") lines.append("## Conflicts") for c in review.conflicts: lines.append(f"- {c.task_a} <-> {c.task_b}: {c.reason}") return "\n".join(lines) # ── 3.2.18: 弱模型任务描述优化 ── def validate_task_description(task_id: str, task_text: str) -> list[str]: """检测任务描述中的歧义词并建议替换。返回警告列表。""" warnings = [] for verb, explanation in AMBIGUOUS_VERBS.items(): if verb in task_text: safe_suggestions = "、".join(SAFE_VERBS.keys()) warnings.append( f"[{task_id}] 任务描述包含歧义词「{verb}」({explanation})," f"请拆分为具体操作({safe_suggestions})" ) return warnings def check_forbidden_degradation(text: str) -> list[str]: """检测文本中的降级语言模式,返回匹配到的模式列表。 检测时归一化空白字符以处理中文/英文间距变化。""" import re normalized = re.sub(r'\s+', '', text.lower()) found = [] for pattern in FORBIDDEN_DEGRADATION_PATTERNS: normalized_pattern = re.sub(r'\s+', '', pattern.lower()) if normalized_pattern in normalized: found.append(pattern) return found # ── AirRvr 三层审查放行标准 ── @dataclass class ReviewVerdict: """三层审查结果。""" verdict: str # pass | fail | conditional-pass layer1_pass: bool = False # Code-to-Design layer2_pass: bool = False # 静态分析 layer3_pass: bool = False # 测试 divergent_entries: list[str] = field(default_factory=list) missing_entries: list[str] = field(default_factory=list) static_findings: list[str] = field(default_factory=list) test_failures: list[str] = field(default_factory=list) pass_reason: str = "" def evaluate_review_pass( code_to_design_table: list[dict], sdb_report: dict | None = None, test_results: dict | None = None, xdb_evidence: dict | None = None, design_spec: dict | None = None, ) -> ReviewVerdict: """三层审查放行评估(不可降级)。 Layer 1 (最高优先级): Code-to-Design 逐行对照 Layer 2: 静态分析通过 Layer 3: 测试通过 """ divergent = [] missing = [] for entry in code_to_design_table: status = entry.get("status", "") if status == "divergent": divergent.append(entry.get("designPoint", entry.get("requirement", "?"))) elif status == "missing": missing.append(entry.get("designPoint", entry.get("requirement", "?"))) layer1_pass = len(divergent) == 0 and len(missing) == 0 # Layer 2: 静态分析 static_findings = [] if sdb_report: for finding in sdb_report.get("findings", []): if finding.get("severity") in ("critical", "high"): static_findings.append(finding.get("description", str(finding))) layer2_pass = len(static_findings) == 0 # Layer 3: 测试 test_failures = [] if test_results: for suite in test_results.get("suites", []): if suite.get("status") != "pass": test_failures.append(suite.get("name", "unknown")) layer3_pass = len(test_failures) == 0 # GUI 任务额外检查 if xdb_evidence and design_spec: if not xdb_evidence.get("matches_design", True): layer3_pass = False test_failures.append("xdb screenshot does not match design spec") # 判定 if not layer1_pass: verdict = "fail" elif layer2_pass and layer3_pass: verdict = "pass" else: verdict = "conditional-pass" return ReviewVerdict( verdict=verdict, layer1_pass=layer1_pass, layer2_pass=layer2_pass, layer3_pass=layer3_pass, divergent_entries=divergent, missing_entries=missing, static_findings=static_findings, test_failures=test_failures, ) def is_forbidden_pass_reason(reason: str) -> bool: """检查审查通过理由是否使用了禁止的表面原因。 检测时归一化空白字符以处理间距变化。""" import re normalized_reason = re.sub(r'\s+', '', reason.lower()) for forbidden in FORBIDDEN_PASS_REASONS: normalized_forbidden = re.sub(r'\s+', '', forbidden.lower()) if normalized_forbidden in normalized_reason: return True return False