mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-30 19:02:16 +00:00
- Rewrite .github/workflows/desktop.yml: 2-job pipeline (validate + build-and-release) with rolling desktop-latest pre-release on push to main and stable desktop-v* releases - Add UpdateChecker component: checks for updates on startup + every 30 min, background download with progress bar, one-click relaunch - Configure Tauri updater: endpoints pointing to desktop-latest release, pubkey placeholder - Add tauri-plugin-process for relaunch support (Cargo.toml, lib.rs, package.json) - Add macOS Entitlements.plist for notarization (network + file access, no sandbox) - Add scripts/bump-desktop-version.sh for atomic version bumps across 3 config files - Add desktop/README.md with dev setup, auto-update architecture, signing docs - Update .gitignore for desktop/node_modules, dist, target - Configure macOS minimumSystemVersion, Windows timestampUrl - Include all Phase 14-21 work: agent hardening, RBAC, taint tracking, workflows, skills, knowledge graph, sessions, A2A, MCP templates, WASM sandbox, TUI dashboard, production tools, CLI expansion, API expansion, learning productionization, Tauri desktop app, and 10 new channels Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
128 lines
4.4 KiB
Python
128 lines
4.4 KiB
Python
"""WorkflowGraph — DAG with validation and topological sort."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections import defaultdict, deque
|
|
from typing import Dict, List, Optional, Set, Tuple
|
|
|
|
from openjarvis.workflow.types import WorkflowEdge, WorkflowNode
|
|
|
|
|
|
class WorkflowGraph:
|
|
"""Directed acyclic graph of workflow nodes.
|
|
|
|
Supports DAG validation (cycle detection), topological sort,
|
|
and execution_stages() for parallel-ready ordering.
|
|
"""
|
|
|
|
def __init__(self, name: str = "") -> None:
|
|
self.name = name
|
|
self._nodes: Dict[str, WorkflowNode] = {}
|
|
self._edges: List[WorkflowEdge] = []
|
|
self._adjacency: Dict[str, List[str]] = defaultdict(list)
|
|
self._reverse: Dict[str, List[str]] = defaultdict(list)
|
|
|
|
def add_node(self, node: WorkflowNode) -> None:
|
|
if node.id in self._nodes:
|
|
raise ValueError(f"Duplicate node id: {node.id}")
|
|
self._nodes[node.id] = node
|
|
|
|
def add_edge(self, edge: WorkflowEdge) -> None:
|
|
if edge.source not in self._nodes:
|
|
raise ValueError(f"Source node '{edge.source}' not found")
|
|
if edge.target not in self._nodes:
|
|
raise ValueError(f"Target node '{edge.target}' not found")
|
|
self._edges.append(edge)
|
|
self._adjacency[edge.source].append(edge.target)
|
|
self._reverse[edge.target].append(edge.source)
|
|
|
|
def get_node(self, node_id: str) -> Optional[WorkflowNode]:
|
|
return self._nodes.get(node_id)
|
|
|
|
@property
|
|
def nodes(self) -> List[WorkflowNode]:
|
|
return list(self._nodes.values())
|
|
|
|
@property
|
|
def edges(self) -> List[WorkflowEdge]:
|
|
return list(self._edges)
|
|
|
|
def validate(self) -> Tuple[bool, str]:
|
|
"""Validate the graph: check for cycles and orphan nodes."""
|
|
# Check for cycles using DFS
|
|
visited: Set[str] = set()
|
|
in_stack: Set[str] = set()
|
|
|
|
def _dfs(node_id: str) -> bool:
|
|
visited.add(node_id)
|
|
in_stack.add(node_id)
|
|
for neighbor in self._adjacency.get(node_id, []):
|
|
if neighbor in in_stack:
|
|
return True # cycle detected
|
|
if neighbor not in visited and _dfs(neighbor):
|
|
return True
|
|
in_stack.discard(node_id)
|
|
return False
|
|
|
|
for node_id in self._nodes:
|
|
if node_id not in visited:
|
|
if _dfs(node_id):
|
|
return False, f"Cycle detected involving node '{node_id}'"
|
|
|
|
return True, ""
|
|
|
|
def topological_sort(self) -> List[str]:
|
|
"""Return node IDs in topological order (Kahn's algorithm)."""
|
|
in_degree: Dict[str, int] = {nid: 0 for nid in self._nodes}
|
|
for edge in self._edges:
|
|
in_degree[edge.target] = in_degree.get(edge.target, 0) + 1
|
|
|
|
queue = deque(nid for nid, deg in in_degree.items() if deg == 0)
|
|
order: List[str] = []
|
|
|
|
while queue:
|
|
node_id = queue.popleft()
|
|
order.append(node_id)
|
|
for neighbor in self._adjacency.get(node_id, []):
|
|
in_degree[neighbor] -= 1
|
|
if in_degree[neighbor] == 0:
|
|
queue.append(neighbor)
|
|
|
|
if len(order) != len(self._nodes):
|
|
raise ValueError("Graph contains a cycle; topological sort is impossible")
|
|
return order
|
|
|
|
def execution_stages(self) -> List[List[str]]:
|
|
"""Group nodes into parallel execution stages.
|
|
|
|
Nodes in the same stage have no dependencies on each other and
|
|
can be executed concurrently.
|
|
"""
|
|
in_degree: Dict[str, int] = {nid: 0 for nid in self._nodes}
|
|
for edge in self._edges:
|
|
in_degree[edge.target] = in_degree.get(edge.target, 0) + 1
|
|
|
|
stages: List[List[str]] = []
|
|
ready = [nid for nid, deg in in_degree.items() if deg == 0]
|
|
|
|
while ready:
|
|
stages.append(sorted(ready))
|
|
next_ready: List[str] = []
|
|
for node_id in ready:
|
|
for neighbor in self._adjacency.get(node_id, []):
|
|
in_degree[neighbor] -= 1
|
|
if in_degree[neighbor] == 0:
|
|
next_ready.append(neighbor)
|
|
ready = next_ready
|
|
|
|
return stages
|
|
|
|
def predecessors(self, node_id: str) -> List[str]:
|
|
return self._reverse.get(node_id, [])
|
|
|
|
def successors(self, node_id: str) -> List[str]:
|
|
return self._adjacency.get(node_id, [])
|
|
|
|
|
|
__all__ = ["WorkflowGraph"]
|