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:
145
markitect/prompts/dependencies/graph.py
Normal file
145
markitect/prompts/dependencies/graph.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
Graph builder for bridging ephemeral manifest edges to persistent storage.
|
||||
|
||||
Bridges RunManifest ephemeral DependencyEdge instances (3-field) to
|
||||
persistent DependencyEdge storage (6-field), and builds DependencyGraph
|
||||
from repository data.
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Set
|
||||
|
||||
from markitect.prompts.dependencies.models import (
|
||||
DependencyEdge,
|
||||
DependencyGraph,
|
||||
EdgeType,
|
||||
)
|
||||
from markitect.prompts.dependencies.repository import IDependencyRepository
|
||||
from markitect.prompts.execution.manifest import (
|
||||
DependencyEdge as ManifestDependencyEdge,
|
||||
RunManifest,
|
||||
)
|
||||
|
||||
|
||||
class GraphBuilder:
|
||||
"""
|
||||
Bridges ephemeral manifest edges to persistent dependency storage.
|
||||
|
||||
Extracts dependency edges from RunManifest instances, persists them
|
||||
via IDependencyRepository, and builds in-memory DependencyGraph
|
||||
instances for analysis.
|
||||
"""
|
||||
|
||||
def __init__(self, repository: IDependencyRepository):
|
||||
"""
|
||||
Initialize with dependency repository.
|
||||
|
||||
Args:
|
||||
repository: Repository for persisting dependency edges
|
||||
"""
|
||||
self._repository = repository
|
||||
|
||||
def extract_edges(self, manifest: RunManifest) -> List[DependencyEdge]:
|
||||
"""
|
||||
Extract persistent DependencyEdge instances from a RunManifest.
|
||||
|
||||
Converts the ephemeral 3-field DependencyEdge from the manifest
|
||||
into persistent 6-field DependencyEdge instances.
|
||||
|
||||
Args:
|
||||
manifest: RunManifest containing ephemeral edges
|
||||
|
||||
Returns:
|
||||
List of persistent DependencyEdge instances
|
||||
"""
|
||||
edges = []
|
||||
for manifest_edge in manifest.dependency_edges:
|
||||
edge_type = EdgeType(manifest_edge.edge_type)
|
||||
edge = DependencyEdge.create(
|
||||
source_artifact_id=manifest_edge.source_id,
|
||||
target_artifact_id=manifest_edge.target_id,
|
||||
run_id=manifest.run_id,
|
||||
edge_type=edge_type,
|
||||
)
|
||||
edges.append(edge)
|
||||
return edges
|
||||
|
||||
def persist_edges(self, manifest: RunManifest) -> List[DependencyEdge]:
|
||||
"""
|
||||
Extract edges from manifest and persist them to the repository.
|
||||
|
||||
Args:
|
||||
manifest: RunManifest containing ephemeral edges
|
||||
|
||||
Returns:
|
||||
List of persisted DependencyEdge instances
|
||||
"""
|
||||
edges = self.extract_edges(manifest)
|
||||
persisted = []
|
||||
for edge in edges:
|
||||
created = self._repository.create(edge)
|
||||
persisted.append(created)
|
||||
return persisted
|
||||
|
||||
def build_graph(
|
||||
self,
|
||||
artifact_ids: Optional[Set[str]] = None,
|
||||
) -> DependencyGraph:
|
||||
"""
|
||||
Build an in-memory DependencyGraph from repository data.
|
||||
|
||||
Args:
|
||||
artifact_ids: Optional set of artifact IDs to scope the graph.
|
||||
If None, builds graph from all edges.
|
||||
|
||||
Returns:
|
||||
DependencyGraph instance
|
||||
"""
|
||||
graph = DependencyGraph()
|
||||
|
||||
if artifact_ids is not None:
|
||||
# Build scoped subgraph
|
||||
for artifact_id in artifact_ids:
|
||||
edges = self._repository.get_by_source(artifact_id)
|
||||
for edge in edges:
|
||||
if edge.target_artifact_id in artifact_ids:
|
||||
graph.add_edge(
|
||||
edge.source_artifact_id,
|
||||
edge.target_artifact_id,
|
||||
edge.edge_type,
|
||||
)
|
||||
# Ensure node exists even if it has no edges
|
||||
if artifact_id not in graph._forward:
|
||||
graph._forward[artifact_id] = set()
|
||||
if artifact_id not in graph._reverse:
|
||||
graph._reverse[artifact_id] = set()
|
||||
else:
|
||||
# Build full graph
|
||||
all_edges = self._repository.get_all()
|
||||
for edge in all_edges:
|
||||
graph.add_edge(
|
||||
edge.source_artifact_id,
|
||||
edge.target_artifact_id,
|
||||
edge.edge_type,
|
||||
)
|
||||
|
||||
return graph
|
||||
|
||||
def build_graph_for_run(self, run_id: str) -> DependencyGraph:
|
||||
"""
|
||||
Build a DependencyGraph from edges of a specific run.
|
||||
|
||||
Args:
|
||||
run_id: Run identifier
|
||||
|
||||
Returns:
|
||||
DependencyGraph for the run's edges
|
||||
"""
|
||||
graph = DependencyGraph()
|
||||
edges = self._repository.get_by_run(run_id)
|
||||
for edge in edges:
|
||||
graph.add_edge(
|
||||
edge.source_artifact_id,
|
||||
edge.target_artifact_id,
|
||||
edge.edge_type,
|
||||
)
|
||||
return graph
|
||||
Reference in New Issue
Block a user