Files
AirPlan-V2/lib/air_runtime/impact_propagator.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

105 lines
3.8 KiB
Python
Raw Permalink 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.
"""
BFS 影响传播 — P1-21 / 3.2.17 Phase 1 组件。
根据 BlastRadius 在任务 DAG 中传播影响,标记每个任务为 IMPACTED / BOUNDARY / SAFE。
"""
from __future__ import annotations
import enum
from collections import deque
from dataclasses import dataclass, field
from air_runtime.change_classifier import BlastRadius
from air_runtime.task_graph import TaskGraph
class ImpactLabel(enum.Enum):
IMPACTED = "impacted"
BOUNDARY = "boundary"
SAFE = "safe"
@dataclass
class PropagationResult:
impacted: list[str] = field(default_factory=list)
boundary: list[str] = field(default_factory=list)
safe: list[str] = field(default_factory=list)
class ImpactPropagator:
"""BFS 影响传播,基于 BlastRadius 差异化标记。
传播规则:
- IMPLEMENTATION: 仅直接引用 ADR 的任务 → IMPACTED其他 → SAFE不下游传播
- INTERFACE: 直接引用 → IMPACTED下游 1 跳邻居 → BOUNDARY其他 → SAFE
- GLOBAL_CONSTRAINT: 全图 → IMPACTED
结果存储在 TaskNode.meta["impact_label"],不修改 status 字段(正交)。
"""
_META_KEY = "impact_label"
def propagate(self, graph: TaskGraph, adr_id: str,
blast_radius: BlastRadius) -> PropagationResult:
self.clear_labels(graph)
tasks_ref_adr = [n.id for n in graph.nodes.values() if adr_id in n.adr_refs]
all_ids = set(graph.nodes.keys())
if blast_radius == BlastRadius.GLOBAL_CONSTRAINT:
for nid in all_ids:
graph.nodes[nid].meta[self._META_KEY] = ImpactLabel.IMPACTED.value
return PropagationResult(
impacted=list(all_ids), boundary=[], safe=[],
)
if blast_radius == BlastRadius.IMPLEMENTATION:
impacted = list(tasks_ref_adr)
safe = [nid for nid in all_ids if nid not in impacted]
for nid in impacted:
graph.nodes[nid].meta[self._META_KEY] = ImpactLabel.IMPACTED.value
for nid in safe:
graph.nodes[nid].meta[self._META_KEY] = ImpactLabel.SAFE.value
return PropagationResult(impacted=impacted, boundary=[], safe=safe)
# INTERFACE: direct refs → IMPACTED, 1-hop downstream → BOUNDARY, rest → SAFE
impacted = set(tasks_ref_adr)
# BFS 收集下游 1 跳邻居(仅从 IMPACTED 出发,排除自身)
boundary = set()
for nid in list(impacted):
node = graph.nodes.get(nid)
if node:
for target in node.out_edges:
if target not in impacted:
boundary.add(target)
safe = all_ids - impacted - boundary
for nid in impacted:
graph.nodes[nid].meta[self._META_KEY] = ImpactLabel.IMPACTED.value
for nid in boundary:
graph.nodes[nid].meta[self._META_KEY] = ImpactLabel.BOUNDARY.value
for nid in safe:
graph.nodes[nid].meta[self._META_KEY] = ImpactLabel.SAFE.value
return PropagationResult(
impacted=list(impacted), boundary=list(boundary), safe=list(safe),
)
def impacted_tasks(self, graph: TaskGraph) -> list[str]:
return [nid for nid, n in graph.nodes.items()
if n.meta.get(self._META_KEY) == ImpactLabel.IMPACTED.value]
def boundary_tasks(self, graph: TaskGraph) -> list[str]:
return [nid for nid, n in graph.nodes.items()
if n.meta.get(self._META_KEY) == ImpactLabel.BOUNDARY.value]
def safe_tasks(self, graph: TaskGraph) -> list[str]:
return [nid for nid, n in graph.nodes.items()
if n.meta.get(self._META_KEY) == ImpactLabel.SAFE.value]
def clear_labels(self, graph: TaskGraph) -> None:
for node in graph.nodes.values():
node.meta.pop(self._META_KEY, None)