Phase 1 of Pulse AI differentiation: - Create internal/ai/context package with types, trends, builder, formatter - Implement linear regression for trend computation (growing/declining/stable/volatile) - Add storage capacity predictions (predicts days until 90% and 100%) - Wire MetricsHistory from monitor to patrol service - Update patrol to use buildEnrichedContext instead of basic summary - Update patrol prompt to reference trend indicators and predictions This gives the AI awareness of historical patterns, enabling it to: - Identify resources with concerning growth rates - Predict capacity exhaustion before it happens - Distinguish between stable high usage vs growing problems - Provide more actionable, time-aware insights All tests passing. Falls back to basic summary if metrics history unavailable.
709 lines
20 KiB
Markdown
709 lines
20 KiB
Markdown
# Pulse AI Implementation Plan
|
|
|
|
This document outlines the concrete implementation steps to realize the Pulse AI vision.
|
|
|
|
---
|
|
|
|
## Current State Audit
|
|
|
|
### What We Have
|
|
|
|
| Component | Location | Status |
|
|
|-----------|----------|--------|
|
|
| Real-time state | `models.StateSnapshot` | ✅ Complete |
|
|
| Metrics collection | `monitoring.MetricsHistory` | ✅ Collecting, exposed to AI |
|
|
| Finding persistence | `ai.FindingsStore` | ✅ Works |
|
|
| Knowledge store | `ai/knowledge.Store` | ✅ Per-guest notes |
|
|
| Alert context | `ai.buildAlertContext()` | ✅ Current alerts only |
|
|
| User annotations | `buildUserAnnotationsContext()` | ✅ Basic |
|
|
| Base patrol | `patrol.go` | ✅ Heuristics + optional AI |
|
|
| **AI Context package** | `ai/context/` | ✅ **NEW - Phase 1** |
|
|
| **Trend computation** | `ai/context/trends.go` | ✅ **NEW - Linear regression** |
|
|
| **Context builder** | `ai/context/builder.go` | ✅ **NEW - Orchestration** |
|
|
| **Metrics adapter** | `ai/metrics_history_adapter.go` | ✅ **NEW - Wiring** |
|
|
|
|
### What's Missing
|
|
|
|
| Component | Impact | Priority | Status |
|
|
|-----------|--------|----------|--------|
|
|
| Historical context for AI | Core differentiator | P0 | ✅ Done |
|
|
| Trend computation | Predictive capability | P0 | ✅ Done |
|
|
| Baseline learning | Anomaly detection | P1 | 🔲 Next |
|
|
| Change detection | Root cause analysis | P1 | 🔲 Planned |
|
|
| Remediation logging | Operational memory | P2 | 🔲 Planned |
|
|
| Correlation engine | Advanced insights | P2 | 🔲 Future |
|
|
| Capacity forecasting | Proactive alerts | P1 | ⚡ Partial (storage predictions) |
|
|
|
|
---
|
|
|
|
## Phase 1: Foundation - AI Context Package
|
|
|
|
**Goal**: Create a clean abstraction for building AI context with historical data.
|
|
|
|
### 1.1 New Package Structure
|
|
|
|
```
|
|
internal/ai/context/
|
|
├── builder.go # Main context builder orchestrator
|
|
├── current.go # Current state formatting (refactor from patrol)
|
|
├── historical.go # Historical metrics integration
|
|
├── trends.go # Trend computation
|
|
├── insights.go # Combined insights (anomalies, predictions)
|
|
├── formatter.go # AI-friendly text formatting
|
|
└── types.go # Shared types
|
|
```
|
|
|
|
### 1.2 Core Types
|
|
|
|
```go
|
|
// types.go
|
|
|
|
// ResourceContext contains all context for a single resource
|
|
type ResourceContext struct {
|
|
ResourceID string
|
|
ResourceType string // "node", "vm", "container", "storage", "docker_host"
|
|
ResourceName string
|
|
|
|
// Current state
|
|
Current CurrentState
|
|
|
|
// Historical analysis
|
|
Trends map[string]Trend // metric -> trend
|
|
Baselines map[string]Baseline // metric -> baseline
|
|
Anomalies []Anomaly
|
|
|
|
// Operational memory
|
|
PastFindings []FindingSummary
|
|
UserNotes []string
|
|
RecentChanges []Change
|
|
LastRemediation *RemediationRecord
|
|
}
|
|
|
|
// Trend represents the direction and rate of change for a metric
|
|
type Trend struct {
|
|
Metric string
|
|
Direction TrendDirection // stable, growing, declining, volatile
|
|
RatePerHour float64 // rate of change per hour
|
|
RatePerDay float64 // rate of change per day
|
|
Current float64
|
|
Average24h float64
|
|
Average7d float64
|
|
Min24h float64
|
|
Max24h float64
|
|
DataPoints int // how much history we have
|
|
Confidence float64 // 0-1, based on data quality
|
|
}
|
|
|
|
type TrendDirection string
|
|
const (
|
|
TrendStable TrendDirection = "stable"
|
|
TrendGrowing TrendDirection = "growing"
|
|
TrendDeclining TrendDirection = "declining"
|
|
TrendVolatile TrendDirection = "volatile"
|
|
)
|
|
|
|
// Baseline represents learned "normal" for a metric
|
|
type Baseline struct {
|
|
Metric string
|
|
Mean float64
|
|
StdDev float64
|
|
P5 float64 // 5th percentile
|
|
P95 float64 // 95th percentile
|
|
SampleSize int
|
|
LearnedAt time.Time
|
|
}
|
|
|
|
// Anomaly represents a detected deviation from normal
|
|
type Anomaly struct {
|
|
Metric string
|
|
Current float64
|
|
Expected float64 // baseline mean
|
|
Deviation float64 // standard deviations from mean
|
|
Severity string // "low", "medium", "high", "critical"
|
|
Since time.Time
|
|
Description string
|
|
}
|
|
|
|
// Prediction represents a forecasted event
|
|
type Prediction struct {
|
|
ResourceID string
|
|
Metric string
|
|
Event string // "capacity_full", "oom", "pattern_repeat"
|
|
ETA time.Time
|
|
Confidence float64
|
|
Basis string // explanation of prediction
|
|
}
|
|
```
|
|
|
|
### 1.3 Context Builder
|
|
|
|
```go
|
|
// builder.go
|
|
|
|
type ContextBuilder struct {
|
|
stateProvider StateProvider
|
|
metricsHistory *monitoring.MetricsHistory
|
|
findingsStore *FindingsStore
|
|
knowledgeStore *knowledge.Store
|
|
baselineStore *BaselineStore
|
|
|
|
// Configuration
|
|
includeTrends bool
|
|
includeBaselines bool
|
|
includeHistory bool
|
|
historicalWindow time.Duration
|
|
}
|
|
|
|
// BuildForResource creates comprehensive context for a single resource
|
|
func (b *ContextBuilder) BuildForResource(resourceID string) (*ResourceContext, error)
|
|
|
|
// BuildForInfrastructure creates summarized context for all infrastructure
|
|
func (b *ContextBuilder) BuildForInfrastructure() (*InfrastructureContext, error)
|
|
|
|
// FormatForAI converts context to AI-consumable markdown
|
|
func (b *ContextBuilder) FormatForAI(ctx *ResourceContext) string
|
|
|
|
// FormatInfrastructureForAI converts full infrastructure context
|
|
func (b *ContextBuilder) FormatInfrastructureForAI(ctx *InfrastructureContext) string
|
|
```
|
|
|
|
### 1.4 Trend Computation
|
|
|
|
```go
|
|
// trends.go
|
|
|
|
// ComputeTrend calculates trend from historical data points
|
|
func ComputeTrend(points []monitoring.MetricPoint, window time.Duration) Trend {
|
|
if len(points) < 2 {
|
|
return Trend{Confidence: 0}
|
|
}
|
|
|
|
// Calculate basic statistics
|
|
avg, min, max, stddev := computeStats(points)
|
|
|
|
// Linear regression for direction and rate
|
|
slope, r2 := linearRegression(points)
|
|
|
|
// Classify direction
|
|
direction := classifyTrend(slope, stddev, avg)
|
|
|
|
// Rate per hour/day
|
|
ratePerHour := slope * 3600 // slope is per second
|
|
ratePerDay := ratePerHour * 24
|
|
|
|
return Trend{
|
|
Direction: direction,
|
|
RatePerHour: ratePerHour,
|
|
RatePerDay: ratePerDay,
|
|
Current: points[len(points)-1].Value,
|
|
Average24h: avg,
|
|
Min24h: min,
|
|
Max24h: max,
|
|
DataPoints: len(points),
|
|
Confidence: r2,
|
|
}
|
|
}
|
|
|
|
func classifyTrend(slope, stddev, avg float64) TrendDirection {
|
|
// Normalize slope relative to value magnitude
|
|
if avg == 0 {
|
|
avg = 1 // avoid division by zero
|
|
}
|
|
normalizedSlope := (slope * 3600) / avg // hourly change as fraction of avg
|
|
|
|
// Threshold based on volatility
|
|
threshold := 0.01 // 1% per hour is significant
|
|
|
|
if stddev/avg > 0.2 {
|
|
return TrendVolatile
|
|
}
|
|
if normalizedSlope > threshold {
|
|
return TrendGrowing
|
|
}
|
|
if normalizedSlope < -threshold {
|
|
return TrendDeclining
|
|
}
|
|
return TrendStable
|
|
}
|
|
```
|
|
|
|
### 1.5 Integration with Existing Code
|
|
|
|
```go
|
|
// In patrol.go, replace buildInfrastructureSummary:
|
|
|
|
func (p *PatrolService) buildEnrichedContext(state models.StateSnapshot) string {
|
|
builder := context.NewBuilder(
|
|
p.stateProvider,
|
|
p.metricsHistory,
|
|
p.findings,
|
|
p.knowledgeStore,
|
|
p.baselineStore,
|
|
)
|
|
|
|
infraCtx, err := builder.BuildForInfrastructure()
|
|
if err != nil {
|
|
log.Warn().Err(err).Msg("Failed to build enriched context, falling back")
|
|
return p.buildBasicSummary(state)
|
|
}
|
|
|
|
return builder.FormatInfrastructureForAI(infraCtx)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 2: Baseline Learning
|
|
|
|
**Goal**: Learn what "normal" looks like for each resource so we can detect anomalies.
|
|
|
|
### 2.1 Baseline Store
|
|
|
|
```go
|
|
// internal/ai/baseline/store.go
|
|
|
|
type Store struct {
|
|
mu sync.RWMutex
|
|
baselines map[string]*ResourceBaseline // resourceID -> baselines
|
|
|
|
persistence Persistence
|
|
|
|
// Configuration
|
|
learningWindow time.Duration // how far back to learn from (default: 7 days)
|
|
minSamples int // minimum samples needed (default: 100)
|
|
updateInterval time.Duration // how often to recompute (default: 1 hour)
|
|
}
|
|
|
|
type ResourceBaseline struct {
|
|
ResourceID string
|
|
LastUpdated time.Time
|
|
|
|
Metrics map[string]*MetricBaseline // metric name -> baseline
|
|
}
|
|
|
|
type MetricBaseline struct {
|
|
Mean float64
|
|
StdDev float64
|
|
Percentiles map[int]float64 // 5, 25, 50, 75, 95
|
|
SampleCount int
|
|
|
|
// Time-of-day patterns (optional, phase 2+)
|
|
HourlyMeans [24]float64
|
|
}
|
|
|
|
// Learn computes baselines from historical data
|
|
func (s *Store) Learn(resourceID string, history *monitoring.MetricsHistory) error
|
|
|
|
// GetBaseline returns the baseline for a resource/metric
|
|
func (s *Store) GetBaseline(resourceID, metric string) (*MetricBaseline, bool)
|
|
|
|
// IsAnomaly checks if a value is anomalous given the baseline
|
|
func (s *Store) IsAnomaly(resourceID, metric string, value float64) (bool, float64)
|
|
```
|
|
|
|
### 2.2 Background Learning Loop
|
|
|
|
```go
|
|
// Run as part of patrol service or separate goroutine
|
|
|
|
func (s *Store) StartLearningLoop(ctx context.Context, interval time.Duration) {
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
s.updateAllBaselines()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Store) updateAllBaselines() {
|
|
// Get list of all resources with metrics
|
|
resources := s.metricsHistory.GetResourceIDs()
|
|
|
|
for _, resourceID := range resources {
|
|
if err := s.Learn(resourceID, s.metricsHistory); err != nil {
|
|
log.Warn().Err(err).Str("resource", resourceID).Msg("Failed to update baseline")
|
|
}
|
|
}
|
|
|
|
// Persist updated baselines
|
|
s.save()
|
|
}
|
|
```
|
|
|
|
### 2.3 Anomaly Detection
|
|
|
|
```go
|
|
// internal/ai/anomaly/detector.go
|
|
|
|
type Detector struct {
|
|
baselineStore *baseline.Store
|
|
|
|
// Thresholds
|
|
warningThreshold float64 // default: 2.0 std devs
|
|
criticalThreshold float64 // default: 3.0 std devs
|
|
}
|
|
|
|
type Detection struct {
|
|
ResourceID string
|
|
Metric string
|
|
CurrentValue float64
|
|
ExpectedMean float64
|
|
StdDev float64
|
|
ZScore float64
|
|
Severity AnomalySeverity
|
|
DetectedAt time.Time
|
|
}
|
|
|
|
func (d *Detector) Check(resourceID, metric string, value float64) *Detection {
|
|
baseline, ok := d.baselineStore.GetBaseline(resourceID, metric)
|
|
if !ok || baseline.SampleCount < 50 {
|
|
return nil // not enough data yet
|
|
}
|
|
|
|
zScore := (value - baseline.Mean) / baseline.StdDev
|
|
absZ := math.Abs(zScore)
|
|
|
|
if absZ < d.warningThreshold {
|
|
return nil // within normal range
|
|
}
|
|
|
|
severity := AnomalyWarning
|
|
if absZ >= d.criticalThreshold {
|
|
severity = AnomalyCritical
|
|
}
|
|
|
|
return &Detection{
|
|
ResourceID: resourceID,
|
|
Metric: metric,
|
|
CurrentValue: value,
|
|
ExpectedMean: baseline.Mean,
|
|
StdDev: baseline.StdDev,
|
|
ZScore: zScore,
|
|
Severity: severity,
|
|
DetectedAt: time.Now(),
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 3: Operational Memory
|
|
|
|
**Goal**: Remember what happened, what users said, and what worked.
|
|
|
|
### 3.1 Change Detection
|
|
|
|
```go
|
|
// internal/ai/memory/changes.go
|
|
|
|
type ChangeDetector struct {
|
|
previousState map[string]ResourceSnapshot
|
|
mu sync.RWMutex
|
|
|
|
changes []Change
|
|
maxChanges int
|
|
persistence Persistence
|
|
}
|
|
|
|
type Change struct {
|
|
ID string
|
|
ResourceID string
|
|
ChangeType ChangeType
|
|
Before interface{}
|
|
After interface{}
|
|
DetectedAt time.Time
|
|
Description string
|
|
}
|
|
|
|
type ChangeType string
|
|
const (
|
|
ChangeCreated ChangeType = "created"
|
|
ChangeDeleted ChangeType = "deleted"
|
|
ChangeConfig ChangeType = "config" // RAM, CPU allocation changed
|
|
ChangeStatus ChangeType = "status" // started, stopped
|
|
ChangeMigrated ChangeType = "migrated" // moved to different node
|
|
)
|
|
|
|
func (d *ChangeDetector) Detect(current models.StateSnapshot) []Change {
|
|
// Compare current state to previous
|
|
// Detect new resources, deleted resources, config changes
|
|
// Store changes and return new ones
|
|
}
|
|
```
|
|
|
|
### 3.2 Remediation Logging
|
|
|
|
```go
|
|
// internal/ai/memory/remediation.go
|
|
|
|
type RemediationLog struct {
|
|
mu sync.RWMutex
|
|
records []RemediationRecord
|
|
|
|
persistence Persistence
|
|
}
|
|
|
|
type RemediationRecord struct {
|
|
ID string
|
|
Timestamp time.Time
|
|
ResourceID string
|
|
FindingID string // linked AI finding if any
|
|
Problem string // what was wrong
|
|
Action string // what was done
|
|
Outcome Outcome // did it work?
|
|
Duration time.Duration // how long until resolved
|
|
Note string // optional user/AI note
|
|
}
|
|
|
|
type Outcome string
|
|
const (
|
|
OutcomeResolved Outcome = "resolved"
|
|
OutcomePartial Outcome = "partial"
|
|
OutcomeFailed Outcome = "failed"
|
|
OutcomeUnknown Outcome = "unknown"
|
|
)
|
|
|
|
// Log records a remediation action
|
|
func (r *RemediationLog) Log(record RemediationRecord) error
|
|
|
|
// GetForResource returns remediation history for a resource
|
|
func (r *RemediationLog) GetForResource(resourceID string, limit int) []RemediationRecord
|
|
|
|
// GetSimilar finds similar past remediations
|
|
func (r *RemediationLog) GetSimilar(problem string, limit int) []RemediationRecord
|
|
```
|
|
|
|
### 3.3 Integration Points
|
|
|
|
When the AI executes a command:
|
|
```go
|
|
func (s *Service) onToolComplete(toolID, command, output string, success bool) {
|
|
// Log the remediation attempt
|
|
s.remediationLog.Log(RemediationRecord{
|
|
ID: uuid.New().String(),
|
|
Timestamp: time.Now(),
|
|
ResourceID: s.currentContext.TargetID,
|
|
FindingID: s.currentContext.FindingID,
|
|
Problem: s.currentContext.Problem,
|
|
Action: command,
|
|
Outcome: outcomeFromSuccess(success),
|
|
})
|
|
}
|
|
```
|
|
|
|
When a finding is resolved:
|
|
```go
|
|
func (s *FindingsStore) Resolve(findingID string, auto bool) bool {
|
|
// Link to any remediation actions
|
|
// Record what was done
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 4: Capacity Forecasting
|
|
|
|
**Goal**: Predict when resources will run out.
|
|
|
|
### 4.1 Forecaster
|
|
|
|
```go
|
|
// internal/ai/forecast/capacity.go
|
|
|
|
type CapacityForecaster struct {
|
|
metricsHistory *monitoring.MetricsHistory
|
|
minDataPoints int // minimum points needed for forecast
|
|
}
|
|
|
|
type CapacityForecast struct {
|
|
ResourceID string
|
|
Metric string
|
|
CurrentUsage float64
|
|
Limit float64
|
|
|
|
GrowthRate float64 // per day
|
|
ETA time.Time // when it hits limit
|
|
DaysLeft float64
|
|
Confidence float64 // 0-1
|
|
|
|
// Projection points for visualization
|
|
Projection []ProjectionPoint
|
|
}
|
|
|
|
func (f *CapacityForecaster) Forecast(resourceID, metric string, limit float64) (*CapacityForecast, error) {
|
|
points := f.metricsHistory.GetMetrics(resourceID, metric, 7*24*time.Hour)
|
|
if len(points) < f.minDataPoints {
|
|
return nil, ErrInsufficientData
|
|
}
|
|
|
|
// Linear regression for growth rate
|
|
slope, r2 := linearRegression(points)
|
|
if slope <= 0 {
|
|
return nil, nil // not growing
|
|
}
|
|
|
|
current := points[len(points)-1].Value
|
|
remaining := limit - current
|
|
hoursUntilFull := remaining / (slope * 3600)
|
|
|
|
if hoursUntilFull <= 0 {
|
|
return nil, nil // already at limit
|
|
}
|
|
|
|
eta := time.Now().Add(time.Duration(hoursUntilFull) * time.Hour)
|
|
|
|
return &CapacityForecast{
|
|
ResourceID: resourceID,
|
|
Metric: metric,
|
|
CurrentUsage: current,
|
|
Limit: limit,
|
|
GrowthRate: slope * 86400, // per day
|
|
ETA: eta,
|
|
DaysLeft: hoursUntilFull / 24,
|
|
Confidence: r2,
|
|
}, nil
|
|
}
|
|
```
|
|
|
|
### 4.2 Integration with Patrol
|
|
|
|
```go
|
|
func (p *PatrolService) generateForecasts(state models.StateSnapshot) []Prediction {
|
|
var predictions []Prediction
|
|
|
|
// Forecast storage capacity
|
|
for _, storage := range state.Storage {
|
|
if storage.Total == 0 {
|
|
continue
|
|
}
|
|
forecast, err := p.forecaster.Forecast(storage.ID, "used", float64(storage.Total))
|
|
if err != nil || forecast == nil {
|
|
continue
|
|
}
|
|
|
|
if forecast.DaysLeft < 30 && forecast.Confidence > 0.5 {
|
|
predictions = append(predictions, Prediction{
|
|
ResourceID: storage.ID,
|
|
Metric: "storage_capacity",
|
|
Event: "capacity_full",
|
|
ETA: forecast.ETA,
|
|
Confidence: forecast.Confidence,
|
|
Basis: fmt.Sprintf("Growing %.1f GB/day", forecast.GrowthRate/1e9),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Forecast VM memory (could predict OOM)
|
|
// Forecast backup storage growth
|
|
// etc.
|
|
|
|
return predictions
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## File System Layout (Final)
|
|
|
|
```
|
|
internal/ai/
|
|
├── context/
|
|
│ ├── builder.go # Main orchestrator
|
|
│ ├── current.go # Current state extraction
|
|
│ ├── historical.go # Historical data integration
|
|
│ ├── trends.go # Trend computation
|
|
│ ├── formatter.go # AI-friendly formatting
|
|
│ └── types.go # Shared types
|
|
├── baseline/
|
|
│ ├── store.go # Baseline storage and learning
|
|
│ ├── persistence.go # Disk persistence
|
|
│ └── learning.go # Statistical learning
|
|
├── anomaly/
|
|
│ ├── detector.go # Anomaly detection
|
|
│ └── types.go
|
|
├── forecast/
|
|
│ ├── capacity.go # Capacity forecasting
|
|
│ └── patterns.go # Pattern-based prediction
|
|
├── memory/
|
|
│ ├── changes.go # Change detection
|
|
│ ├── remediation.go # Remediation logging
|
|
│ └── persistence.go
|
|
├── knowledge/ # (existing)
|
|
│ ├── store.go
|
|
│ └── store_test.go
|
|
├── providers/ # (existing)
|
|
├── findings.go # (existing)
|
|
├── patrol.go # (existing, will use new context/)
|
|
├── service.go # (existing, will use new context/)
|
|
└── routing.go # (existing)
|
|
```
|
|
|
|
---
|
|
|
|
## Migration Strategy
|
|
|
|
### Step 1: Add without changing
|
|
|
|
Create new packages (`context/`, `baseline/`, etc.) that work alongside existing code. Don't break anything.
|
|
|
|
### Step 2: Wire up to MetricsHistory
|
|
|
|
Pass `*monitoring.MetricsHistory` to the AI service at startup. Required for historical context.
|
|
|
|
### Step 3: Switch patrol to enriched context
|
|
|
|
Replace `buildInfrastructureSummary` with `buildEnrichedContext` behind a feature flag.
|
|
|
|
### Step 4: Add baseline learning
|
|
|
|
Start computing baselines in background. Initially just store, don't act.
|
|
|
|
### Step 5: Enable anomaly annotations
|
|
|
|
Add anomaly context to AI prompts. Let AI mention anomalies in findings.
|
|
|
|
### Step 6: Add forecasts
|
|
|
|
Enable capacity forecasting. Create new finding types for predicted issues.
|
|
|
|
### Step 7: Phase out old code
|
|
|
|
Remove deprecated methods once new system is stable.
|
|
|
|
---
|
|
|
|
## Testing Strategy
|
|
|
|
1. **Unit tests** for trend computation, baseline learning, anomaly detection
|
|
2. **Integration tests** with mock metrics history
|
|
3. **Golden file tests** for AI context formatting (ensure consistent output)
|
|
4. **Baseline learning tests** with synthetic time-series data
|
|
5. **Forecast accuracy tests** with historical data validation
|
|
|
|
---
|
|
|
|
## Success Criteria
|
|
|
|
Phase 1 complete when:
|
|
- AI prompts include historical trends for all resources
|
|
- "24h trend" visible in patrol output
|
|
|
|
Phase 2 complete when:
|
|
- Baselines computed automatically
|
|
- Anomalies flagged in AI context
|
|
- "X is unusual" appearing in findings
|
|
|
|
Phase 3 complete when:
|
|
- Changes detected and logged
|
|
- Remediation history queryable
|
|
- "Last time this happened..." in AI responses
|
|
|
|
Phase 4 complete when:
|
|
- Capacity forecasts generated
|
|
- "Full in X days" predictions accurate
|
|
- Predictive findings created before issues occur
|