""" 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