feat: P1-21 ADR 变更级联失效 — 架构变更自动失效已完成任务
- TaskNode 新增 adr_refs 字段(ADR→任务溯源链) - 新增 INVALIDATED 状态,允许覆盖 DONE/DISPATCHED - TaskGraph.invalidate_by_adr() 级联失效 + BFS 下游传播 - PlanDelta 新增 rollback_ref(回滚快照引用) - CascadeReport 数据结构(失效统计 + 任务ID列表) - Eng dispatch_frozen 冻结调度,ready_tasks() 返回空 - handle_adr_invalidation() 10步处理流程(含 git revert) - unfreeze_after_replan() Arc 重新规划后解冻 - AirRvr check_invalidated_cleanup() 检查旧代码残留 - INV-15 + L1 保障项 15 写入 SKILL.md - 11 项功能测试全通过,含 ffmpeg→gstreamer 完整场景 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ from typing import Any
|
||||
@dataclass
|
||||
class TaskNode:
|
||||
id: str
|
||||
status: str = "TODO" # TODO | DISPATCHED | DONE | BLOCKED
|
||||
status: str = "TODO" # TODO | DISPATCHED | DONE | BLOCKED | INVALIDATED
|
||||
task: str = ""
|
||||
files_dirs: str = ""
|
||||
done_when: str = ""
|
||||
@@ -21,6 +21,7 @@ class TaskNode:
|
||||
write_set: list[str] = field(default_factory=list)
|
||||
meta: dict[str, Any] = field(default_factory=dict)
|
||||
test_required: bool = False # P1-19.1: 边界测试强制标记
|
||||
adr_refs: list[str] = field(default_factory=list) # P1-21: ADR→任务溯源链
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -36,6 +37,16 @@ class EdgeChange:
|
||||
removed: list[Edge] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CascadeReport:
|
||||
"""P1-21: ADR 变更级联失效报告。"""
|
||||
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)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanDelta:
|
||||
"""Arc 重规划产出的增量差异,替代全量覆盖 todo.md。"""
|
||||
@@ -43,6 +54,7 @@ class PlanDelta:
|
||||
added_tasks: list[TaskNode] = field(default_factory=list)
|
||||
modified_tasks: list[TaskNode] = field(default_factory=list)
|
||||
edge_changes: EdgeChange = field(default_factory=EdgeChange)
|
||||
rollback_ref: str = "" # P1-21: 回滚快照引用
|
||||
|
||||
|
||||
class TaskGraph:
|
||||
@@ -51,6 +63,7 @@ class TaskGraph:
|
||||
def __init__(self):
|
||||
self.nodes: dict[str, TaskNode] = {}
|
||||
self.edges: list[Edge] = []
|
||||
self.dispatch_frozen: bool = False # P1-21: 调度冻结
|
||||
|
||||
def add_node(self, node: TaskNode) -> None:
|
||||
self.nodes[node.id] = node
|
||||
@@ -90,7 +103,9 @@ class TaskGraph:
|
||||
self.nodes[edge.source].out_edges.append(edge.target)
|
||||
|
||||
def ready_tasks(self) -> list[str]:
|
||||
"""返回当前入度为 0 且状态为 TODO 的任务。"""
|
||||
"""返回当前入度为 0 且状态为 TODO 的任务。调度冻结时返回空。"""
|
||||
if self.dispatch_frozen:
|
||||
return []
|
||||
return [nid for nid, n in self.nodes.items() if n.in_degree == 0 and n.status == "TODO"]
|
||||
|
||||
def diff(self, other: TaskGraph) -> PlanDelta:
|
||||
@@ -138,6 +153,7 @@ class TaskGraph:
|
||||
graph = cls()
|
||||
if not data or not isinstance(data, dict):
|
||||
return graph
|
||||
graph.dispatch_frozen = data.get("dispatchFrozen", False)
|
||||
for nid, nd in data.get("nodes", {}).items():
|
||||
graph.nodes[nid] = TaskNode(
|
||||
id=nd.get("id", nid),
|
||||
@@ -149,6 +165,7 @@ class TaskGraph:
|
||||
out_edges=list(nd.get("outEdges", [])),
|
||||
write_set=list(nd.get("writeSet", [])),
|
||||
test_required=nd.get("testRequired", False),
|
||||
adr_refs=list(nd.get("adrRefs", [])),
|
||||
)
|
||||
for ed in data.get("edges", []):
|
||||
graph.edges.append(Edge(
|
||||
@@ -205,7 +222,8 @@ class TaskGraph:
|
||||
if node.id in self.nodes:
|
||||
existing_status = self.nodes[node.id].status
|
||||
self.nodes[node.id] = node
|
||||
if existing_status in ("DISPATCHED", "DONE"):
|
||||
# INVALIDATED 可覆盖 DONE/DISPATCHED(P1-21: ADR 级联失效)
|
||||
if existing_status in ("DISPATCHED", "DONE") and node.status != "INVALIDATED":
|
||||
self.nodes[node.id].status = existing_status
|
||||
|
||||
def _remove_edge(self, edge: Edge) -> None:
|
||||
@@ -220,3 +238,70 @@ class TaskGraph:
|
||||
self.nodes[edge.target].in_degree += 1
|
||||
if edge.source in self.nodes:
|
||||
self.nodes[edge.source].out_edges.append(edge.target)
|
||||
|
||||
# P1-21: ADR 级联失效
|
||||
|
||||
def tasks_by_adr(self, adr_id: str) -> list[TaskNode]:
|
||||
"""查找所有引用指定 ADR 的任务(含已完成)。"""
|
||||
return [n for n in self.nodes.values() if adr_id in n.adr_refs]
|
||||
|
||||
def _find_downstream(self, task_ids: list[str]) -> list[str]:
|
||||
"""BFS 遍历下游依赖任务。"""
|
||||
visited: set[str] = set()
|
||||
queue = list(task_ids)
|
||||
while queue:
|
||||
current = queue.pop(0)
|
||||
if current in visited:
|
||||
continue
|
||||
visited.add(current)
|
||||
node = self.nodes.get(current)
|
||||
if node:
|
||||
for target in node.out_edges:
|
||||
if target not in visited:
|
||||
queue.append(target)
|
||||
# 排除起点自身
|
||||
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 变更时级联失效所有相关任务。"""
|
||||
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"]
|
||||
|
||||
# 1. 冻结调度
|
||||
self.dispatch_frozen = True
|
||||
|
||||
# 2. 标记已完成任务为 INVALIDATED
|
||||
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. 级联失效下游
|
||||
downstream_ids = self._find_downstream([t.id for t in completed + in_progress])
|
||||
cascaded = []
|
||||
for tid in downstream_ids:
|
||||
node = self.nodes.get(tid)
|
||||
if node and node.status in ("TODO", "DISPATCHED"):
|
||||
node.status = "INVALIDATED"
|
||||
delta.removed_tasks.append(tid)
|
||||
cascaded.append(tid)
|
||||
|
||||
# 5. 回滚快照引用(由调用方在 git revert 后填入)
|
||||
all_invalidated = [t.id for t in completed + in_progress] + cascaded
|
||||
|
||||
return CascadeReport(
|
||||
invalidated_completed=len(completed),
|
||||
terminated_in_progress=len(in_progress),
|
||||
cascaded_downstream=len(cascaded),
|
||||
rollback_ref=delta.rollback_ref,
|
||||
invalidated_task_ids=all_invalidated,
|
||||
)
|
||||
|
||||
def unfreeze_dispatch(self) -> None:
|
||||
"""P1-21: 解冻调度,在 Arc 重新生成受影响任务后调用。"""
|
||||
self.dispatch_frozen = False
|
||||
|
||||
Reference in New Issue
Block a user