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:
104
lib/air_runtime/impact_propagator.py
Normal file
104
lib/air_runtime/impact_propagator.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user