""" Halting policy engine for refinement control. Implements FR-10: Halting and Refinement Policy Evaluates halting conditions based on quality gate results, marginal improvement, iteration limits, and resource budgets. """ from typing import List, Optional from markitect.prompts.quality.models import ( HaltDecision, HaltingRecord, QualityPolicy, ValidationResult, ValidationStatus, ) class HaltingPolicyEngine: """ Evaluates halting decisions based on configurable policies. Implements FR-10.2: Evaluate halting based on quality gate results, marginal improvement metrics, iteration limits, and resource budgets. Implements FR-10.3: Record halting decisions. """ def __init__(self, policy: QualityPolicy): """ Initialize with a quality policy. Args: policy: Quality policy configuration """ self.policy = policy def evaluate( self, results: List[ValidationResult], iteration: int, score_history: Optional[List[float]] = None, total_runs: int = 0, ) -> HaltingRecord: """ Evaluate whether to halt or continue refinement. Checks in order: 1. Resource budget exhaustion 2. Iteration limit 3. Quality met (all required gates pass) 4. Marginal improvement below threshold Args: results: Validation results from current iteration iteration: Current iteration number (1-based) score_history: Aggregate scores from previous iterations total_runs: Total number of runs consumed Returns: HaltingRecord with the decision """ score_history = score_history or [] current_score = self._aggregate_score(results) all_scores = score_history + [current_score] # Check resource budget if total_runs >= self.policy.resource_budget: return HaltingRecord( decision=HaltDecision.HALT_BUDGET_EXHAUSTED, iteration=iteration, max_iterations=self.policy.max_iterations, scores=all_scores, reason=f"Resource budget exhausted: {total_runs}/{self.policy.resource_budget} runs used", ) # Check iteration limit if iteration >= self.policy.max_iterations: return HaltingRecord( decision=HaltDecision.HALT_ITERATION_LIMIT, iteration=iteration, max_iterations=self.policy.max_iterations, scores=all_scores, reason=f"Iteration limit reached: {iteration}/{self.policy.max_iterations}", ) # Check if quality is met if self._quality_met(results): return HaltingRecord( decision=HaltDecision.HALT_QUALITY_MET, iteration=iteration, max_iterations=self.policy.max_iterations, scores=all_scores, reason="All quality gates passed", ) # Check marginal improvement if len(all_scores) >= 2: improvement = all_scores[-1] - all_scores[-2] if improvement < self.policy.min_improvement: return HaltingRecord( decision=HaltDecision.HALT_NO_IMPROVEMENT, iteration=iteration, max_iterations=self.policy.max_iterations, scores=all_scores, reason=( f"Marginal improvement {improvement:.4f} below " f"threshold {self.policy.min_improvement}" ), ) # Continue refinement return HaltingRecord( decision=HaltDecision.CONTINUE, iteration=iteration, max_iterations=self.policy.max_iterations, scores=all_scores, reason="Continuing refinement", ) def _quality_met(self, results: List[ValidationResult]) -> bool: """ Check if quality requirements are met. If required_gate_ids is set, only those gates must pass. Otherwise, all gates must pass. Args: results: Validation results to check Returns: True if quality requirements are met """ if self.policy.required_gate_ids: for gate_id in self.policy.required_gate_ids: gate_results = [r for r in results if r.gate_id == gate_id] if not gate_results: return False if any(r.status == ValidationStatus.FAIL for r in gate_results): return False return True return all(r.status == ValidationStatus.PASS for r in results) def _aggregate_score(self, results: List[ValidationResult]) -> float: """Calculate aggregate score from results.""" if not results: return 0.0 scores = [r.score for r in results if r.score is not None] if not scores: return 0.0 return sum(scores) / len(scores)