Files
markitect-main/markitect/prompts/dependencies/queries.py
tegwick 9ce157400e feat(prompts): implement Phase 5 - Dependency Tracking (FR-6)
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>
2026-02-09 13:18:18 +01:00

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