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

148 lines
5.1 KiB
Python

"""
CLI commands for prompt dependency resolution.
Provides Click commands for tracing provenance, visualizing graphs,
querying run history, and inspecting impact debt.
"""
import json
import os
import click
from markitect.prompts.dependencies.graph import GraphBuilder
from markitect.prompts.dependencies.repository import SQLiteDependencyRepository
from markitect.prompts.queries.operations import PromptQueryService
from markitect.prompts.repositories.sqlite import SQLiteArtifactRepository
from markitect.prompts.traceability.service import TraceabilityService
from markitect.prompts.visualization.graph import GraphExporter
def _default_db_path():
"""Return the default database path."""
return os.path.expanduser("~/.markitect/markitect.db")
def _get_repos(database):
"""Create artifact and dependency repos from database path."""
db = database or _default_db_path()
artifact_repo = SQLiteArtifactRepository(db)
dep_repo = SQLiteDependencyRepository(db)
return artifact_repo, dep_repo, db
@click.group(name="prompt")
def prompt_commands():
"""Prompt dependency resolution commands."""
pass
@prompt_commands.command("trace")
@click.argument("artifact_id")
@click.option("--database", type=click.Path(), help="Database file path")
def trace_artifact(artifact_id, database):
"""Trace provenance of an artifact."""
artifact_repo, dep_repo, db = _get_repos(database)
service = TraceabilityService(artifact_repo, dep_repo, db_path=db)
artifact = artifact_repo.get_by_id(artifact_id)
if not artifact:
click.echo(f"Artifact '{artifact_id}' not found.", err=True)
raise SystemExit(1)
trace = service.trace_artifact(artifact_id)
click.echo(json.dumps(trace.to_dict(), indent=2, default=str))
@prompt_commands.command("graph")
@click.argument("artifact_id")
@click.option(
"--format",
"fmt",
type=click.Choice(["dot", "mermaid"]),
default="mermaid",
help="Output format",
)
@click.option("--database", type=click.Path(), help="Database file path")
def show_graph(artifact_id, fmt, database):
"""Visualize dependency graph for an artifact."""
artifact_repo, dep_repo, db = _get_repos(database)
builder = GraphBuilder(dep_repo)
# Build graph from all edges involving this artifact
from markitect.prompts.dependencies.queries import DependencyQueryService
query_svc = DependencyQueryService(dep_repo)
deps = query_svc.find_transitive_dependencies(artifact_id)
dependents = query_svc.find_transitive_dependents(artifact_id)
all_ids = deps | dependents | {artifact_id}
graph = builder.build_graph(all_ids)
if fmt == "dot":
click.echo(GraphExporter.to_dot(graph, title=f"Dependencies: {artifact_id}"))
else:
click.echo(
GraphExporter.to_mermaid(graph, title=f"Dependencies: {artifact_id}")
)
@prompt_commands.command("runs")
@click.option("--template", help="Filter by template ID")
@click.option(
"--status",
type=click.Choice(["pending", "running", "success", "failed", "skipped"]),
help="Filter by status",
)
@click.option("--limit", default=20, type=int, help="Maximum results")
@click.option("--database", type=click.Path(), help="Database file path")
def list_runs(template, status, limit, database):
"""List prompt execution runs."""
artifact_repo, dep_repo, db = _get_repos(database)
query_svc = PromptQueryService(artifact_repo, dep_repo, db_path=db)
runs = query_svc.get_run_history(
template_id=template, status=status, limit=limit
)
if not runs:
click.echo("No runs found.")
return
click.echo(json.dumps(runs, indent=2, default=str))
@prompt_commands.command("debt")
@click.option("--artifact", help="Filter by artifact ID")
@click.option("--database", type=click.Path(), help="Database file path")
def show_debt(artifact, database):
"""Show impact debt (suppressed recomputations)."""
artifact_repo, dep_repo, db = _get_repos(database)
query_svc = PromptQueryService(artifact_repo, dep_repo, db_path=db)
if artifact:
# Get debt for specific artifact via traceability service
trace_svc = TraceabilityService(artifact_repo, dep_repo, db_path=db)
debts = trace_svc.get_impact_debt(artifact)
if not debts:
click.echo(f"No impact debt for artifact '{artifact}'.")
return
click.echo(json.dumps(debts, indent=2, default=str))
else:
stale = query_svc.get_stale_artifacts()
if not stale:
click.echo("No stale artifacts found.")
return
click.echo(json.dumps(stale, indent=2, default=str))
@prompt_commands.command("stats")
@click.option("--database", type=click.Path(), help="Database file path")
def show_stats(database):
"""Show dependency graph statistics."""
artifact_repo, dep_repo, db = _get_repos(database)
query_svc = PromptQueryService(artifact_repo, dep_repo, db_path=db)
stats = query_svc.get_dependency_stats()
click.echo(json.dumps(stats, indent=2))