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>
This commit is contained in:
221
markitect/prompts/dependencies/queries.py
Normal file
221
markitect/prompts/dependencies/queries.py
Normal file
@@ -0,0 +1,221 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user