Phase 8 Production Hardening with complete governance infrastructure: - Vault integration with tiered policies (T0-T4) - DragonflyDB state management - SQLite audit ledger - Pipeline DSL and templates - Promotion/revocation engine - Checkpoint system for session persistence - Health manager and circuit breaker for fault tolerance - GitHub/Slack integrations - Architectural test pipeline with bug watcher, suggestion engine, council review - Multi-agent chaos testing framework Test Results: - Governance tests: 68/68 passing - E2E workflow: 16/16 passing - Phase 2 Vault: 14/14 passing - Integration tests: 27/27 passing Coverage: 57.6% average across 12 phases Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
657 lines
24 KiB
Python
657 lines
24 KiB
Python
"""
|
|
Suggestion + Reasoning Pipeline
|
|
===============================
|
|
Analyzes outputs and bug reports using historical context to generate
|
|
suggested fixes with rationale, ranked by risk/impact.
|
|
|
|
Features:
|
|
- Uses checkpoints, memory logs, STATUS files for context
|
|
- Generates fix suggestions with reasoning
|
|
- Ranks by risk and impact
|
|
- Tracks suggestion outcomes for learning
|
|
"""
|
|
|
|
import json
|
|
import hashlib
|
|
from datetime import datetime, timezone
|
|
from dataclasses import dataclass, field, asdict
|
|
from enum import Enum
|
|
from pathlib import Path
|
|
from typing import Any, Optional
|
|
import sqlite3
|
|
import redis
|
|
|
|
from .bug_watcher import Anomaly, AnomalyType, Severity
|
|
|
|
|
|
class RiskLevel(str, Enum):
|
|
"""Risk level for implementing a suggestion"""
|
|
CRITICAL = "critical" # Could break system
|
|
HIGH = "high" # Significant change
|
|
MEDIUM = "medium" # Moderate change
|
|
LOW = "low" # Safe change
|
|
TRIVIAL = "trivial" # No risk
|
|
|
|
|
|
class ImpactLevel(str, Enum):
|
|
"""Impact level of implementing a suggestion"""
|
|
TRANSFORMATIVE = "transformative" # Major system improvement
|
|
HIGH = "high" # Significant improvement
|
|
MEDIUM = "medium" # Moderate improvement
|
|
LOW = "low" # Minor improvement
|
|
MINIMAL = "minimal" # Negligible improvement
|
|
|
|
|
|
class SuggestionStatus(str, Enum):
|
|
"""Status of a suggestion"""
|
|
PENDING = "pending"
|
|
APPROVED = "approved"
|
|
REJECTED = "rejected"
|
|
IMPLEMENTED = "implemented"
|
|
FAILED = "failed"
|
|
|
|
|
|
@dataclass
|
|
class Suggestion:
|
|
"""A suggested fix or improvement"""
|
|
id: str
|
|
anomaly_id: str
|
|
title: str
|
|
description: str
|
|
reasoning: str
|
|
fix_steps: list[str]
|
|
risk: RiskLevel
|
|
impact: ImpactLevel
|
|
estimated_effort: str # "trivial", "small", "medium", "large", "epic"
|
|
files_affected: list[str] = field(default_factory=list)
|
|
code_changes: Optional[dict] = None # {"file": {"old": ..., "new": ...}}
|
|
auto_fixable: bool = False
|
|
requires_human: bool = True
|
|
status: SuggestionStatus = SuggestionStatus.PENDING
|
|
created_at: str = ""
|
|
reviewed_at: Optional[str] = None
|
|
reviewed_by: Optional[str] = None
|
|
outcome: Optional[str] = None
|
|
|
|
def __post_init__(self):
|
|
if not self.created_at:
|
|
self.created_at = datetime.now(timezone.utc).isoformat()
|
|
if not self.id:
|
|
self.id = f"sug-{hashlib.sha256(f'{self.anomaly_id}{self.title}{self.created_at}'.encode()).hexdigest()[:12]}"
|
|
|
|
@property
|
|
def priority_score(self) -> float:
|
|
"""Calculate priority score (higher = more urgent)"""
|
|
risk_scores = {"critical": 0.2, "high": 0.4, "medium": 0.6, "low": 0.8, "trivial": 1.0}
|
|
impact_scores = {"transformative": 1.0, "high": 0.8, "medium": 0.6, "low": 0.4, "minimal": 0.2}
|
|
|
|
# Handle both enum and string values
|
|
risk_val = self.risk.value if hasattr(self.risk, 'value') else self.risk
|
|
impact_val = self.impact.value if hasattr(self.impact, 'value') else self.impact
|
|
|
|
# Higher impact + lower risk = higher priority
|
|
return impact_scores.get(impact_val, 0.5) * risk_scores.get(risk_val, 0.5)
|
|
|
|
|
|
@dataclass
|
|
class HistoricalContext:
|
|
"""Context gathered from historical data"""
|
|
recent_checkpoints: list[dict] = field(default_factory=list)
|
|
related_anomalies: list[dict] = field(default_factory=list)
|
|
past_suggestions: list[dict] = field(default_factory=list)
|
|
memory_entries: list[dict] = field(default_factory=list)
|
|
status_history: list[dict] = field(default_factory=list)
|
|
|
|
|
|
class SuggestionEngine:
|
|
"""
|
|
Analyzes anomalies and generates fix suggestions using historical context.
|
|
|
|
Process:
|
|
1. Gather context from checkpoints, memory, STATUS files
|
|
2. Analyze anomaly patterns
|
|
3. Generate suggestions with reasoning
|
|
4. Rank by risk/impact
|
|
5. Track outcomes for learning
|
|
"""
|
|
|
|
# Anomaly type -> common fixes mapping
|
|
FIX_PATTERNS = {
|
|
AnomalyType.UNHANDLED_ERROR: [
|
|
("Add try-except block", "medium", "medium"),
|
|
("Add error logging", "low", "low"),
|
|
("Create error handler", "medium", "high"),
|
|
],
|
|
AnomalyType.REGRESSION: [
|
|
("Rollback to previous version", "high", "high"),
|
|
("Add regression test", "low", "medium"),
|
|
("Investigate root cause", "low", "high"),
|
|
],
|
|
AnomalyType.MISSING_ARTIFACT: [
|
|
("Create missing file", "low", "medium"),
|
|
("Update documentation", "trivial", "low"),
|
|
("Add artifact generation step", "medium", "medium"),
|
|
],
|
|
AnomalyType.STATE_INCONSISTENCY: [
|
|
("Resync state from source", "medium", "high"),
|
|
("Add state validation", "low", "medium"),
|
|
("Implement state machine", "high", "high"),
|
|
],
|
|
AnomalyType.DEPENDENCY_UNAVAILABLE: [
|
|
("Restart dependency service", "low", "high"),
|
|
("Add fallback mechanism", "medium", "high"),
|
|
("Implement circuit breaker", "medium", "high"),
|
|
],
|
|
AnomalyType.HEALTH_CHECK_FAILURE: [
|
|
("Investigate failing check", "low", "medium"),
|
|
("Restart affected service", "low", "high"),
|
|
("Add health recovery logic", "medium", "high"),
|
|
],
|
|
AnomalyType.SECURITY_VIOLATION: [
|
|
("Revoke compromised credentials", "low", "transformative"),
|
|
("Audit access logs", "low", "high"),
|
|
("Strengthen access controls", "medium", "high"),
|
|
],
|
|
AnomalyType.TIMEOUT: [
|
|
("Increase timeout threshold", "low", "low"),
|
|
("Optimize slow operation", "medium", "high"),
|
|
("Add async processing", "high", "high"),
|
|
],
|
|
}
|
|
|
|
def __init__(self, base_path: str = "/opt/agent-governance"):
|
|
self.base_path = Path(base_path)
|
|
self.ledger_db = self.base_path / "ledger" / "governance.db"
|
|
self.checkpoint_dir = self.base_path / "checkpoint" / "storage"
|
|
self.memory_dir = self.base_path / "memory"
|
|
self.suggestions: list[Suggestion] = []
|
|
self._redis: Optional[redis.Redis] = None
|
|
self._setup_redis()
|
|
|
|
def _setup_redis(self):
|
|
"""Connect to DragonflyDB"""
|
|
try:
|
|
self._redis = redis.Redis(
|
|
host='127.0.0.1',
|
|
port=6379,
|
|
password='governance2026',
|
|
decode_responses=True
|
|
)
|
|
self._redis.ping()
|
|
except Exception:
|
|
self._redis = None
|
|
|
|
def _now(self) -> str:
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
def gather_context(self, anomaly: Anomaly) -> HistoricalContext:
|
|
"""Gather historical context relevant to an anomaly"""
|
|
context = HistoricalContext()
|
|
|
|
# 1. Recent checkpoints
|
|
context.recent_checkpoints = self._get_recent_checkpoints(5)
|
|
|
|
# 2. Related anomalies (same type or phase)
|
|
context.related_anomalies = self._get_related_anomalies(anomaly)
|
|
|
|
# 3. Past suggestions for similar issues
|
|
context.past_suggestions = self._get_past_suggestions(anomaly)
|
|
|
|
# 4. Memory entries related to the directory/phase
|
|
context.memory_entries = self._get_memory_entries(anomaly.directory)
|
|
|
|
# 5. STATUS history for the affected directory
|
|
context.status_history = self._get_status_history(anomaly.directory)
|
|
|
|
return context
|
|
|
|
def _get_recent_checkpoints(self, limit: int) -> list[dict]:
|
|
"""Get recent checkpoints"""
|
|
checkpoints = []
|
|
|
|
if not self.checkpoint_dir.exists():
|
|
return checkpoints
|
|
|
|
files = sorted(self.checkpoint_dir.glob("ckpt-*.json"), reverse=True)[:limit]
|
|
|
|
for f in files:
|
|
try:
|
|
data = json.loads(f.read_text())
|
|
checkpoints.append({
|
|
"id": data.get("checkpoint_id"),
|
|
"phase": data.get("phase", {}).get("name"),
|
|
"created_at": data.get("created_at"),
|
|
"notes": data.get("phase", {}).get("notes", "")
|
|
})
|
|
except Exception:
|
|
continue
|
|
|
|
return checkpoints
|
|
|
|
def _get_related_anomalies(self, anomaly: Anomaly) -> list[dict]:
|
|
"""Get related anomalies from history"""
|
|
related = []
|
|
|
|
if not self._redis:
|
|
return related
|
|
|
|
try:
|
|
# Get anomalies of same type
|
|
raw = self._redis.lrange("oversight:anomalies", 0, 100)
|
|
|
|
for item in raw:
|
|
data = json.loads(item)
|
|
if data.get("type") == anomaly.type.value or data.get("phase") == anomaly.phase:
|
|
if data.get("id") != anomaly.id:
|
|
related.append(data)
|
|
if len(related) >= 10:
|
|
break
|
|
except Exception:
|
|
pass
|
|
|
|
return related
|
|
|
|
def _get_past_suggestions(self, anomaly: Anomaly) -> list[dict]:
|
|
"""Get past suggestions for similar issues"""
|
|
suggestions = []
|
|
|
|
if not self._redis:
|
|
return suggestions
|
|
|
|
try:
|
|
raw = self._redis.lrange("oversight:suggestions", 0, 100)
|
|
|
|
for item in raw:
|
|
data = json.loads(item)
|
|
# Match by anomaly type pattern in title
|
|
if anomaly.type.value.lower() in data.get("title", "").lower():
|
|
suggestions.append(data)
|
|
if len(suggestions) >= 5:
|
|
break
|
|
except Exception:
|
|
pass
|
|
|
|
return suggestions
|
|
|
|
def _get_memory_entries(self, directory: str) -> list[dict]:
|
|
"""Get memory entries related to a directory"""
|
|
entries = []
|
|
|
|
summaries_dir = self.memory_dir / "summaries"
|
|
if not summaries_dir.exists():
|
|
return entries
|
|
|
|
try:
|
|
files = sorted(summaries_dir.glob("*.json"), reverse=True)[:10]
|
|
|
|
for f in files:
|
|
data = json.loads(f.read_text())
|
|
if directory in data.get("metadata", {}).get("source", ""):
|
|
entries.append({
|
|
"id": data.get("id"),
|
|
"summary": data.get("summary", "")[:200],
|
|
"created_at": data.get("created_at")
|
|
})
|
|
except Exception:
|
|
pass
|
|
|
|
return entries
|
|
|
|
def _get_status_history(self, directory: str) -> list[dict]:
|
|
"""Get STATUS.md history for a directory"""
|
|
history = []
|
|
|
|
status_file = self.base_path / directory / "STATUS.md"
|
|
if not status_file.exists():
|
|
return history
|
|
|
|
try:
|
|
content = status_file.read_text()
|
|
|
|
# Parse activity log section
|
|
if "## Activity Log" in content:
|
|
log_section = content.split("## Activity Log")[1]
|
|
|
|
# Extract entries (format: ### YYYY-MM-DD ...)
|
|
import re
|
|
entries = re.findall(r'### (\d{4}-\d{2}-\d{2}[^\n]*)\n([^#]*)', log_section)
|
|
|
|
for date, details in entries[:5]:
|
|
history.append({
|
|
"date": date.strip(),
|
|
"details": details.strip()[:200]
|
|
})
|
|
except Exception:
|
|
pass
|
|
|
|
return history
|
|
|
|
def generate_suggestions(self, anomaly: Anomaly) -> list[Suggestion]:
|
|
"""Generate fix suggestions for an anomaly"""
|
|
suggestions = []
|
|
context = self.gather_context(anomaly)
|
|
|
|
# 1. Get pattern-based suggestions
|
|
pattern_suggestions = self._generate_pattern_suggestions(anomaly)
|
|
suggestions.extend(pattern_suggestions)
|
|
|
|
# 2. Generate context-aware suggestions
|
|
context_suggestions = self._generate_context_suggestions(anomaly, context)
|
|
suggestions.extend(context_suggestions)
|
|
|
|
# 3. Generate phase-specific suggestions
|
|
phase_suggestions = self._generate_phase_suggestions(anomaly)
|
|
suggestions.extend(phase_suggestions)
|
|
|
|
# Sort by priority score
|
|
suggestions.sort(key=lambda s: s.priority_score, reverse=True)
|
|
|
|
# Persist suggestions
|
|
self._persist_suggestions(suggestions)
|
|
self.suggestions.extend(suggestions)
|
|
|
|
return suggestions
|
|
|
|
def _generate_pattern_suggestions(self, anomaly: Anomaly) -> list[Suggestion]:
|
|
"""Generate suggestions based on known patterns"""
|
|
suggestions = []
|
|
|
|
patterns = self.FIX_PATTERNS.get(anomaly.type, [])
|
|
|
|
for title, risk, impact in patterns:
|
|
suggestion = Suggestion(
|
|
id="",
|
|
anomaly_id=anomaly.id,
|
|
title=title,
|
|
description=f"Standard fix for {anomaly.type.value}: {title}",
|
|
reasoning=f"This is a common fix pattern for {anomaly.type.value} anomalies. "
|
|
f"Historical data shows this approach resolves similar issues.",
|
|
fix_steps=[
|
|
f"1. Analyze the specific {anomaly.type.value} in {anomaly.directory}",
|
|
f"2. Apply {title.lower()}",
|
|
"3. Verify fix with tests",
|
|
"4. Update STATUS.md and create checkpoint"
|
|
],
|
|
risk=RiskLevel(risk),
|
|
impact=ImpactLevel(impact),
|
|
estimated_effort="small" if risk == "low" else "medium",
|
|
files_affected=[anomaly.directory],
|
|
auto_fixable=risk in ["low", "trivial"],
|
|
requires_human=risk in ["critical", "high"]
|
|
)
|
|
suggestions.append(suggestion)
|
|
|
|
return suggestions
|
|
|
|
def _generate_context_suggestions(self, anomaly: Anomaly, context: HistoricalContext) -> list[Suggestion]:
|
|
"""Generate suggestions based on historical context"""
|
|
suggestions = []
|
|
|
|
# Check if similar anomalies were resolved before
|
|
for past in context.past_suggestions:
|
|
if past.get("status") == "implemented" and past.get("outcome") == "success":
|
|
suggestion = Suggestion(
|
|
id="",
|
|
anomaly_id=anomaly.id,
|
|
title=f"Repeat: {past.get('title', 'Previous fix')}",
|
|
description=f"This fix worked for a similar issue before",
|
|
reasoning=f"A similar anomaly was resolved on {past.get('created_at', 'unknown date')} "
|
|
f"using this approach. The outcome was successful.",
|
|
fix_steps=past.get("fix_steps", ["Review and apply previous fix"]),
|
|
risk=RiskLevel.LOW, # Lower risk since proven
|
|
impact=ImpactLevel(past.get("impact", "medium")),
|
|
estimated_effort="small",
|
|
auto_fixable=True,
|
|
requires_human=False
|
|
)
|
|
suggestions.append(suggestion)
|
|
break
|
|
|
|
# Check checkpoint notes for relevant context
|
|
for ckpt in context.recent_checkpoints:
|
|
notes = ckpt.get("notes", "")
|
|
if anomaly.type.value.lower() in notes.lower() or anomaly.directory in notes:
|
|
suggestion = Suggestion(
|
|
id="",
|
|
anomaly_id=anomaly.id,
|
|
title="Review recent checkpoint context",
|
|
description=f"Recent checkpoint {ckpt.get('id')} may have relevant context",
|
|
reasoning=f"Checkpoint notes mention related content: {notes[:100]}...",
|
|
fix_steps=[
|
|
f"1. Load checkpoint {ckpt.get('id')}",
|
|
"2. Review context and changes",
|
|
"3. Determine if rollback or forward-fix needed"
|
|
],
|
|
risk=RiskLevel.LOW,
|
|
impact=ImpactLevel.MEDIUM,
|
|
estimated_effort="small",
|
|
requires_human=True
|
|
)
|
|
suggestions.append(suggestion)
|
|
break
|
|
|
|
return suggestions
|
|
|
|
def _generate_phase_suggestions(self, anomaly: Anomaly) -> list[Suggestion]:
|
|
"""Generate phase-specific suggestions"""
|
|
suggestions = []
|
|
|
|
if anomaly.phase == 5: # Agent Bootstrapping - SPECIAL ATTENTION
|
|
if "config" in anomaly.message.lower() or "missing" in anomaly.message.lower():
|
|
suggestions.append(Suggestion(
|
|
id="",
|
|
anomaly_id=anomaly.id,
|
|
title="Regenerate agent configuration",
|
|
description="Agent configuration may be corrupted or missing",
|
|
reasoning="Phase 5 (Agent Bootstrapping) is critical. Missing configuration "
|
|
"can block agent initialization and all dependent phases.",
|
|
fix_steps=[
|
|
"1. Check agents/tier0-agent/config/ directory",
|
|
"2. Regenerate agent.json from template",
|
|
"3. Verify AppRole credentials in credentials/",
|
|
"4. Run agent.py status to verify",
|
|
"5. Create checkpoint"
|
|
],
|
|
risk=RiskLevel.MEDIUM,
|
|
impact=ImpactLevel.HIGH,
|
|
estimated_effort="medium",
|
|
files_affected=[
|
|
"agents/tier0-agent/config/agent.json",
|
|
"agents/tier0-agent/credentials/approle.json"
|
|
],
|
|
requires_human=True
|
|
))
|
|
|
|
elif anomaly.phase == 8: # Production Hardening
|
|
if "health" in anomaly.message.lower():
|
|
suggestions.append(Suggestion(
|
|
id="",
|
|
anomaly_id=anomaly.id,
|
|
title="Implement health_manager.py (Phase 8 prerequisite)",
|
|
description="Health management is a critical Phase 8 component",
|
|
reasoning="Phase 8 Production Hardening requires health monitoring. "
|
|
"This is blocking other Phase 8 work (circuit breaker, alerts).",
|
|
fix_steps=[
|
|
"1. Create runtime/health_manager.py",
|
|
"2. Implement HealthStatus tracking",
|
|
"3. Add DragonflyDB persistence",
|
|
"4. Connect to evidence.py health checks",
|
|
"5. Update monitors.py with HealthWatcher"
|
|
],
|
|
risk=RiskLevel.MEDIUM,
|
|
impact=ImpactLevel.TRANSFORMATIVE,
|
|
estimated_effort="large",
|
|
files_affected=[
|
|
"runtime/health_manager.py",
|
|
"runtime/monitors.py",
|
|
"evidence/evidence.py"
|
|
],
|
|
requires_human=True
|
|
))
|
|
|
|
return suggestions
|
|
|
|
def _persist_suggestions(self, suggestions: list[Suggestion]):
|
|
"""Persist suggestions to storage"""
|
|
if not self._redis:
|
|
return
|
|
|
|
for suggestion in suggestions:
|
|
self._redis.lpush(
|
|
"oversight:suggestions",
|
|
json.dumps(asdict(suggestion))
|
|
)
|
|
|
|
# Keep only last 500 suggestions
|
|
self._redis.ltrim("oversight:suggestions", 0, 499)
|
|
|
|
def update_suggestion_status(
|
|
self,
|
|
suggestion_id: str,
|
|
status: SuggestionStatus,
|
|
reviewed_by: str = "system",
|
|
outcome: Optional[str] = None
|
|
) -> bool:
|
|
"""Update suggestion status after review"""
|
|
if not self._redis:
|
|
for sug in self.suggestions:
|
|
if sug.id == suggestion_id:
|
|
sug.status = status
|
|
sug.reviewed_at = self._now()
|
|
sug.reviewed_by = reviewed_by
|
|
sug.outcome = outcome
|
|
return True
|
|
return False
|
|
|
|
self._redis.hset(f"oversight:suggestion:{suggestion_id}", mapping={
|
|
"status": status.value,
|
|
"reviewed_at": self._now(),
|
|
"reviewed_by": reviewed_by,
|
|
"outcome": outcome or ""
|
|
})
|
|
|
|
return True
|
|
|
|
def get_suggestions(
|
|
self,
|
|
status: Optional[SuggestionStatus] = None,
|
|
limit: int = 50
|
|
) -> list[Suggestion]:
|
|
"""Get suggestions with optional status filter"""
|
|
if not self._redis:
|
|
filtered = self.suggestions
|
|
if status:
|
|
filtered = [s for s in filtered if s.status == status]
|
|
return sorted(filtered, key=lambda s: s.priority_score, reverse=True)[:limit]
|
|
|
|
raw = self._redis.lrange("oversight:suggestions", 0, limit - 1)
|
|
suggestions = []
|
|
|
|
for item in raw:
|
|
try:
|
|
data = json.loads(item)
|
|
|
|
# Convert enum strings back to enums
|
|
data['risk'] = RiskLevel(data['risk'])
|
|
data['impact'] = ImpactLevel(data['impact'])
|
|
data['status'] = SuggestionStatus(data['status'])
|
|
|
|
suggestion = Suggestion(**data)
|
|
|
|
if status and suggestion.status != status:
|
|
continue
|
|
|
|
suggestions.append(suggestion)
|
|
except Exception:
|
|
continue
|
|
|
|
return sorted(suggestions, key=lambda s: s.priority_score, reverse=True)
|
|
|
|
def get_summary(self) -> dict:
|
|
"""Get summary of suggestions"""
|
|
suggestions = self.get_suggestions(limit=500)
|
|
|
|
by_status = {s.value: 0 for s in SuggestionStatus}
|
|
by_risk = {r.value: 0 for r in RiskLevel}
|
|
by_impact = {i.value: 0 for i in ImpactLevel}
|
|
auto_fixable = 0
|
|
|
|
for s in suggestions:
|
|
# Handle both enum and string values
|
|
status_val = s.status.value if hasattr(s.status, 'value') else s.status
|
|
risk_val = s.risk.value if hasattr(s.risk, 'value') else s.risk
|
|
impact_val = s.impact.value if hasattr(s.impact, 'value') else s.impact
|
|
|
|
by_status[status_val] = by_status.get(status_val, 0) + 1
|
|
by_risk[risk_val] = by_risk.get(risk_val, 0) + 1
|
|
by_impact[impact_val] = by_impact.get(impact_val, 0) + 1
|
|
if s.auto_fixable:
|
|
auto_fixable += 1
|
|
|
|
return {
|
|
"total": len(suggestions),
|
|
"pending": by_status.get("pending", 0),
|
|
"approved": by_status.get("approved", 0),
|
|
"implemented": by_status.get("implemented", 0),
|
|
"auto_fixable": auto_fixable,
|
|
"by_status": by_status,
|
|
"by_risk": by_risk,
|
|
"by_impact": by_impact
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import argparse
|
|
from .bug_watcher import BugWindowWatcher
|
|
|
|
parser = argparse.ArgumentParser(description="Suggestion Engine")
|
|
parser.add_argument("command", choices=["analyze", "list", "status"])
|
|
parser.add_argument("--phase", type=int, help="Phase to analyze")
|
|
parser.add_argument("--json", action="store_true")
|
|
|
|
args = parser.parse_args()
|
|
|
|
engine = SuggestionEngine()
|
|
|
|
if args.command == "analyze":
|
|
watcher = BugWindowWatcher()
|
|
anomalies = watcher.scan_all_phases() if not args.phase else watcher.scan_phase(args.phase)
|
|
|
|
all_suggestions = []
|
|
for anomaly in anomalies:
|
|
suggestions = engine.generate_suggestions(anomaly)
|
|
all_suggestions.extend(suggestions)
|
|
|
|
if args.json:
|
|
print(json.dumps([asdict(s) for s in all_suggestions], indent=2))
|
|
else:
|
|
print(f"\n{'='*60}")
|
|
print(f"SUGGESTION ENGINE - Analysis Results")
|
|
print(f"{'='*60}")
|
|
print(f"Anomalies analyzed: {len(anomalies)}")
|
|
print(f"Suggestions generated: {len(all_suggestions)}")
|
|
print()
|
|
|
|
for s in all_suggestions[:10]:
|
|
print(f"[{s.risk.value.upper()}] {s.title}")
|
|
print(f" Impact: {s.impact.value} | Auto-fix: {s.auto_fixable}")
|
|
print(f" {s.reasoning[:100]}...")
|
|
print()
|
|
|
|
elif args.command == "list":
|
|
suggestions = engine.get_suggestions()
|
|
if args.json:
|
|
print(json.dumps([asdict(s) for s in suggestions], indent=2))
|
|
else:
|
|
for s in suggestions:
|
|
status_icon = {"pending": "⏳", "approved": "✅", "rejected": "❌", "implemented": "🎯"}.get(s.status.value, "❓")
|
|
print(f"{status_icon} [{s.id}] {s.title} ({s.risk.value}/{s.impact.value})")
|
|
|
|
elif args.command == "status":
|
|
summary = engine.get_summary()
|
|
if args.json:
|
|
print(json.dumps(summary, indent=2))
|
|
else:
|
|
print(f"\nSuggestion Engine Status")
|
|
print(f"Total: {summary['total']} | Pending: {summary['pending']} | Auto-fixable: {summary['auto_fixable']}")
|