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>
146 lines
4.7 KiB
Python
146 lines
4.7 KiB
Python
"""
|
|
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
|