Complete implementation of Phase 8, the final phase of prompt dependency resolution infrastructure, adding full observability and traceability. ## Features (FR-11) ### FR-11.1: Complete Artifact Provenance Tracing - TraceabilityService: composition layer for full artifact lineage - Trace any artifact to producing PromptTemplate, input artifacts, generator runs, and quality validation results - ProvenanceTrace model with complete dependency chain reconstruction - RunSummary and ArtifactLineage models for structured trace output ### FR-11.2: Recomputation Query Infrastructure - PromptQueryService: cross-service complex queries - Run history queries with template and status filters - Stale artifact detection via impact debt analysis - Dependency graph statistics (nodes, edges, cycles, roots, leaves) - Content-based artifact lookups by digest ### Visualization Support - GraphExporter: DOT (Graphviz) and Mermaid format export - Supports all edge types (requires, generates, includes) - Handles isolated nodes, linear chains, diamonds, and complex graphs ### CLI Commands (prompt group) - `prompt trace <artifact_id>` - Full provenance trace as JSON - `prompt graph <artifact_id>` - Dependency graph (DOT/Mermaid) - `prompt runs` - List execution runs with filters - `prompt debt` - Show impact debt and stale artifacts - `prompt stats` - Dependency graph statistics ## Implementation Source files (8): - markitect/prompts/traceability/models.py - Trace data models - markitect/prompts/traceability/service.py - TraceabilityService - markitect/prompts/visualization/graph.py - Graph export - markitect/prompts/queries/operations.py - PromptQueryService - markitect/prompts/cli.py - Click CLI commands - Package __init__.py files (3) Tests (64 total, all passing): - tests/unit/prompts/test_traceability_service.py (21 tests) - tests/unit/prompts/test_visualization.py (14 tests) - tests/unit/prompts/test_query_operations.py (12 tests) - tests/integration/prompts/test_traceability_workflow.py (7 tests) - tests/integration/prompts/test_prompt_cli.py (10 tests) ## Architecture TraceabilityService is a composition layer that delegates to: - DependencyQueryService (transitive dependency lookups) - QualityValidator (validation history) - IncrementalExecutionEngine (impact debt queries) - Direct repository access (artifacts, edges) No duplicate data storage - all data comes from existing Phase 1-7 infrastructure (artifact repo, dependency repo, validation DB, debt DB). ## Verification All 2250 tests pass with 0 regressions. Phase 8 completes the full 8-phase implementation roadmap. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
166 lines
5.4 KiB
Python
166 lines
5.4 KiB
Python
"""
|
|
Complex cross-service query operations for prompt infrastructure.
|
|
|
|
Combines artifact repository, dependency repository, and run data
|
|
into higher-level queries like run history, impact analysis, and stats.
|
|
"""
|
|
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from markitect.prompts.dependencies.graph import GraphBuilder
|
|
from markitect.prompts.dependencies.queries import DependencyQueryService
|
|
from markitect.prompts.dependencies.repository import IDependencyRepository
|
|
from markitect.prompts.execution.models import PromptRun
|
|
from markitect.prompts.incremental.engine import IncrementalExecutionEngine
|
|
from markitect.prompts.repositories.interfaces import IArtifactRepository
|
|
|
|
|
|
class PromptQueryService:
|
|
"""
|
|
Complex cross-service queries over prompt infrastructure.
|
|
|
|
Provides higher-level queries that span multiple data sources:
|
|
run history, stale artifact detection, dependency statistics,
|
|
and content-based artifact lookups.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
artifact_repo: IArtifactRepository,
|
|
dependency_repo: IDependencyRepository,
|
|
db_path: Optional[str] = None,
|
|
):
|
|
"""
|
|
Initialize with data sources.
|
|
|
|
Args:
|
|
artifact_repo: Artifact repository
|
|
dependency_repo: Dependency edge repository
|
|
db_path: Optional database path for debt queries
|
|
"""
|
|
self._artifact_repo = artifact_repo
|
|
self._dependency_repo = dependency_repo
|
|
self._db_path = db_path
|
|
self._query_service = DependencyQueryService(dependency_repo)
|
|
self._graph_builder = GraphBuilder(dependency_repo)
|
|
self._engine = (
|
|
IncrementalExecutionEngine(db_path, self._query_service)
|
|
if db_path
|
|
else None
|
|
)
|
|
# Run registry: external code registers runs for querying
|
|
self._runs: Dict[str, PromptRun] = {}
|
|
|
|
def register_run(self, run: PromptRun) -> None:
|
|
"""Register a run for query lookups."""
|
|
self._runs[run.id] = run
|
|
|
|
def get_run_history(
|
|
self,
|
|
template_id: Optional[str] = None,
|
|
status: Optional[str] = None,
|
|
limit: int = 50,
|
|
) -> List[Dict[str, Any]]:
|
|
"""
|
|
Query run history with optional filters.
|
|
|
|
Args:
|
|
template_id: Optional template ID filter
|
|
status: Optional status filter
|
|
limit: Maximum results to return
|
|
|
|
Returns:
|
|
List of run dictionaries sorted by start time (newest first)
|
|
"""
|
|
runs = list(self._runs.values())
|
|
|
|
if template_id:
|
|
runs = [r for r in runs if r.template_id == template_id]
|
|
|
|
if status:
|
|
runs = [r for r in runs if r.status.value == status]
|
|
|
|
runs.sort(key=lambda r: r.started_at, reverse=True)
|
|
runs = runs[:limit]
|
|
|
|
return [r.to_dict() for r in runs]
|
|
|
|
def get_stale_artifacts(self) -> List[Dict[str, Any]]:
|
|
"""
|
|
Find artifacts with outstanding impact debt.
|
|
|
|
Returns:
|
|
List of dicts with artifact info and debt details
|
|
"""
|
|
if not self._engine:
|
|
return []
|
|
|
|
all_debt = self._engine.get_all_debt()
|
|
if not all_debt:
|
|
return []
|
|
|
|
# Group debt by artifact
|
|
debt_by_artifact: Dict[str, list] = {}
|
|
for d in all_debt:
|
|
debt_by_artifact.setdefault(d.artifact_id, []).append(d)
|
|
|
|
results = []
|
|
for artifact_id, debts in debt_by_artifact.items():
|
|
artifact = self._artifact_repo.get_by_id(artifact_id)
|
|
results.append({
|
|
"artifact_id": artifact_id,
|
|
"artifact_name": artifact.name if artifact else "unknown",
|
|
"debt_count": len(debts),
|
|
"total_magnitude": sum(d.change_magnitude for d in debts),
|
|
"reasons": list({d.suppression_reason for d in debts}),
|
|
})
|
|
|
|
return results
|
|
|
|
def get_dependency_stats(self) -> Dict[str, Any]:
|
|
"""
|
|
Get summary statistics about the dependency graph.
|
|
|
|
Returns:
|
|
Dictionary with graph statistics
|
|
"""
|
|
graph = self._graph_builder.build_graph()
|
|
nodes = graph.nodes
|
|
edge_count = graph.edge_count
|
|
|
|
# Find root nodes (no predecessors) and leaf nodes (no successors)
|
|
roots = [n for n in nodes if not graph.get_predecessors(n)]
|
|
leaves = [n for n in nodes if not graph.get_successors(n)]
|
|
|
|
# Check for cycles
|
|
has_cycles = graph.has_cycle()
|
|
|
|
return {
|
|
"total_nodes": len(nodes),
|
|
"total_edges": edge_count,
|
|
"root_count": len(roots),
|
|
"leaf_count": len(leaves),
|
|
"has_cycles": has_cycles,
|
|
}
|
|
|
|
def find_artifacts_by_digest(self, digest: str) -> List[Dict[str, Any]]:
|
|
"""
|
|
Find all artifacts with a given content digest.
|
|
|
|
Args:
|
|
digest: Content digest to search for
|
|
|
|
Returns:
|
|
List of artifact dictionaries
|
|
"""
|
|
artifacts = self._artifact_repo.get_by_digest(digest)
|
|
return [
|
|
{
|
|
"artifact_id": a.id,
|
|
"name": a.name,
|
|
"space_id": a.space_id,
|
|
"artifact_type": a.artifact_type.value,
|
|
}
|
|
for a in artifacts
|
|
]
|