Files
AirPlan-V2/lib/air_runtime/change_classifier.py
AirPlan 9702f1b186 feat: 3.2.17中途变更处理(git驱动) + 3.2.9a Worker git强制 + 3.2.5 Phase7 squash merge + Arc git初始化检测
- 新增 ChangeClassifier(爆炸半径分类:IMPLEMENTATION/INTERFACE/GLOBAL_CONSTRAINT)
- 新增 ImpactPropagator(BFS影响传播:IMPACTED/BOUNDARY/SAFE差异化标记)
- task_graph.py:invalidate_by_adr()差异化失效 + CascadeReport扩展字段(向后兼容)
- eng_mode.py:三阶段差异化流程(分类→传播→失效→git操作→验证任务→重规划)
- eng_mode.py:_git_squash_merge_and_tag() + Phase 7 集成
- do_mode.py:_ensure_all_committed() Worker git操作强制
- adr_watcher.py:内容快照 + get_content_for_classification()
- events.py:ADR_CLASSIFIED/IMPACT_PROPAGATED/BOUNDARY_VERIFICATION_GENERATED
- partial_replanner.py:replan_with_constraints() + generate_verification_tasks()
- project_bootstrap.py:ensure_git_initialized() Arc规划前检测
- test_p1_21_phase2.py:21个新测试,95个全量测试0失败

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-15 14:45:41 +08:00

102 lines
4.1 KiB
Python
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.
"""
ADR 变更爆炸半径分类 — P1-21 / 3.2.17 Phase 1 组件。
将 ADR 内容变更按影响范围分为 IMPLEMENTATION / INTERFACE / GLOBAL_CONSTRAINT 三级。
"""
from __future__ import annotations
import enum
from dataclasses import dataclass, field
from pathlib import Path
class BlastRadius(enum.Enum):
IMPLEMENTATION = "implementation"
INTERFACE = "interface"
GLOBAL_CONSTRAINT = "global_constraint"
@dataclass
class ChangeClassification:
adr_id: str
blast_radius: BlastRadius
reason: str = ""
changed_sections: list[str] = field(default_factory=list)
class ChangeClassifier:
"""基于关键词启发式的 ADR 变更爆炸半径分类。
分类规则:
- IMPLEMENTATION: 仅实现细节变更不影响公开接口。关键词implementation detail, can use, for example, internal, private
- INTERFACE: 公开 API/合约变更。关键词interface, api, contract, signature, must implement, public header, exported, caller
- GLOBAL_CONSTRAINT: 跨模块约束变更。关键词must, shall, all modules, coding standard, every module, entire system, global policy
回退策略:内容不足无法确定时回退到 INTERFACE保守宁可多失效不少失效
"""
_INTERFACE_KEYWORDS = [
"interface", "api", "contract", "signature", "must implement",
"public header", "exported", "caller",
]
_GLOBAL_KEYWORDS = [
"must", "shall", "all modules", "coding standard",
"every module", "entire system", "global policy",
]
_IMPLEMENTATION_KEYWORDS = [
"implementation detail", "can use", "for example",
"internally", "private", "optional",
]
def classify(self, adr_id: str, old_content: str | None,
new_content: str | None) -> ChangeClassification:
if new_content is None:
return ChangeClassification(
adr_id=adr_id, blast_radius=BlastRadius.GLOBAL_CONSTRAINT,
reason="ADR deleted — all dependent tasks must be invalidated",
)
if old_content is None:
return ChangeClassification(
adr_id=adr_id, blast_radius=BlastRadius.IMPLEMENTATION,
reason="new ADR — no existing tasks to invalidate",
)
text = new_content.lower()
global_hits = sum(1 for kw in self._GLOBAL_KEYWORDS if kw in text)
interface_hits = sum(1 for kw in self._INTERFACE_KEYWORDS if kw in text)
impl_hits = sum(1 for kw in self._IMPLEMENTATION_KEYWORDS if kw in text)
if global_hits >= 2:
return ChangeClassification(
adr_id=adr_id, blast_radius=BlastRadius.GLOBAL_CONSTRAINT,
reason=f"global constraint keywords matched ({global_hits} hits)",
)
if interface_hits >= 2:
return ChangeClassification(
adr_id=adr_id, blast_radius=BlastRadius.INTERFACE,
reason=f"interface keywords matched ({interface_hits} hits)",
)
if interface_hits >= 1:
return ChangeClassification(
adr_id=adr_id, blast_radius=BlastRadius.INTERFACE,
reason="interface keyword matched — conservative classification",
)
if impl_hits >= interface_hits and impl_hits > 0:
return ChangeClassification(
adr_id=adr_id, blast_radius=BlastRadius.IMPLEMENTATION,
reason=f"implementation keywords matched ({impl_hits} hits)",
)
# 无法确定 → 保守回退
return ChangeClassification(
adr_id=adr_id, blast_radius=BlastRadius.INTERFACE,
reason="inconclusive — defaulting to INTERFACE (conservative)",
)
def classify_from_files(self, adr_id: str, old_path: Path | None,
new_path: Path | None) -> ChangeClassification:
old_content = old_path.read_text(encoding="utf-8") if old_path and old_path.exists() else None
new_content = new_path.read_text(encoding="utf-8") if new_path and new_path.exists() else None
return self.classify(adr_id, old_content, new_content)