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