""" Quality validator for applying multiple gates to artifacts. Implements FR-9.2: Multiple QualityGates per artifact. Implements FR-9.3: Record pass/fail results and diagnostics in RunManifest. """ import sqlite3 from pathlib import Path from typing import Dict, Any, List, Optional from markitect.prompts.quality.models import ( GateType, QualityGate, ValidationResult, ValidationStatus, ) # SQL schema for quality tables QUALITY_TABLES_SQL = """ CREATE TABLE IF NOT EXISTS quality_gates ( id TEXT PRIMARY KEY, name TEXT NOT NULL, gate_type TEXT NOT NULL, config JSON NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS validation_results ( id TEXT PRIMARY KEY, run_id TEXT NOT NULL, gate_id TEXT NOT NULL, artifact_id TEXT, status TEXT NOT NULL, score REAL, diagnostics JSON, validated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_validations_run ON validation_results(run_id); CREATE INDEX IF NOT EXISTS idx_validations_artifact ON validation_results(artifact_id); """ class QualityValidator: """ Applies multiple quality gates to artifacts and records results. Implements FR-9.2 and FR-9.3. """ def __init__( self, gates: Optional[List[QualityGate]] = None, db_path: Optional[str] = None, ): """ Initialize validator with quality gates. Args: gates: List of quality gates to apply db_path: Optional database path for persisting results """ self.gates: List[QualityGate] = gates or [] self.db_path = db_path if db_path: self._initialize_tables() def _initialize_tables(self) -> None: """Initialize quality tables if DB path is set.""" db_dir = Path(self.db_path).parent if db_dir and not db_dir.exists(): db_dir.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(self.db_path) try: conn.executescript(QUALITY_TABLES_SQL) conn.commit() finally: conn.close() def _get_connection(self) -> sqlite3.Connection: """Get a database connection.""" conn = sqlite3.connect(self.db_path) conn.row_factory = sqlite3.Row return conn def add_gate(self, gate: QualityGate) -> None: """ Add a quality gate to the validator. Args: gate: Quality gate to add """ self.gates.append(gate) def validate_artifact( self, content: str, artifact_id: str, run_id: Optional[str] = None, ) -> List[ValidationResult]: """ Apply all quality gates to an artifact. Args: content: Artifact content to validate artifact_id: ID of the artifact run_id: Optional run ID for persistence Returns: List of ValidationResult from all gates """ results = [] for gate in self.gates: result = gate.validate(content, artifact_id) results.append(result) if run_id and self.db_path: self._persist_result(result, run_id) return results def all_passed(self, results: List[ValidationResult]) -> bool: """ Check if all validation results passed. Args: results: List of validation results Returns: True if all results have PASS status """ return all(r.status == ValidationStatus.PASS for r in results) def aggregate_score(self, results: List[ValidationResult]) -> float: """ Calculate aggregate score across all results. Args: results: List of validation results Returns: Average score (0.0-1.0), or 1.0 if no results """ if not results: return 1.0 scores = [r.score for r in results if r.score is not None] if not scores: return 1.0 return sum(scores) / len(scores) def get_failed_gates( self, results: List[ValidationResult], ) -> List[ValidationResult]: """ Get only failed validation results. Args: results: List of validation results Returns: List of results with FAIL status """ return [r for r in results if r.status == ValidationStatus.FAIL] def results_to_manifest_dict( self, results: List[ValidationResult], ) -> Dict[str, Any]: """ Convert validation results to RunManifest-compatible dict. Implements FR-9.3: Results for RunManifest. Args: results: List of validation results Returns: Dictionary suitable for RunManifest.validation_results """ return { "quality_gates": [r.to_dict() for r in results], "all_passed": self.all_passed(results), "aggregate_score": self.aggregate_score(results), } def _persist_result( self, result: ValidationResult, run_id: str, ) -> None: """Persist a validation result to the database.""" import json conn = self._get_connection() try: conn.execute( """ INSERT INTO validation_results ( id, run_id, gate_id, artifact_id, status, score, diagnostics, validated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, ( result.id, run_id, result.gate_id, result.artifact_id, result.status.value, result.score, json.dumps([d.to_dict() for d in result.diagnostics]), result.validated_at.isoformat(), ), ) conn.commit() finally: conn.close() def get_results_for_run(self, run_id: str) -> List[Dict[str, Any]]: """ Retrieve persisted validation results for a run. Args: run_id: Run identifier Returns: List of result dictionaries """ if not self.db_path: return [] import json conn = self._get_connection() try: cursor = conn.execute( "SELECT * FROM validation_results WHERE run_id = ?", (run_id,), ) results = [] for row in cursor.fetchall(): results.append({ "id": row["id"], "run_id": run_id, "gate_id": row["gate_id"], "artifact_id": row["artifact_id"], "status": row["status"], "score": row["score"], "diagnostics": json.loads(row["diagnostics"]) if row["diagnostics"] else [], "validated_at": row["validated_at"], }) return results finally: conn.close() def get_results_for_artifact(self, artifact_id: str) -> List[Dict[str, Any]]: """ Retrieve persisted validation results for an artifact. Args: artifact_id: Artifact identifier Returns: List of result dictionaries """ if not self.db_path: return [] import json conn = self._get_connection() try: cursor = conn.execute( "SELECT * FROM validation_results WHERE artifact_id = ?", (artifact_id,), ) results = [] for row in cursor.fetchall(): results.append({ "id": row["id"], "run_id": row["run_id"], "gate_id": row["gate_id"], "artifact_id": artifact_id, "status": row["status"], "score": row["score"], "diagnostics": json.loads(row["diagnostics"]) if row["diagnostics"] else [], "validated_at": row["validated_at"], }) return results finally: conn.close()