// Package ai provides AI-powered infrastructure monitoring and investigation. // This file contains the unified AIIntelligence orchestrator that ties together // all AI subsystems into one coherent intelligence layer. package ai import ( "fmt" "sort" "strings" "sync" "time" "github.com/rcourtman/pulse-go-rewrite/internal/ai/baseline" "github.com/rcourtman/pulse-go-rewrite/internal/ai/correlation" "github.com/rcourtman/pulse-go-rewrite/internal/ai/knowledge" "github.com/rcourtman/pulse-go-rewrite/internal/ai/memory" "github.com/rcourtman/pulse-go-rewrite/internal/ai/patterns" "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources" ) // HealthGrade represents the overall health assessment type HealthGrade string const ( HealthGradeA HealthGrade = "A" // Excellent - no issues HealthGradeB HealthGrade = "B" // Good - minor issues HealthGradeC HealthGrade = "C" // Fair - some concerns HealthGradeD HealthGrade = "D" // Poor - needs attention HealthGradeF HealthGrade = "F" // Critical - immediate action needed ) // HealthTrend indicates the direction of health over time type HealthTrend string const ( HealthTrendImproving HealthTrend = "improving" HealthTrendStable HealthTrend = "stable" HealthTrendDeclining HealthTrend = "declining" ) // HealthFactor represents a single component affecting health type HealthFactor struct { Name string `json:"name"` Impact float64 `json:"impact"` // -1 to 1, negative is bad Description string `json:"description"` Category string `json:"category"` // finding, prediction, baseline, incident } // HealthScore represents the overall health of a resource or system type HealthScore struct { Score float64 `json:"score"` // 0-100 Grade HealthGrade `json:"grade"` // A, B, C, D, F Trend HealthTrend `json:"trend"` // improving, stable, declining Factors []HealthFactor `json:"factors"` // What's affecting the score Prediction string `json:"prediction"` // Human-readable outlook } // ResourceIntelligence aggregates all AI knowledge about a single resource type ResourceIntelligence struct { ResourceID string `json:"resource_id"` ResourceName string `json:"resource_name,omitempty"` ResourceType string `json:"resource_type,omitempty"` Health HealthScore `json:"health"` ActiveFindings []*Finding `json:"active_findings,omitempty"` Predictions []patterns.FailurePrediction `json:"predictions,omitempty"` Dependencies []string `json:"dependencies,omitempty"` // Resources this depends on Dependents []string `json:"dependents,omitempty"` // Resources that depend on this Correlations []*correlation.Correlation `json:"correlations,omitempty"` Baselines map[string]*baseline.FlatBaseline `json:"baselines,omitempty"` Anomalies []AnomalyReport `json:"anomalies,omitempty"` RecentIncidents []*memory.Incident `json:"recent_incidents,omitempty"` RecentChanges []unifiedresources.ResourceChange `json:"recent_changes,omitempty"` Knowledge *knowledge.GuestKnowledge `json:"knowledge,omitempty"` NoteCount int `json:"note_count"` } // AnomalyReport describes a metric that's deviating from baseline type AnomalyReport struct { Metric string `json:"metric"` CurrentValue float64 `json:"current_value"` BaselineMean float64 `json:"baseline_mean"` ZScore float64 `json:"z_score"` Severity baseline.AnomalySeverity `json:"severity"` Description string `json:"description"` } // IntelligenceSummary provides a system-wide intelligence overview type IntelligenceSummary struct { Timestamp time.Time `json:"timestamp"` OverallHealth HealthScore `json:"overall_health"` // Findings summary FindingsCount FindingsCounts `json:"findings_count"` TopFindings []*Finding `json:"top_findings,omitempty"` // Most critical // Predictions PredictionsCount int `json:"predictions_count"` UpcomingRisks []patterns.FailurePrediction `json:"upcoming_risks,omitempty"` // Recent activity RecentChangesCount int `json:"recent_changes_count"` RecentChanges []unifiedresources.ResourceChange `json:"recent_changes,omitempty"` RecentRemediations []memory.RemediationRecord `json:"recent_remediations,omitempty"` // Data governance posture PolicyPosture *unifiedresources.PolicyPostureSummary `json:"policy_posture,omitempty"` // Learning progress Learning LearningStats `json:"learning"` // Resources needing attention ResourcesAtRisk []ResourceRiskSummary `json:"resources_at_risk,omitempty"` } // FindingsCounts provides a breakdown of findings by severity type FindingsCounts struct { Critical int `json:"critical"` Warning int `json:"warning"` Watch int `json:"watch"` Info int `json:"info"` Total int `json:"total"` } // LearningStats shows how much the AI has learned type LearningStats struct { ResourcesWithKnowledge int `json:"resources_with_knowledge"` TotalNotes int `json:"total_notes"` ResourcesWithBaselines int `json:"resources_with_baselines"` PatternsDetected int `json:"patterns_detected"` CorrelationsLearned int `json:"correlations_learned"` IncidentsTracked int `json:"incidents_tracked"` } // ResourceRiskSummary is a brief summary of a resource at risk type ResourceRiskSummary struct { ResourceID string `json:"resource_id"` ResourceName string `json:"resource_name"` ResourceType string `json:"resource_type"` Health HealthScore `json:"health"` TopIssue string `json:"top_issue"` } // Intelligence orchestrates all AI subsystems into a unified system type Intelligence struct { mu sync.RWMutex // Core subsystems findings *FindingsStore patterns *patterns.Detector correlations *correlation.Detector baselines *baseline.Store incidents *memory.IncidentStore knowledge *knowledge.Store changes *memory.ChangeDetector remediations *memory.RemediationLog runHistoryStore *PatrolRunHistoryStore resourceTimelineStore unifiedresources.ResourceStore resourceTimelineStoreOrgID string unifiedResourceProvider UnifiedResourceProvider // State access stateProvider StateProvider // Optional hook for anomaly detection (used by patrol integration/tests) anomalyDetector func(resourceID string) []AnomalyReport // Configuration dataDir string } const ( intelligencePatrolCoverageWindow = 24 * time.Hour intelligenceRecentRunLimit = 10 ) // IntelligenceConfig configures the unified intelligence layer type IntelligenceConfig struct { DataDir string } // NewIntelligence creates a new unified intelligence orchestrator func NewIntelligence(cfg IntelligenceConfig) *Intelligence { return &Intelligence{ dataDir: cfg.DataDir, } } // SetSubsystems wires up all the AI subsystems func (i *Intelligence) SetSubsystems( findings *FindingsStore, patternsDetector *patterns.Detector, correlationsDetector *correlation.Detector, baselinesStore *baseline.Store, incidentsStore *memory.IncidentStore, knowledgeStore *knowledge.Store, changesDetector *memory.ChangeDetector, remediationsLog *memory.RemediationLog, ) { i.mu.Lock() defer i.mu.Unlock() i.findings = findings i.patterns = patternsDetector i.correlations = correlationsDetector i.baselines = baselinesStore i.incidents = incidentsStore i.knowledge = knowledgeStore i.changes = changesDetector i.remediations = remediationsLog } // SetResourceTimelineStore wires the canonical unified-resource timeline used // for recent change summaries when available. func (i *Intelligence) SetResourceTimelineStore(store unifiedresources.ResourceStore, orgID string) { i.mu.Lock() defer i.mu.Unlock() i.resourceTimelineStore = store i.resourceTimelineStoreOrgID = strings.TrimSpace(orgID) } // SetRunHistoryStore wires Patrol run history so intelligence summaries can // distinguish broad successful coverage from recent scoped or incomplete runs. func (i *Intelligence) SetRunHistoryStore(store *PatrolRunHistoryStore) { i.mu.Lock() defer i.mu.Unlock() i.runHistoryStore = store } // SetUnifiedResourceProvider wires the canonical unified resource provider used // for infrastructure-wide posture summaries. func (i *Intelligence) SetUnifiedResourceProvider(urp UnifiedResourceProvider) { i.mu.Lock() defer i.mu.Unlock() i.unifiedResourceProvider = urp } // SetStateProvider sets the state provider for current metrics func (i *Intelligence) SetStateProvider(sp StateProvider) { i.mu.Lock() defer i.mu.Unlock() i.stateProvider = sp } // GetSummary returns a comprehensive intelligence summary func (i *Intelligence) GetSummary() *IntelligenceSummary { summary := &IntelligenceSummary{ Timestamp: time.Now(), } i.mu.RLock() findings := i.findings patternsDetector := i.patterns remediations := i.remediations runHistoryStore := i.runHistoryStore unifiedResourceProvider := i.unifiedResourceProvider i.mu.RUnlock() // Aggregate findings if findings != nil { all := findings.GetActive(FindingSeverityInfo) // Get all active findings summary.FindingsCount = i.countFindings(all) summary.TopFindings = i.getTopFindings(all, 5) } // Aggregate predictions if patternsDetector != nil { predictions := patternsDetector.GetPredictions() summary.PredictionsCount = len(predictions) summary.UpcomingRisks = i.getUpcomingRisks(predictions, 5) } // Aggregate recent activity if recent := i.GetRecentChanges(time.Now().Add(-24*time.Hour), 100); len(recent) > 0 { summary.RecentChangesCount = len(recent) summary.RecentChanges = append([]unifiedresources.ResourceChange{}, recent[:min(len(recent), 5)]...) } if remediations != nil { recent := remediations.GetRecentRemediations(5, time.Now().Add(-24*time.Hour)) summary.RecentRemediations = recent } if unifiedResourceProvider != nil { summary.PolicyPosture = unifiedresources.SummarizePolicyPosture( unifiedresources.RefreshCanonicalMetadataSlice(unifiedResourceProvider.GetAll()), ) } // Learning stats summary.Learning = i.getLearningStats() // Calculate overall health summary.OverallHealth = i.calculateOverallHealth(summary, runHistoryStore) // Resources at risk summary.ResourcesAtRisk = i.getResourcesAtRisk(5) return summary } // GetResourceIntelligence returns aggregated intelligence for a specific resource func (i *Intelligence) GetResourceIntelligence(resourceID string) *ResourceIntelligence { i.mu.RLock() defer i.mu.RUnlock() intel := &ResourceIntelligence{ ResourceID: resourceID, } // Active findings if i.findings != nil { intel.ActiveFindings = i.findings.GetByResource(resourceID) if len(intel.ActiveFindings) > 0 { intel.ResourceName = intel.ActiveFindings[0].ResourceName intel.ResourceType = intel.ActiveFindings[0].ResourceType } } // Predictions if i.patterns != nil { intel.Predictions = i.patterns.GetPredictionsForResource(resourceID) } // Correlations and dependencies if i.correlations != nil { intel.Correlations = i.correlations.GetCorrelationsForResource(resourceID) intel.Dependencies = i.correlations.GetDependsOn(resourceID) intel.Dependents = i.correlations.GetDependencies(resourceID) } // Baselines if i.baselines != nil { if rb, ok := i.baselines.GetResourceBaseline(resourceID); ok { intel.Baselines = make(map[string]*baseline.FlatBaseline) for metric, mb := range rb.Metrics { intel.Baselines[metric] = &baseline.FlatBaseline{ ResourceID: resourceID, Metric: metric, Mean: mb.Mean, StdDev: mb.StdDev, Samples: mb.SampleCount, LastUpdate: rb.LastUpdated, } } } } // Recent incidents if i.incidents != nil { intel.RecentIncidents = i.incidents.ListIncidentsByResource(resourceID, 5) } // Recent changes if recentChanges := i.getRecentChangesForResource(resourceID, 5); len(recentChanges) > 0 { intel.RecentChanges = recentChanges } // Knowledge if i.knowledge != nil { if k, err := i.knowledge.GetKnowledge(resourceID); err == nil && k != nil { intel.Knowledge = k intel.NoteCount = len(k.Notes) if intel.ResourceName == "" && k.GuestName != "" { intel.ResourceName = k.GuestName } if intel.ResourceType == "" && k.GuestType != "" { intel.ResourceType = k.GuestType } } } // Calculate health score intel.Health = i.calculateResourceHealth(intel) return intel } // HasRecentChangesSource reports whether the intelligence layer has any source // available for recent infrastructure changes. func (i *Intelligence) HasRecentChangesSource() bool { i.mu.RLock() defer i.mu.RUnlock() return i.resourceTimelineStore != nil || i.changes != nil } // DescribeResource returns the canonical display name and type for a resource // when the unified resource provider is available. func (i *Intelligence) DescribeResource(resourceID string) (string, string) { resourceID = strings.TrimSpace(resourceID) if resourceID == "" { return "", "" } i.mu.RLock() unifiedResourceProvider := i.unifiedResourceProvider i.mu.RUnlock() if unifiedResourceProvider == nil { return "", "" } for _, resource := range unifiedresources.RefreshCanonicalMetadataSlice(unifiedResourceProvider.GetAll()) { if strings.TrimSpace(resource.ID) != resourceID { continue } return unifiedresources.ResourceDisplayName(resource), string(resource.Type) } return "", "" } // GetRecentChanges returns the most recent infrastructure changes across the // canonical unified-resource timeline, with patrol-local memory as fallback. func (i *Intelligence) GetRecentChanges(since time.Time, limit int) []unifiedresources.ResourceChange { if limit <= 0 { return nil } i.mu.RLock() resourceTimelineStore := i.resourceTimelineStore changesDetector := i.changes i.mu.RUnlock() if resourceTimelineStore != nil { if recent, err := resourceTimelineStore.GetRecentChanges("", since, limit); err == nil && len(recent) > 0 { return recent } } if changesDetector == nil { return nil } recent := changesDetector.GetRecentChanges(limit, since) if len(recent) == 0 { return nil } converted := make([]unifiedresources.ResourceChange, 0, len(recent)) for _, change := range recent { converted = append(converted, memory.ResourceChangeFromMemoryChange(change)) } return converted } // FormatContext builds a comprehensive context string for AI prompts func (i *Intelligence) FormatContext(resourceID string) string { i.mu.RLock() knowledgeStore := i.knowledge baselinesStore := i.baselines patternsDetector := i.patterns correlationsDetector := i.correlations incidentsStore := i.incidents resourceTimelineStore := i.resourceTimelineStore changesDetector := i.changes anomalyDetector := i.anomalyDetector i.mu.RUnlock() var sections []string // Knowledge (most important - what we've learned) if knowledgeStore != nil { if ctx := knowledgeStore.FormatForContext(resourceID); ctx != "" { sections = append(sections, ctx) } } // Baselines (what's normal for this resource) if baselinesStore != nil { if ctx := i.formatBaselinesForContext(resourceID); ctx != "" { sections = append(sections, ctx) } } // Current anomalies if anomalyDetector != nil { if anomalies := anomalyDetector(resourceID); len(anomalies) > 0 { sections = append(sections, i.formatAnomaliesForContext(anomalies)) } } // Canonical recent changes (preferred) with patrol-local fallback if recentChanges := i.buildRecentChangesContext( resourceID, resourceTimelineStore, changesDetector, false, 5, ); recentChanges != "" { sections = append(sections, recentChanges) } // Patterns/Predictions if patternsDetector != nil { if ctx := patternsDetector.FormatForContext(resourceID); ctx != "" { sections = append(sections, ctx) } } // Correlations if correlationsDetector != nil { if ctx := correlationsDetector.FormatForContext(resourceID); ctx != "" { sections = append(sections, ctx) } } // Incidents if incidentsStore != nil { if ctx := incidentsStore.FormatForResource(resourceID, 5); ctx != "" { sections = append(sections, ctx) } } return strings.Join(sections, "\n") } // FormatGlobalContext builds context for infrastructure-wide analysis func (i *Intelligence) FormatGlobalContext() string { i.mu.RLock() knowledgeStore := i.knowledge incidentsStore := i.incidents correlationsDetector := i.correlations patternsDetector := i.patterns resourceTimelineStore := i.resourceTimelineStore changesDetector := i.changes i.mu.RUnlock() var sections []string // All saved knowledge (limited) if knowledgeStore != nil { if ctx := knowledgeStore.FormatAllForContext(); ctx != "" { sections = append(sections, ctx) } } // Canonical recent changes across infrastructure (preferred) with patrol-local fallback if recentChanges := i.buildRecentChangesContext( "", resourceTimelineStore, changesDetector, true, 5, ); recentChanges != "" { sections = append(sections, recentChanges) } // Recent incidents across infrastructure if incidentsStore != nil { if ctx := incidentsStore.FormatForPatrol(8); ctx != "" { sections = append(sections, ctx) } } // Top correlations if correlationsDetector != nil { if ctx := correlationsDetector.FormatForContext(""); ctx != "" { sections = append(sections, ctx) } } // Top predictions if patternsDetector != nil { if ctx := patternsDetector.FormatForContext(""); ctx != "" { sections = append(sections, ctx) } } return strings.Join(sections, "\n") } func (i *Intelligence) buildRecentChangesContext(resourceID string, resourceTimelineStore unifiedresources.ResourceStore, changesDetector *memory.ChangeDetector, includeResourcePrefix bool, limit int) string { since := time.Now().Add(-24 * time.Hour) if resourceTimelineStore != nil { if recent, err := resourceTimelineStore.GetRecentChanges(resourceID, since, limit); err == nil && len(recent) > 0 { return unifiedresources.FormatResourceRecentChangesContext(recent, includeResourcePrefix, "##") } } if changesDetector == nil { return "" } var recent []memory.Change if resourceID != "" { recent = changesDetector.GetChangesForResource(resourceID, limit) } else { recent = changesDetector.GetRecentChanges(limit, since) } if len(recent) == 0 { return "" } return memory.FormatRecentChangesContext(recent, includeResourcePrefix, "##") } // RecordLearning saves a learning to the knowledge store after a fix func (i *Intelligence) RecordLearning(resourceID, resourceName, resourceType, title, content string) error { i.mu.RLock() defer i.mu.RUnlock() if i.knowledge == nil { return nil } return i.knowledge.SaveNote(resourceID, resourceName, resourceType, "learning", title, content) } // CheckBaselinesForResource checks current metrics against baselines and returns anomalies func (i *Intelligence) CheckBaselinesForResource(resourceID string, metrics map[string]float64) []AnomalyReport { i.mu.RLock() defer i.mu.RUnlock() if i.baselines == nil { return nil } var anomalies []AnomalyReport for metric, value := range metrics { severity, zScore, bl := i.baselines.CheckAnomaly(resourceID, metric, value) if severity != baseline.AnomalyNone && bl != nil { anomalies = append(anomalies, AnomalyReport{ Metric: metric, CurrentValue: value, BaselineMean: bl.Mean, ZScore: zScore, Severity: severity, Description: i.formatAnomalyDescription(metric, value, bl, zScore), }) } } return anomalies } // CreatePredictionFinding creates a finding from a prediction that's imminent func (i *Intelligence) CreatePredictionFinding(pred patterns.FailurePrediction) *Finding { severity := FindingSeverityWatch if pred.DaysUntil < 1 { severity = FindingSeverityWarning } if pred.Confidence > 0.8 && pred.DaysUntil < 1 { severity = FindingSeverityCritical } return &Finding{ ID: fmt.Sprintf("pred-%s-%s", pred.ResourceID, pred.EventType), Key: fmt.Sprintf("prediction:%s:%s", pred.ResourceID, pred.EventType), Severity: severity, Category: FindingCategoryReliability, ResourceID: pred.ResourceID, Title: fmt.Sprintf("Predicted: %s", pred.EventType), Description: pred.Basis, DetectedAt: time.Now(), LastSeenAt: time.Now(), } } // Helper methods func (i *Intelligence) countFindings(findings []*Finding) FindingsCounts { counts := FindingsCounts{} for _, f := range findings { if f == nil { continue } counts.Total++ switch f.Severity { case FindingSeverityCritical: counts.Critical++ case FindingSeverityWarning: counts.Warning++ case FindingSeverityWatch: counts.Watch++ case FindingSeverityInfo: counts.Info++ } } return counts } func (i *Intelligence) getTopFindings(findings []*Finding, limit int) []*Finding { if len(findings) == 0 { return nil } // Sort by severity (critical first) then by detection time (newest first) sorted := make([]*Finding, len(findings)) copy(sorted, findings) sort.Slice(sorted, func(a, b int) bool { sevA := severityOrder(sorted[a].Severity) sevB := severityOrder(sorted[b].Severity) if sevA != sevB { return sevA < sevB } return sorted[a].DetectedAt.After(sorted[b].DetectedAt) }) if len(sorted) > limit { sorted = sorted[:limit] } return sorted } func severityOrder(s FindingSeverity) int { switch s { case FindingSeverityCritical: return 0 case FindingSeverityWarning: return 1 case FindingSeverityWatch: return 2 case FindingSeverityInfo: return 3 default: return 4 } } func (i *Intelligence) getUpcomingRisks(predictions []patterns.FailurePrediction, limit int) []patterns.FailurePrediction { if len(predictions) == 0 { return nil } // Filter to next 7 days and sort by days until var upcoming []patterns.FailurePrediction for _, p := range predictions { if p.DaysUntil <= 7 && p.Confidence >= 0.5 { upcoming = append(upcoming, p) } } sort.Slice(upcoming, func(a, b int) bool { return upcoming[a].DaysUntil < upcoming[b].DaysUntil }) if len(upcoming) > limit { upcoming = upcoming[:limit] } return upcoming } func (i *Intelligence) getLearningStats() LearningStats { stats := LearningStats{} if i.knowledge != nil { guests, _ := i.knowledge.ListGuests() for _, guestID := range guests { if k, err := i.knowledge.GetKnowledge(guestID); err == nil && k != nil && len(k.Notes) > 0 { stats.ResourcesWithKnowledge++ stats.TotalNotes += len(k.Notes) } } } if i.baselines != nil { stats.ResourcesWithBaselines = i.baselines.ResourceCount() } if i.patterns != nil { p := i.patterns.GetPatterns() stats.PatternsDetected = len(p) } if i.correlations != nil { c := i.correlations.GetCorrelations() stats.CorrelationsLearned = len(c) } if i.incidents != nil { // Count is not available, so we skip this stat for now // Could be added to IncidentStore if needed stats.IncidentsTracked = 0 } return stats } // HasCorrelationsSource reports whether the intelligence layer can provide // learned correlation data. func (i *Intelligence) HasCorrelationsSource() bool { i.mu.RLock() defer i.mu.RUnlock() return i.correlations != nil } // GetCorrelations returns canonical learned correlations for the optional // resource scope. func (i *Intelligence) GetCorrelations(resourceID string) []*correlation.Correlation { i.mu.RLock() detector := i.correlations i.mu.RUnlock() if detector == nil { return nil } resourceID = strings.TrimSpace(resourceID) if resourceID != "" { return detector.GetCorrelationsForResource(resourceID) } return detector.GetCorrelations() } // FormatCorrelationsContext returns the canonical AI prompt section for learned // correlations. func (i *Intelligence) FormatCorrelationsContext(resourceID string) string { i.mu.RLock() detector := i.correlations i.mu.RUnlock() if detector == nil { return "" } return detector.FormatForContext(resourceID) } func (i *Intelligence) calculateOverallHealth(summary *IntelligenceSummary, runHistoryStore *PatrolRunHistoryStore) HealthScore { health := HealthScore{ Score: 100, Grade: HealthGradeA, Trend: HealthTrendStable, Factors: []HealthFactor{}, } // Deduct for findings if summary.FindingsCount.Critical > 0 { impact := float64(summary.FindingsCount.Critical) * 20 if impact > 40 { impact = 40 } health.Score -= impact health.Factors = append(health.Factors, HealthFactor{ Name: "Critical findings", Impact: -impact / 100, Description: fmt.Sprintf("%d critical issues need immediate attention", summary.FindingsCount.Critical), Category: "finding", }) } if summary.FindingsCount.Warning > 0 { impact := float64(summary.FindingsCount.Warning) * 10 if impact > 20 { impact = 20 } health.Score -= impact health.Factors = append(health.Factors, HealthFactor{ Name: "Warnings", Impact: -impact / 100, Description: fmt.Sprintf("%d warnings need attention soon", summary.FindingsCount.Warning), Category: "finding", }) } // Deduct for imminent predictions for _, pred := range summary.UpcomingRisks { if pred.DaysUntil < 3 && pred.Confidence > 0.7 { impact := 10.0 health.Score -= impact health.Factors = append(health.Factors, HealthFactor{ Name: "Predicted issue", Impact: -impact / 100, Description: fmt.Sprintf("%s predicted within %.1f days", pred.EventType, pred.DaysUntil), Category: "prediction", }) } } if factor, ok := summarizeRecentPatrolCoverage(runHistoryStore, time.Now()); ok { health.Score -= factor.impact health.Factors = append(health.Factors, HealthFactor{ Name: factor.name, Impact: -factor.impact / 100, Description: factor.description, Category: "coverage", }) } // Bonus for learning progress if summary.Learning.ResourcesWithKnowledge > 5 { bonus := 5.0 health.Score += bonus health.Factors = append(health.Factors, HealthFactor{ Name: "Knowledge learned", Impact: bonus / 100, Description: fmt.Sprintf("Pulse Patrol has learned about %d resources", summary.Learning.ResourcesWithKnowledge), Category: "learning", }) } // Clamp score if health.Score < 0 { health.Score = 0 } if health.Score > 100 { health.Score = 100 } // Assign grade health.Grade = scoreToGrade(health.Score) // Generate prediction text health.Prediction = i.generateHealthPrediction(health, summary) return health } func (i *Intelligence) calculateResourceHealth(intel *ResourceIntelligence) HealthScore { health := HealthScore{ Score: 100, Grade: HealthGradeA, Trend: HealthTrendStable, Factors: []HealthFactor{}, } // Deduct for active findings for _, f := range intel.ActiveFindings { if f == nil { continue } var impact float64 switch f.Severity { case FindingSeverityCritical: impact = 30 case FindingSeverityWarning: impact = 15 case FindingSeverityWatch: impact = 5 case FindingSeverityInfo: impact = 2 } health.Score -= impact health.Factors = append(health.Factors, HealthFactor{ Name: f.Title, Impact: -impact / 100, Description: f.Description, Category: "finding", }) } // Deduct for predictions for _, p := range intel.Predictions { if p.DaysUntil < 7 && p.Confidence > 0.5 { impact := 10.0 * p.Confidence health.Score -= impact health.Factors = append(health.Factors, HealthFactor{ Name: "Predicted: " + string(p.EventType), Impact: -impact / 100, Description: p.Basis, Category: "prediction", }) } } // Deduct for anomalies for _, a := range intel.Anomalies { var impact float64 switch a.Severity { case baseline.AnomalyCritical: impact = 20 case baseline.AnomalyHigh: impact = 10 case baseline.AnomalyMedium: impact = 5 case baseline.AnomalyLow: impact = 2 } health.Score -= impact health.Factors = append(health.Factors, HealthFactor{ Name: a.Metric + " anomaly", Impact: -impact / 100, Description: a.Description, Category: "baseline", }) } // Bonus for having knowledge if intel.NoteCount > 0 { bonus := 2.0 health.Score += bonus health.Factors = append(health.Factors, HealthFactor{ Name: "Documented", Impact: bonus / 100, Description: fmt.Sprintf("%d notes saved for this resource", intel.NoteCount), Category: "learning", }) } // Clamp if health.Score < 0 { health.Score = 0 } if health.Score > 100 { health.Score = 100 } health.Grade = scoreToGrade(health.Score) return health } func scoreToGrade(score float64) HealthGrade { switch { case score >= 90: return HealthGradeA case score >= 75: return HealthGradeB case score >= 60: return HealthGradeC case score >= 40: return HealthGradeD default: return HealthGradeF } } func (i *Intelligence) generateHealthPrediction(health HealthScore, summary *IntelligenceSummary) string { for _, factor := range health.Factors { if factor.Category == "coverage" { return factor.Description } } if health.Grade == HealthGradeA { return "Infrastructure is healthy with no significant issues detected." } if summary.FindingsCount.Critical > 0 { return fmt.Sprintf("Immediate attention required: %d critical issues.", summary.FindingsCount.Critical) } if len(summary.UpcomingRisks) > 0 { risk := summary.UpcomingRisks[0] return fmt.Sprintf("Predicted %s event on resource within %.1f days (%.0f%% confidence).", risk.EventType, risk.DaysUntil, risk.Confidence*100) } if summary.FindingsCount.Warning > 0 { return fmt.Sprintf("%d warnings should be addressed soon to maintain stability.", summary.FindingsCount.Warning) } return "Infrastructure is stable with minor issues to monitor." } type patrolCoverageFactor struct { name string description string impact float64 } func summarizeRecentPatrolCoverage( runHistoryStore *PatrolRunHistoryStore, now time.Time, ) (patrolCoverageFactor, bool) { if runHistoryStore == nil { return patrolCoverageFactor{}, false } recentRuns := runHistoryStore.GetRecent(intelligenceRecentRunLimit) if len(recentRuns) == 0 { return patrolCoverageFactor{}, false } cutoff := now.Add(-intelligencePatrolCoverageWindow) relevant := make([]PatrolRunRecord, 0, len(recentRuns)) for _, run := range recentRuns { if run.CompletedAt.IsZero() || run.CompletedAt.Before(cutoff) { continue } relevant = append(relevant, run) } if len(relevant) == 0 { return patrolCoverageFactor{}, false } var recentErrors int var hasSuccessfulFullRun bool var hasRecentFullRun bool var scopedRuns int for _, run := range relevant { if run.ErrorCount > 0 || strings.EqualFold(strings.TrimSpace(run.Status), "error") { recentErrors++ } if !isScopedPatrolRun(run) { hasRecentFullRun = true } if isSuccessfulFullPatrolRun(run) { hasSuccessfulFullRun = true } if isScopedPatrolRun(run) { scopedRuns++ } } switch { case !hasSuccessfulFullRun && hasRecentFullRun && recentErrors > 0: return patrolCoverageFactor{ name: "Patrol coverage incomplete", description: "Patrol coverage is incomplete: a recent full patrol ended with errors, so overall health is not fully verified.", impact: 35, }, true case !hasSuccessfulFullRun && recentErrors > 0: return patrolCoverageFactor{ name: "Patrol coverage incomplete", description: "Patrol coverage is incomplete: recent activity was limited to scoped runs and ended with errors, so overall health is not fully verified.", impact: 35, }, true case !hasSuccessfulFullRun && scopedRuns == len(relevant): return patrolCoverageFactor{ name: "Patrol coverage incomplete", description: "Patrol coverage is incomplete: recent activity was limited to scoped runs, so overall infrastructure health is not fully verified.", impact: 20, }, true case recentErrors > 0: return patrolCoverageFactor{ name: "Recent Patrol errors", description: "Recent Patrol runs encountered errors, so the current health summary may be incomplete.", impact: 10, }, true default: return patrolCoverageFactor{}, false } } func isScopedPatrolRun(run PatrolRunRecord) bool { return strings.EqualFold(strings.TrimSpace(run.Type), "scoped") } func isSuccessfulFullPatrolRun(run PatrolRunRecord) bool { return !isScopedPatrolRun(run) && run.ErrorCount == 0 && !strings.EqualFold(strings.TrimSpace(run.Status), "error") } func (i *Intelligence) getResourcesAtRisk(limit int) []ResourceRiskSummary { if i.findings == nil { return nil } // Group findings by resource byResource := make(map[string][]*Finding) for _, f := range i.findings.GetActive(FindingSeverityInfo) { byResource[f.ResourceID] = append(byResource[f.ResourceID], f) } // Calculate risk for each resource type resourceRisk struct { id string name string rtype string score float64 top string } var risks []resourceRisk for id, findings := range byResource { score := 0.0 var topFinding *Finding for _, f := range findings { switch f.Severity { case FindingSeverityCritical: score += 30 case FindingSeverityWarning: score += 15 case FindingSeverityWatch: score += 5 case FindingSeverityInfo: score += 2 } if topFinding == nil || severityOrder(f.Severity) < severityOrder(topFinding.Severity) { topFinding = f } } if score > 0 && topFinding != nil { risks = append(risks, resourceRisk{ id: id, name: topFinding.ResourceName, rtype: topFinding.ResourceType, score: score, top: topFinding.Title, }) } } // Sort by risk score descending sort.Slice(risks, func(a, b int) bool { return risks[a].score > risks[b].score }) if len(risks) > limit { risks = risks[:limit] } // Convert to summaries var summaries []ResourceRiskSummary for _, r := range risks { health := HealthScore{ Score: 100 - r.score, Grade: scoreToGrade(100 - r.score), } summaries = append(summaries, ResourceRiskSummary{ ResourceID: r.id, ResourceName: r.name, ResourceType: r.rtype, Health: health, TopIssue: r.top, }) } return summaries } func (i *Intelligence) detectCurrentAnomalies(resourceID string) []AnomalyReport { if i.anomalyDetector != nil { return i.anomalyDetector(resourceID) } // This would be called with current metrics from state // For now, return empty - will be integrated with patrol return nil } func (i *Intelligence) getRecentChangesForResource(resourceID string, limit int) []unifiedresources.ResourceChange { resourceID = strings.TrimSpace(resourceID) if resourceID == "" || limit <= 0 { return nil } since := time.Now().Add(-24 * time.Hour) i.mu.RLock() resourceTimelineStore := i.resourceTimelineStore changesDetector := i.changes i.mu.RUnlock() if resourceTimelineStore != nil { if recent, err := resourceTimelineStore.GetRecentChanges(resourceID, since, limit); err == nil && len(recent) > 0 { return recent } } if changesDetector == nil { return nil } recent := changesDetector.GetChangesForResource(resourceID, limit) if len(recent) == 0 { return nil } converted := make([]unifiedresources.ResourceChange, 0, len(recent)) for _, change := range recent { converted = append(converted, memory.ResourceChangeFromMemoryChange(change)) } return converted } func (i *Intelligence) formatBaselinesForContext(resourceID string) string { i.mu.RLock() store := i.baselines i.mu.RUnlock() if store == nil { return "" } rb, ok := store.GetResourceBaseline(resourceID) if !ok || len(rb.Metrics) == 0 { return "" } var lines []string lines = append(lines, "\n## Learned Baselines") lines = append(lines, "Normal operating ranges for this resource:") for metric, mb := range rb.Metrics { lines = append(lines, fmt.Sprintf("- %s: mean %.1f, stddev %.1f (samples: %d)", metric, mb.Mean, mb.StdDev, mb.SampleCount)) } return strings.Join(lines, "\n") } func (i *Intelligence) formatAnomaliesForContext(anomalies []AnomalyReport) string { if len(anomalies) == 0 { return "" } var lines []string lines = append(lines, "\n## Current Anomalies") lines = append(lines, "Metrics deviating from normal:") for _, a := range anomalies { lines = append(lines, fmt.Sprintf("- %s: %s", a.Metric, a.Description)) } return strings.Join(lines, "\n") } func (i *Intelligence) formatAnomalyDescription(_ string, value float64, bl *baseline.MetricBaseline, zScore float64) string { direction := "above" if zScore < 0 { direction = "below" } return fmt.Sprintf("%.1f is %.1f std devs %s baseline (mean: %.1f)", value, absFloatIntel(zScore), direction, bl.Mean) } // absFloatIntel is a local helper (service.go has its own) func absFloatIntel(f float64) float64 { if f < 0 { return -f } return f }