Files
AirPlan-V2/lib/air_runtime/review.py
AirPlan a60d1a0c04 fix: Windows兼容性修复 + P1-24弱模型优化 + 3.2.9b禁止降级方案 + AirRvr三层审查放行标准
- lock.py: 跨平台进程锁(Unix fcntl / Windows msvcrt / O_CREAT|O_EXCL降级)
- eng_mode.py/eng_orchestrator.py: hasattr(os, "getloadavg") Windows防护
- arc_mode.py: 路径分隔符 replace("\\", "/") Windows兼容
- deploy_runtime.py: 修复语法错误(清理 import tempfile 残留)
- P1-24(3.2.18): AMBIGUOUS_VERBS歧义词检测 + SAFE_VERBS安全动词 + validate_task_description()
- TaskNode.keep_constraints 保留约束字段 + JSON序列化
- _build_graph_from_todo 返回歧义警告 + Arc自检集成
- 3.2.9b: FORBIDDEN_DEGRADATION_PATTERNS + check_forbidden_degradation()
- AirRvr三层审查放行标准: ReviewVerdict + evaluate_review_pass() + is_forbidden_pass_reason()
- commands/arc.md: 弱模型安全重写(操作类型拆分+保留约束+自检)
- commands/do.md/eng.md/rvr.md: 禁止降级方案 + 三层审查标准
- 测试: 7个新测试 + 74全量通过

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 09:58:29 +08:00

284 lines
9.9 KiB
Python
Executable File
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.
"""
并行审查模块 — 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