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>
This commit is contained in:
AirPlan
2026-06-15 14:45:41 +08:00
parent a60d1a0c04
commit 9702f1b186
11 changed files with 1351 additions and 82 deletions

View File

@@ -40,12 +40,14 @@ class EdgeChange:
@dataclass
class CascadeReport:
"""P1-21: ADR 变更级联失效报告。"""
"""P1-21: ADR 变更级联失效报告。3.2.17 差异化处理后新增 boundary/impact 字段。"""
invalidated_completed: int = 0
terminated_in_progress: int = 0
cascaded_downstream: int = 0
rollback_ref: str = ""
invalidated_task_ids: list[str] = field(default_factory=list)
boundary_task_ids: list[str] = field(default_factory=list)
blast_radius: str = "" # "implementation" | "interface" | "global_constraint"
@dataclass
@@ -266,32 +268,40 @@ class TaskGraph:
# 排除起点自身
return [tid for tid in visited if tid not in set(task_ids)]
def invalidate_by_adr(self, adr_id: str, delta: PlanDelta) -> CascadeReport:
"""P1-21: ADR 变更时级联失效所有相关任务。"""
def invalidate_by_adr(self, adr_id: str, delta: PlanDelta,
impact_labels: dict[str, str] | None = None) -> CascadeReport:
"""P1-21: ADR 变更时级联失效相关任务。
当 impact_labels 为 None 时使用旧行为(所有引用+下游统一 INVALIDATED
当 impact_labels 提供时使用差异化行为IMPACTED→INVALIDATED, BOUNDARY+DONE→验证, SAFE→保留
"""
affected = self.tasks_by_adr(adr_id)
completed = [t for t in affected if t.status == "DONE"]
in_progress = [t for t in affected if t.status == "DISPATCHED"]
pending = [t for t in affected if t.status == "TODO"]
# 1. 冻结调度
self.dispatch_frozen = True
# 2. 标记已完成任务为 INVALIDATED
if impact_labels is None:
return self._invalidate_uniform(adr_id, delta, affected)
return self._invalidate_differentiated(adr_id, delta, affected, impact_labels)
def _invalidate_uniform(self, adr_id: str, delta: PlanDelta,
affected: list[TaskNode]) -> CascadeReport:
"""旧行为:所有引用 ADR 的任务及其下游统一 INVALIDATED。"""
completed = [t for t in affected if t.status == "DONE"]
in_progress = [t for t in affected if t.status == "DISPATCHED"]
pending = [t for t in affected if t.status == "TODO"]
for t in completed:
t.status = "INVALIDATED"
delta.removed_tasks.append(t.id)
# 3. 标记进行中任务为 INVALIDATED调用方负责中止 Worker
for t in in_progress:
t.status = "INVALIDATED"
delta.removed_tasks.append(t.id)
# 4. 标记 ADR 直接关联的 TODO 任务为 INVALIDATED
for t in pending:
t.status = "INVALIDATED"
delta.removed_tasks.append(t.id)
# 5. 级联失效下游
downstream_ids = self._find_downstream([t.id for t in completed + in_progress + pending])
cascaded = []
for tid in downstream_ids:
@@ -301,7 +311,6 @@ class TaskGraph:
delta.removed_tasks.append(tid)
cascaded.append(tid)
# 6. 回滚快照引用(由调用方在 git revert 后填入)
all_invalidated = [t.id for t in completed + in_progress + pending] + cascaded
return CascadeReport(
@@ -312,6 +321,92 @@ class TaskGraph:
invalidated_task_ids=all_invalidated,
)
def _invalidate_differentiated(self, adr_id: str, delta: PlanDelta,
affected: list[TaskNode],
impact_labels: dict[str, str]) -> CascadeReport:
"""差异化行为:按 IMPACTED/BOUNDARY/SAFE 分别处理每个任务。"""
invalidated_completed = 0
terminated_in_progress = 0
invalidated_ids: list[str] = []
boundary_ids: list[str] = []
from air_runtime.impact_propagator import ImpactLabel
for t in affected:
label = impact_labels.get(t.id, ImpactLabel.SAFE.value)
if label == ImpactLabel.SAFE.value:
continue
if label == ImpactLabel.BOUNDARY.value:
if t.status == "DONE":
t.meta["verification_needed"] = True
boundary_ids.append(t.id)
elif t.status in ("TODO", "BLOCKED"):
t.status = "INVALIDATED"
delta.removed_tasks.append(t.id)
invalidated_ids.append(t.id)
# DISPATCHED + BOUNDARY: wait for completion, no action
elif label == ImpactLabel.IMPACTED.value:
if t.status == "DONE":
t.status = "INVALIDATED"
delta.removed_tasks.append(t.id)
invalidated_ids.append(t.id)
invalidated_completed += 1
elif t.status == "DISPATCHED":
t.status = "INVALIDATED"
delta.removed_tasks.append(t.id)
invalidated_ids.append(t.id)
terminated_in_progress += 1
elif t.status in ("TODO", "BLOCKED"):
t.status = "INVALIDATED"
delta.removed_tasks.append(t.id)
invalidated_ids.append(t.id)
# 级联下游:仅 IMPACTED 任务的下游受影响
impacted_affected = [t for t in affected
if impact_labels.get(t.id) == ImpactLabel.IMPACTED.value]
cascaded_ids: list[str] = []
if impacted_affected:
downstream_ids = self._find_downstream([t.id for t in impacted_affected])
for tid in downstream_ids:
node = self.nodes.get(tid)
if not node:
continue
dlabel = impact_labels.get(tid, ImpactLabel.SAFE.value)
if dlabel == ImpactLabel.SAFE.value:
continue
if dlabel == ImpactLabel.BOUNDARY.value:
if node.status == "DONE":
node.meta["verification_needed"] = True
boundary_ids.append(tid)
elif node.status in ("TODO", "BLOCKED"):
node.status = "INVALIDATED"
delta.removed_tasks.append(tid)
cascaded_ids.append(tid)
invalidated_ids.append(tid)
elif dlabel == ImpactLabel.IMPACTED.value:
if node.status in ("TODO", "DISPATCHED"):
node.status = "INVALIDATED"
delta.removed_tasks.append(tid)
cascaded_ids.append(tid)
invalidated_ids.append(tid)
elif node.status == "DONE":
node.status = "INVALIDATED"
delta.removed_tasks.append(tid)
cascaded_ids.append(tid)
invalidated_ids.append(tid)
invalidated_completed += 1
return CascadeReport(
invalidated_completed=invalidated_completed,
terminated_in_progress=terminated_in_progress,
cascaded_downstream=len(cascaded_ids),
rollback_ref=delta.rollback_ref,
invalidated_task_ids=invalidated_ids,
boundary_task_ids=boundary_ids,
)
def unfreeze_dispatch(self) -> None:
"""P1-21: 解冻调度,在 Arc 重新生成受影响任务后调用。"""
self.dispatch_frozen = False