Add directed dependency graph with cycle detection, topological sort, and query service for finding dependents/dependencies transitively. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
222 lines
6.9 KiB
Python
222 lines
6.9 KiB
Python
"""
|
|
Dependency query service for analyzing artifact dependency graphs.
|
|
|
|
Provides operations for finding dependents, dependencies, transitive
|
|
closures, dependency chains, cycle detection, and build ordering.
|
|
"""
|
|
|
|
from collections import deque
|
|
from typing import List, Optional, Set
|
|
|
|
from markitect.prompts.dependencies.models import (
|
|
CircularDependencyError,
|
|
DependencyGraph,
|
|
EdgeType,
|
|
)
|
|
from markitect.prompts.dependencies.repository import IDependencyRepository
|
|
from markitect.prompts.dependencies.graph import GraphBuilder
|
|
|
|
|
|
class DependencyQueryService:
|
|
"""
|
|
Service for querying dependency relationships between artifacts.
|
|
|
|
Provides direct and transitive dependency lookups, dependency chain
|
|
computation via BFS, cycle detection, pre-validation, and build
|
|
order (topological sort).
|
|
"""
|
|
|
|
def __init__(self, repository: IDependencyRepository):
|
|
"""
|
|
Initialize with dependency repository.
|
|
|
|
Args:
|
|
repository: Repository for reading dependency edges
|
|
"""
|
|
self._repository = repository
|
|
self._graph_builder = GraphBuilder(repository)
|
|
|
|
def find_dependents(self, artifact_id: str) -> Set[str]:
|
|
"""
|
|
Find direct dependents of an artifact (who depends on it).
|
|
|
|
Args:
|
|
artifact_id: Artifact to find dependents of
|
|
|
|
Returns:
|
|
Set of artifact IDs that directly depend on this artifact
|
|
"""
|
|
edges = self._repository.get_by_target(artifact_id)
|
|
return {edge.source_artifact_id for edge in edges}
|
|
|
|
def find_dependencies(self, artifact_id: str) -> Set[str]:
|
|
"""
|
|
Find direct dependencies of an artifact (what it depends on).
|
|
|
|
Args:
|
|
artifact_id: Artifact to find dependencies of
|
|
|
|
Returns:
|
|
Set of artifact IDs that this artifact directly depends on
|
|
"""
|
|
edges = self._repository.get_by_source(artifact_id)
|
|
return {edge.target_artifact_id for edge in edges}
|
|
|
|
def find_transitive_dependents(self, artifact_id: str) -> Set[str]:
|
|
"""
|
|
Find all transitive dependents of an artifact (full upstream impact).
|
|
|
|
Uses BFS to traverse the reverse dependency graph.
|
|
|
|
Args:
|
|
artifact_id: Artifact to find transitive dependents of
|
|
|
|
Returns:
|
|
Set of all artifact IDs that transitively depend on this artifact
|
|
"""
|
|
graph = self._graph_builder.build_graph()
|
|
visited: Set[str] = set()
|
|
queue = deque([artifact_id])
|
|
|
|
while queue:
|
|
current = queue.popleft()
|
|
for predecessor in graph.get_predecessors(current):
|
|
if predecessor not in visited:
|
|
visited.add(predecessor)
|
|
queue.append(predecessor)
|
|
|
|
return visited
|
|
|
|
def find_transitive_dependencies(self, artifact_id: str) -> Set[str]:
|
|
"""
|
|
Find all transitive dependencies of an artifact (full dependency tree).
|
|
|
|
Uses BFS to traverse the forward dependency graph.
|
|
|
|
Args:
|
|
artifact_id: Artifact to find transitive dependencies of
|
|
|
|
Returns:
|
|
Set of all artifact IDs this artifact transitively depends on
|
|
"""
|
|
graph = self._graph_builder.build_graph()
|
|
visited: Set[str] = set()
|
|
queue = deque([artifact_id])
|
|
|
|
while queue:
|
|
current = queue.popleft()
|
|
for successor in graph.get_successors(current):
|
|
if successor not in visited:
|
|
visited.add(successor)
|
|
queue.append(successor)
|
|
|
|
return visited
|
|
|
|
def get_dependency_chain(
|
|
self,
|
|
source_id: str,
|
|
target_id: str,
|
|
) -> Optional[List[str]]:
|
|
"""
|
|
Find a dependency chain between two artifacts using BFS.
|
|
|
|
Args:
|
|
source_id: Starting artifact ID
|
|
target_id: Target artifact ID
|
|
|
|
Returns:
|
|
List of artifact IDs forming the chain from source to target,
|
|
or None if no path exists
|
|
"""
|
|
graph = self._graph_builder.build_graph()
|
|
|
|
if source_id == target_id:
|
|
return [source_id]
|
|
|
|
visited: Set[str] = {source_id}
|
|
queue: deque[List[str]] = deque([[source_id]])
|
|
|
|
while queue:
|
|
path = queue.popleft()
|
|
current = path[-1]
|
|
|
|
for successor in graph.get_successors(current):
|
|
if successor == target_id:
|
|
return path + [successor]
|
|
if successor not in visited:
|
|
visited.add(successor)
|
|
queue.append(path + [successor])
|
|
|
|
return None
|
|
|
|
def detect_circular_dependencies(self) -> List[List[str]]:
|
|
"""
|
|
Detect all circular dependencies in the graph.
|
|
|
|
Returns:
|
|
List of cycles, where each cycle is a list of artifact IDs
|
|
"""
|
|
graph = self._graph_builder.build_graph()
|
|
return graph.detect_cycles()
|
|
|
|
def would_create_cycle(
|
|
self,
|
|
source_id: str,
|
|
target_id: str,
|
|
) -> bool:
|
|
"""
|
|
Check if adding an edge would create a cycle.
|
|
|
|
Pre-validation check before persisting a new dependency edge.
|
|
|
|
Args:
|
|
source_id: Proposed source artifact ID
|
|
target_id: Proposed target artifact ID
|
|
|
|
Returns:
|
|
True if adding this edge would create a cycle
|
|
"""
|
|
graph = self._graph_builder.build_graph()
|
|
# Adding source -> target creates a cycle if target can reach source
|
|
# (i.e., there's already a path from target to source)
|
|
if source_id == target_id:
|
|
return True
|
|
|
|
visited: Set[str] = set()
|
|
queue = deque([target_id])
|
|
|
|
while queue:
|
|
current = queue.popleft()
|
|
if current == source_id:
|
|
return True
|
|
for successor in graph.get_successors(current):
|
|
if successor not in visited:
|
|
visited.add(successor)
|
|
queue.append(successor)
|
|
|
|
return False
|
|
|
|
def get_build_order(
|
|
self,
|
|
artifact_ids: Optional[Set[str]] = None,
|
|
) -> List[str]:
|
|
"""
|
|
Get artifacts in build order (topological sort).
|
|
|
|
Dependencies appear before the artifacts that depend on them.
|
|
|
|
Args:
|
|
artifact_ids: Optional set of artifact IDs to include.
|
|
If None, returns build order for all artifacts.
|
|
|
|
Returns:
|
|
List of artifact IDs in build order
|
|
|
|
Raises:
|
|
CircularDependencyError: If graph contains cycles
|
|
"""
|
|
graph = self._graph_builder.build_graph(artifact_ids)
|
|
order = graph.topological_sort()
|
|
order.reverse() # Dependencies before dependents
|
|
return order
|