Files
markitect-main/markitect/prompts/queries/operations.py
tegwick 7b4bd461c9 feat(prompts): implement Phase 8 - Observability & Traceability (FR-11)
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>
2026-02-09 20:32:18 +01:00

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
]