- 新增 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>
105 lines
3.8 KiB
Python
105 lines
3.8 KiB
Python
"""
|
||
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)
|