Spensara Spensara Knowledge Base & Documentation Repository

Repository Docs & Knowledge Base

Consolidated Spensara documentation repository (722 Markdown & architecture documents in ../repository)
Built Word Documents (.docx): Spensara Overview (.docx) Capability Assessment (.docx) Brink POS Ingestion Analysis (.docx) The Borg Ingestion Architecture (.docx)

Implementation Specs (157)

Detailed engineering and feature implementation specifications.
implementation-specs/specs/AnomalyDetection/TASK4.3.md 10,664 bytes · Updated Oct 23, 2026 10:44 AM EDT
Raw File

TASK 4.3: Add AlertEvent Emission Logic on Anomaly Detection

Summary

Create the alert emitter service that converts anomaly detection results into AlertEvent entities and persists them to the database.

Dependencies

  • TASK 1.1 (MonitorSpec for context)
  • TASK 2.1 (AnomalyDetectionResult as input)
  • Existing AlertEvent entity from Kds.Ai.Data

Files to Create

1. backend/Kds.Ai.AnomalyDetector/Alerts/IAlertEmitter.cs

csharp
using Kds.Ai.AnomalyDetector.Agents;
using Kds.Ai.Data.Entities;

namespace Kds.Ai.AnomalyDetector.Alerts;

/// <summary>
/// Service for emitting alerts based on anomaly detection results
/// </summary>
public interface IAlertEmitter
{
    /// <summary>
    /// Emit an alert for a detected anomaly
    /// </summary>
    /// <param name="spec">The monitor spec that detected the anomaly</param>
    /// <param name="result">The detection result</param>
    /// <param name="ct">Cancellation token</param>
    /// <returns>The created alert event (Id == Guid.Empty if suppressed)</returns>
    Task<AlertEvent> EmitAsync(
        MonitorSpec spec,
        AnomalyDetectionResult result,
        CancellationToken ct = default);
    
    /// <summary>
    /// Check if an alert should be suppressed due to deduplication
    /// </summary>
    /// <param name="spec">The monitor spec</param>
    /// <param name="result">The detection result</param>
    /// <param name="ct">Cancellation token</param>
    /// <returns>True if alert should be suppressed</returns>
    Task<bool> ShouldSuppressAsync(
        MonitorSpec spec,
        AnomalyDetectionResult result,
        CancellationToken ct = default);
}

2. backend/Kds.Ai.AnomalyDetector/Alerts/AlertEmitter.cs

csharp
using System.Security.Cryptography;
using System.Text;
using Kds.Ai.AnomalyDetector.Agents;
using Kds.Ai.Data;
using Kds.Ai.Data.Entities;
using Kds.Ai.Data.Enums;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace Kds.Ai.AnomalyDetector.Alerts;

internal sealed class AlertEmitter(
    KdsAiDbContext dbContext,
    ILogger<AlertEmitter> logger) : IAlertEmitter
{
    // Deduplication window - suppress similar alerts within this period
    private static readonly TimeSpan DedupeWindow = TimeSpan.FromHours(4);
    
    public async Task<AlertEvent> EmitAsync(
        MonitorSpec spec,
        AnomalyDetectionResult result,
        CancellationToken ct = default)
    {
        if (!result.AnomalyDetected)
        {
            throw new InvalidOperationException("Cannot emit alert for non-anomaly result");
        }
        
        // Check deduplication
        if (await ShouldSuppressAsync(spec, result, ct))
        {
            logger.LogInformation(
                "Suppressing duplicate alert for monitor '{MonitorName}'",
                spec.Name);
            
            // Return a "suppressed" marker alert (not saved)
            return new AlertEvent
            {
                Id = Guid.Empty, // Indicates not saved
                Type = EventType.AiAnomaly,
                Severity = result.Severity ?? EventSeverity.Low,
                Timestamp = DateTimeOffset.UtcNow,
                Name = $"[SUPPRESSED] {BuildAlertName(spec, result)}",
                Summary = "Alert suppressed due to deduplication",
                Content = result.DetailedAnalysis ?? ""
            };
        }
        
        var alert = new AlertEvent
        {
            Id = Guid.NewGuid(),
            Type = EventType.AiAnomaly,
            Severity = result.Severity ?? EventSeverity.Low,
            Timestamp = DateTimeOffset.UtcNow,
            Name = BuildAlertName(spec, result),
            Summary = BuildAlertSummary(spec, result),
            Content = BuildAlertContent(spec, result)
        };
        
        dbContext.Events.Add(alert);
        await dbContext.SaveChangesAsync(ct);
        
        logger.LogInformation(
            "Emitted {Severity} alert {AlertId}: {Name}",
            alert.Severity, alert.Id, alert.Name);
        
        return alert;
    }
    
    public async Task<bool> ShouldSuppressAsync(
        MonitorSpec spec,
        AnomalyDetectionResult result,
        CancellationToken ct = default)
    {
        // Check for recent similar alerts from the same monitor
        var cutoff = DateTimeOffset.UtcNow - DedupeWindow;
        var alertPrefix = $"[{spec.Name}]";
        
        var recentSimilar = await dbContext.Events
            .Where(e => e.Type == EventType.AiAnomaly)
            .Where(e => e.Timestamp > cutoff)
            .Where(e => e.Name.StartsWith(alertPrefix))
            .AnyAsync(ct);
        
        return recentSimilar;
    }
    
    private static string BuildAlertName(MonitorSpec spec, AnomalyDetectionResult result)
    {
        // Format: [MonitorName] - Summary
        var summaryPart = result.Summary?.Length > 60
            ? result.Summary[..60] + "..."
            : result.Summary ?? "Anomaly Detected";
        
        return $"[{spec.Name}] {summaryPart}";
    }
    
    private static string BuildAlertSummary(MonitorSpec spec, AnomalyDetectionResult result)
    {
        if (!string.IsNullOrEmpty(result.Summary))
        {
            return result.Summary;
        }
        
        var findingCount = result.Findings.Count;
        return findingCount switch
        {
            0 => "Anomaly detected in monitoring analysis",
            1 => result.Findings[0].Description,
            _ => $"{findingCount} anomalies detected by {spec.Name}"
        };
    }
    
    private static string BuildAlertContent(MonitorSpec spec, AnomalyDetectionResult result)
    {
        var sb = new StringBuilder();
        
        // Header
        sb.AppendLine($"# {spec.Name} - Alert");
        sb.AppendLine();
        sb.AppendLine($"**Monitor:** {spec.Name}");
        sb.AppendLine($"**Severity:** {result.Severity}");
        sb.AppendLine($"**Generated:** {DateTimeOffset.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
        sb.AppendLine();
        
        // Summary
        if (!string.IsNullOrEmpty(result.Summary))
        {
            sb.AppendLine("## Summary");
            sb.AppendLine();
            sb.AppendLine(result.Summary);
            sb.AppendLine();
        }
        
        // Findings
        if (result.Findings.Count > 0)
        {
            sb.AppendLine("## Findings");
            sb.AppendLine();
            
            foreach (var finding in result.Findings)
            {
                sb.AppendLine($"### {finding.Description}");
                sb.AppendLine();
                
                sb.AppendLine($"- **Severity:** {finding.Severity}");
                
                if (finding.AffectedPeriod is not null)
                    sb.AppendLine($"- **Period:** {finding.AffectedPeriod}");
                
                if (finding.Evidence is not null)
                    sb.AppendLine($"- **Evidence:** {finding.Evidence}");
                
                sb.AppendLine();
            }
        }
        
        // Detailed Analysis
        if (!string.IsNullOrEmpty(result.DetailedAnalysis))
        {
            sb.AppendLine("## Detailed Analysis");
            sb.AppendLine();
            sb.AppendLine(result.DetailedAnalysis);
            sb.AppendLine();
        }
        
        // Warnings
        if (result.Warnings.Count > 0)
        {
            sb.AppendLine("## Warnings");
            sb.AppendLine();
            foreach (var warning in result.Warnings)
            {
                sb.AppendLine($"- {warning}");
            }
            sb.AppendLine();
        }
        
        return sb.ToString();
    }
    
    private static string GenerateFingerprint(MonitorSpec spec, AnomalyDetectionResult result)
    {
        // Create a fingerprint based on monitor name and severity
        var input = $"{spec.Name}|{result.Severity}|" +
                   string.Join(",", result.Findings.Select(f => f.Description).Distinct());
        
        var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
        return Convert.ToHexString(bytes)[..16];
    }
}

Files to Modify

3. backend/Kds.Ai.AnomalyDetector/DependencyInjectionExtensions.cs

Add emitter registration:

csharp
// Register alert emitter
builder.Services.AddScoped<IAlertEmitter, AlertEmitter>();

Alert Content Structure

The generated alert content follows this Markdown structure:

markdown
# Prep Time Monitor - Downtown - Alert

**Monitor:** Prep Time Monitor - Downtown
**Severity:** High
**Generated:** 2025-01-15 14:30:00 UTC

## Summary

Prep times 45% above baseline during lunch rush (11:00-14:00)

## Findings

### Significant increase in average prep time during peak hours

- **Severity:** High
- **Period:** 2025-01-15 12:00-13:00
- **Evidence:** Average 450s vs baseline 310s (45% increase, 2.8 standard deviations above mean)

## Detailed Analysis

[LLM-generated detailed analysis...]

## Warnings

- Sample size for 13:00 hour was below minimum threshold

Deduplication Logic

The deduplication prevents alert fatigue by:

  1. Checking for recent alerts with the same monitor name prefix within the dedupe window (4 hours)
  2. Suppressing if a similar alert exists
  3. The fingerprint method is available for future enhanced similarity matching

Changes from Old Design

  • Dedup uses spec.Name prefix instead of spec.MonitorType + spec.StoreGuid[..8]
  • BuildAlertName uses [MonitorName] Summary format instead of [MonitorType] Store: xxx - Summary
  • BuildAlertContent uses spec.Name instead of spec.StoreGuid and spec.MonitorType
  • Findings use Description, Severity, AffectedPeriod, Evidence instead of Type, DeviationPercent, ZScore, Confidence
  • Class is internal sealed with primary constructor

Acceptance Criteria

  • [ ] IAlertEmitter interface created
  • [ ] AlertEmitter implementation created (internal sealed with primary constructor)
  • [ ] Alert name format: [MonitorName] Summary
  • [ ] Alert content in Markdown format
  • [ ] Deduplication logic uses spec.Name prefix
  • [ ] Suppressed alerts return marker with Id = Guid.Empty
  • [ ] No references to MonitorType, StoreGuid, Type (on findings), DeviationPercent, ZScore, Confidence
  • [ ] Logging integrated
  • [ ] Emitter registered in DI
  • [ ] Solution builds without errors

Notes

  • The dedupe window (4 hours) can be made configurable
  • Consider adding alert channels (email, Slack) in future
  • May want to add alert acknowledgment/resolution tracking
  • Fingerprinting could be more sophisticated (e.g., LSH for similarity)