Files
markitect-main/markitect/prompts/dependencies/graph.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

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