From 4de01269b595af1f8c5a20845f9081522e21b784 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Tue, 3 Mar 2026 11:44:23 -0800 Subject: [PATCH] feat(ai-diagnostics): add error clustering types and cosmos containers [1.1.1-1.1.2] --- .../platform-service/src/lib/cosmos-init.ts | 6 + .../src/modules/ab-testing/types.ts | 422 ++++++++++++++++++ .../src/modules/ai-diagnostics/types.ts | 411 +++++++++++++++++ 3 files changed, 839 insertions(+) create mode 100644 services/platform-service/src/modules/ab-testing/types.ts create mode 100644 services/platform-service/src/modules/ai-diagnostics/types.ts diff --git a/services/platform-service/src/lib/cosmos-init.ts b/services/platform-service/src/lib/cosmos-init.ts index bd63318f..5024469f 100644 --- a/services/platform-service/src/lib/cosmos-init.ts +++ b/services/platform-service/src/lib/cosmos-init.ts @@ -76,6 +76,12 @@ const CONTAINER_DEFS: Record = { debug_traces: { partitionKeyPath: '/pk', defaultTtl: 7 * 86400 }, debug_logs: { partitionKeyPath: '/pk', defaultTtl: 3 * 86400 }, debug_screenshots: { partitionKeyPath: '/sessionId', defaultTtl: 7 * 86400 }, + // AI Diagnostics (see docs/roadmaps/AI_DIAGNOSTIC_ASSISTANT_ROADMAP.md) + error_clusters: { partitionKeyPath: '/productId', defaultTtl: 90 * 86400 }, + error_fingerprints: { partitionKeyPath: '/fingerprintHash' }, + diagnostic_insights: { partitionKeyPath: '/clusterId', defaultTtl: 90 * 86400 }, + diagnostic_queries: { partitionKeyPath: '/userId', defaultTtl: 30 * 86400 }, + proactive_alerts: { partitionKeyPath: '/productId', defaultTtl: 30 * 86400 }, // Broadcast Messaging & Surveys (see docs/roadmaps/not-started/platform_BROADCAST_SURVEY_ROADMAP.md) broadcasts: { partitionKeyPath: '/productId' }, broadcast_deliveries: { partitionKeyPath: '/userId', defaultTtl: 90 * 86400 }, diff --git a/services/platform-service/src/modules/ab-testing/types.ts b/services/platform-service/src/modules/ab-testing/types.ts new file mode 100644 index 00000000..fc043fcd --- /dev/null +++ b/services/platform-service/src/modules/ab-testing/types.ts @@ -0,0 +1,422 @@ +/** + * Intelligent A/B Testing — Extended types and schemas. + * Bayesian statistics, Thompson sampling, early stopping, AI hypothesis generation. + */ + +import { z } from 'zod'; + +// ───────────────────────────────────────────────────────────────────────────── +// Enums and Constants +// ───────────────────────────────────────────────────────────────────────────── + +export type ExperimentStatus = 'draft' | 'running' | 'paused' | 'stopped' | 'completed'; + +export type AllocationStrategy = 'random' | 'thompson' | 'epsilon_greedy' | 'ucb'; + +export type MetricType = 'conversion' | 'count' | 'duration' | 'revenue' | 'custom'; + +export type MetricAggregation = 'sum' | 'mean' | 'count' | 'unique'; + +export type MetricDirection = 'increase' | 'decrease'; + +// ───────────────────────────────────────────────────────────────────────────── +// Core Types +// ───────────────────────────────────────────────────────────────────────────── + +export interface ExperimentVariant { + id: string; // var_ + key: string; // e.g., 'control', 'variant_a' + name: string; + description?: string; + isControl: boolean; + flagConfig: Record; // Arbitrary config payload + currentAllocationPercent: number; // 0–100%, dynamic for bandit +} + +export interface PrimaryMetric { + name: string; + type: MetricType; + eventName: string; // Telemetry event to track + aggregation: MetricAggregation; + direction: MetricDirection; // Is higher better? + minimumDetectableEffect: number; // % change we want to detect +} + +export interface SecondaryMetric { + name: string; + type: MetricType; + eventName: string; +} + +export interface TargetingConfig { + platforms?: string[]; // ios, android, web + appVersions?: { min: string; max?: string }; + regions?: string[]; + userSegments?: string[]; // pro, free, enterprise + userProperties?: Record; +} + +export interface GuardrailsConfig { + minSampleSizePerVariant: number; // Default: 100 + maxDurationDays: number; // Safety limit, default: 30 + autoStopEnabled: boolean; + winnerThreshold: number; // % probability to auto-stop, default: 95 + requireApprovalFor: 'none' | 'revenue' | 'all'; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Document Types (Cosmos DB) +// ───────────────────────────────────────────────────────────────────────────── + +export interface ExperimentDoc { + id: string; // exp_ + productId: string; // partition key + + // Experiment definition + name: string; + description: string; + hypothesis: string; + aiGeneratedHypothesis?: boolean; + + // Status lifecycle + status: ExperimentStatus; + + // Variants + controlVariantId: string; + variantIds: string[]; + + // Configuration + allocationStrategy: AllocationStrategy; + targetPercent: number; // % of eligible traffic + + // Audience targeting + targeting: TargetingConfig; + + // Metrics + primaryMetric: PrimaryMetric; + secondaryMetrics: SecondaryMetric[]; + + // Guardrails + guardrails: GuardrailsConfig; + + // Scheduling + startAt?: string; + endAt?: string; + + // Stats (denormalized for fast reads) + totalParticipants: number; + totalEvents: number; + + // Timestamps + createdAt: string; + updatedAt: string; + startedAt?: string; + completedAt?: string; + + // TTL for completed experiments (2 years) + ttl?: number; +} + +export interface VariantDoc { + id: string; // var_ + experimentId: string; // partition key + + // Variant definition + name: string; + description?: string; + isControl: boolean; + + // Feature flag configuration + flagConfig: Record; + + // Traffic allocation (dynamic for bandit) + currentAllocationPercent: number; + + // Statistics (real-time computed) + stats: { + participants: number; + events: number; + primaryMetricValue: number; + primaryMetricStdDev?: number; + conversions?: number; + conversionRate?: number; + + // Bayesian posterior parameters + betaAlpha?: number; + betaBeta?: number; + gammaShape?: number; + gammaScale?: number; + }; + + // Bayesian results + bayesianResults?: { + probabilityBeatsControl: number; + probabilityBeatsAll: number; + expectedLiftPercent: number; + expectedLoss: number; + credibleInterval: { + lower: number; + mean: number; + upper: number; + }; + }; + + createdAt: string; + updatedAt: string; +} + +export interface ExperimentAssignmentDoc { + id: string; // ea_ + userId: string; // partition key + + experimentId: string; + variantId: string; + + // Assignment metadata + assignedAt: string; + firstExposedAt?: string; + + // Context at assignment + assignmentContext: { + platform: string; + appVersion: string; + osVersion: string; + deviceModel?: string; + region?: string; + }; + + // Events attributed + eventCount: number; + lastEventAt?: string; + + // TTL: Remove after experiment completes + analysis period + ttl?: number; +} + +export interface ExperimentEventDoc { + id: string; // ee_ + experimentId: string; // partition key + timestamp: string; // Sort key for time-series + + // Attribution + userId: string; + variantId: string; + assignmentId: string; + + // Event details + metricName: string; + metricType: MetricType; + value: number; + + // Conversion tracking + converted: boolean; + + // Context + eventMetadata?: Record; + + // Denormalized for filtering + platform: string; + appVersion: string; + + // TTL + ttl?: number; +} + +export interface ExperimentMetricDoc { + id: string; // em_ + experimentId: string; // partition key + + metricName: string; + variantId: string; + + // Aggregated values + count: number; + sum: number; + mean: number; + stdDev: number; + min: number; + max: number; + + // Conversion-specific + conversions: number; + conversionRate: number; + + // Last updated + updatedAt: string; +} + +export interface ExperimentResult { + experimentId: string; + status: 'in_progress' | 'winner_found' | 'no_winner' | 'stopped'; + + // Overall stats + totalParticipants: number; + totalEvents: number; + daysRunning: number; + + // Winner info + winnerVariantId?: string; + winnerProbability?: number; + + // Variant results + variantResults: Array<{ + variantId: string; + variantName: string; + isControl: boolean; + participants: number; + primaryMetricValue: number; + probabilityBeatsControl: number; + expectedLiftPercent: number; + credibleInterval: { lower: number; mean: number; upper: number }; + }>; + + // Statistical summary + statisticalSummary: { + probabilityAnyBeatsControl: number; + expectedLossIfShipped: number; + recommendedAction: 'ship' | 'rollback' | 'continue' | 'stop'; + }; + + // Early stopping + earlyStopped: boolean; + stopReason?: string; + + generatedAt: string; +} + +// ───────────────────────────────────────────────────────────────────────────── +// AI Hypothesis Types +// ───────────────────────────────────────────────────────────────────────────── + +export interface HypothesisInput { + featureName: string; + adoptionRate: number; + baselineRate: number; + segmentData: Record; + feedbackSamples: string[]; + competitorFeatures?: string[]; +} + +export interface GeneratedHypothesis { + primary: string; + alternatives: string[]; + expectedEffectSize: number; + successMetric: string; + riskAssessment: 'low' | 'medium' | 'high'; + impactScore: number; // 0–100 + difficultyScore: number; // 0–100 + powerPrediction: number; // Statistical power 0–100 +} + +export interface ExperimentSuggestion { + id: string; + hypothesis: GeneratedHypothesis; + suggestedVariants: Array<{ name: string; description: string }>; + suggestedMetrics: PrimaryMetric[]; + suggestedDuration: number; + suggestedSampleSize: number; + priority: number; + aiGenerated: boolean; + createdAt: string; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Zod Schemas +// ───────────────────────────────────────────────────────────────────────────── + +const VariantInputSchema = z.object({ + key: z + .string() + .min(1) + .regex(/^[a-z0-9_]+$/), + name: z.string().min(1).max(200), + description: z.string().default(''), + isControl: z.boolean().default(false), + flagConfig: z.record(z.unknown()).default({}), +}); + +const PrimaryMetricSchema = z.object({ + name: z.string().min(1), + type: z.enum(['conversion', 'count', 'duration', 'revenue', 'custom']), + eventName: z.string().min(1), + aggregation: z.enum(['sum', 'mean', 'count', 'unique']), + direction: z.enum(['increase', 'decrease']), + minimumDetectableEffect: z.number().min(0.01).max(100).default(5), +}); + +const SecondaryMetricSchema = z.object({ + name: z.string().min(1), + type: z.enum(['conversion', 'count', 'duration', 'revenue', 'custom']), + eventName: z.string().min(1), +}); + +const TargetingSchema = z.object({ + platforms: z.array(z.enum(['ios', 'android', 'web'])).optional(), + appVersions: z + .object({ + min: z.string(), + max: z.string().optional(), + }) + .optional(), + regions: z.array(z.string()).optional(), + userSegments: z.array(z.string()).optional(), + userProperties: z.record(z.union([z.string(), z.number(), z.boolean()])).optional(), +}); + +const GuardrailsSchema = z.object({ + minSampleSizePerVariant: z.number().int().min(10).default(100), + maxDurationDays: z.number().int().min(1).max(90).default(30), + autoStopEnabled: z.boolean().default(true), + winnerThreshold: z.number().int().min(80).max(99).default(95), + requireApprovalFor: z.enum(['none', 'revenue', 'all']).default('none'), +}); + +export const CreateExperimentSchema = z.object({ + name: z.string().min(1).max(200), + description: z.string().default(''), + hypothesis: z.string().min(1), + variants: z.array(VariantInputSchema).min(2).max(10), + allocationStrategy: z.enum(['random', 'thompson', 'epsilon_greedy', 'ucb']).default('random'), + targetPercent: z.number().int().min(1).max(100).default(100), + targeting: TargetingSchema.default({}), + primaryMetric: PrimaryMetricSchema, + secondaryMetrics: z.array(SecondaryMetricSchema).default([]), + guardrails: GuardrailsSchema.default({}), + startAt: z.string().datetime().optional(), +}); + +export const UpdateExperimentSchema = z.object({ + name: z.string().min(1).max(200).optional(), + description: z.string().optional(), + hypothesis: z.string().optional(), + status: z.enum(['draft', 'running', 'paused', 'stopped', 'completed']).optional(), + targetPercent: z.number().int().min(1).max(100).optional(), + targeting: TargetingSchema.optional(), + guardrails: GuardrailsSchema.optional(), +}); + +export const TrackEventSchema = z.object({ + experimentId: z.string(), + userId: z.string(), + metricName: z.string(), + metricType: z.enum(['conversion', 'count', 'duration', 'revenue', 'custom']), + value: z.number(), + converted: z.boolean().default(true), + eventMetadata: z.record(z.unknown()).optional(), + platform: z.string().default('unknown'), + appVersion: z.string().default('unknown'), +}); + +export const AdjustAllocationSchema = z.object({ + variantId: z.string(), + newAllocationPercent: z.number().int().min(0).max(100), +}); + +// ───────────────────────────────────────────────────────────────────────────── +// Type Exports +// ───────────────────────────────────────────────────────────────────────────── + +export type CreateExperimentInput = z.infer; +export type UpdateExperimentInput = z.infer; +export type TrackEventInput = z.infer; +export type AdjustAllocationInput = z.infer; diff --git a/services/platform-service/src/modules/ai-diagnostics/types.ts b/services/platform-service/src/modules/ai-diagnostics/types.ts new file mode 100644 index 00000000..c82fc6e7 --- /dev/null +++ b/services/platform-service/src/modules/ai-diagnostics/types.ts @@ -0,0 +1,411 @@ +import { z } from 'zod'; + +// ============================================================================ +// Error Fingerprinting Types +// ============================================================================ + +export const ErrorFingerprintSchema = z.object({ + id: z.string(), // ef_ + productId: z.string(), + fingerprintHash: z.string(), // SHA-256 of normalized error + + // Normalized error signature + errorType: z.string(), // Exception class/name + messageTemplate: z.string(), // Normalized message with placeholders + stackSignature: z.string(), // Normalized stack frames + + // Source location (normalized) + sourceLocation: z + .object({ + file: z.string().optional(), + function: z.string().optional(), + line: z.number().optional(), + }) + .optional(), + + // Metadata + createdAt: z.string(), + updatedAt: z.string(), + occurrenceCount: z.number().default(1), + uniqueUsers: z.number().default(1), + + // TTL for cleanup + ttl: z.number().default(90 * 86400), // 90 days +}); + +export type ErrorFingerprint = z.infer; + +// ============================================================================ +// Error Cluster Types +// ============================================================================ + +export const ClusterStatusSchema = z.enum(['active', 'investigating', 'resolved', 'ignored']); +export type ClusterStatus = z.infer; + +export const CommonContextSchema = z.object({ + osVersions: z.array(z.object({ version: z.string(), count: z.number() })), + appVersions: z.array(z.object({ version: z.string(), count: z.number() })), + deviceModels: z.array(z.object({ model: z.string(), count: z.number() })), + screenContexts: z.array(z.object({ screen: z.string(), count: z.number() })), +}); + +export type CommonContext = z.infer; + +export const ErrorClusterDocSchema = z.object({ + id: z.string(), // ec_ + productId: z.string(), // partition key + fingerprintHash: z.string(), + + // Cluster metadata + firstSeenAt: z.string(), // ISO 8601 + lastSeenAt: z.string(), + occurrenceCount: z.number(), // Total occurrences + uniqueUsers: z.number(), // Affected user count + + // Error signature + errorType: z.string(), // Exception class/name + messageTemplate: z.string(), // Normalized message with placeholders + stackSignature: z.string(), // Normalized stack frames + + // Vector embedding for semantic search + embedding: z.array(z.number()).optional(), // 1536-dim from text-embedding-3-small + embeddingVersion: z.string().optional(), // Model version for re-embedding + + // Context patterns (auto-extracted) + commonContext: CommonContextSchema.optional(), + + // Related data + relatedClusterIds: z.array(z.string()).default([]), // Similar clusters (vector similarity) + mergedIntoClusterId: z.string().optional(), // If deduplicated + + // Resolution tracking + status: ClusterStatusSchema.default('active'), + resolvedAt: z.string().optional(), + resolutionCommit: z.string().optional(), // Link to fix + + // Analysis metadata + lastAnalyzedAt: z.string().optional(), + insightId: z.string().optional(), // Link to latest diagnostic insight + + // Timestamps + createdAt: z.string(), + updatedAt: z.string(), + ttl: z.number().default(90 * 86400), // 90 days +}); + +export type ErrorClusterDoc = z.infer; + +// ============================================================================ +// Cluster Analysis Types +// ============================================================================ + +export const ClusterAnalysisSchema = z.object({ + id: z.string(), // ca_ + clusterId: z.string(), + productId: z.string(), + + // Analysis metadata + analyzedAt: z.string(), + analysisType: z.enum(['root_cause', 'pattern', 'comparison', 'trend']), + + // AI-generated summary + summary: z.string(), // "This appears to be a memory leak in the image cache..." + patternDescription: z.string(), // Detailed pattern analysis + + // Key observations + keyObservations: z.array( + z.object({ + observation: z.string(), + evidence: z.string(), + confidence: z.enum(['high', 'medium', 'low']), + }) + ), + + // Related clusters + similarClusters: z.array( + z.object({ + clusterId: z.string(), + similarityScore: z.number(), + reason: z.string(), + }) + ), + + // Timestamps + createdAt: z.string(), + updatedAt: z.string(), + ttl: z.number().default(90 * 86400), +}); + +export type ClusterAnalysis = z.infer; + +// ============================================================================ +// Diagnostic Insight Types +// ============================================================================ + +export const RootCauseCategorySchema = z.enum([ + 'config', + 'dependency', + 'logic', + 'resource', + 'external', + 'unknown', +]); +export type RootCauseCategory = z.infer; + +export const EvidenceTypeSchema = z.enum([ + 'stack_trace', + 'telemetry_pattern', + 'device_correlation', + 'api_failure', + 'similar_issue', + 'config_mismatch', + 'version_regression', +]); +export type EvidenceType = z.infer; + +export const EvidenceSchema = z.object({ + type: EvidenceTypeSchema, + description: z.string(), + strength: z.enum(['strong', 'moderate', 'weak']), + data: z.record(z.unknown()).optional(), +}); + +export type Evidence = z.infer; + +export const SimilarResolvedIssueSchema = z.object({ + clusterId: z.string(), + resolution: z.string(), + confidence: z.number(), // 0.0-1.0 +}); + +export type SimilarResolvedIssue = z.infer; + +export const FeedbackStatsSchema = z.object({ + helpful: z.number().default(0), + notHelpful: z.number().default(0), + engineerNotes: z.array(z.string()).default([]), +}); + +export type FeedbackStats = z.infer; + +export const DiagnosticInsightDocSchema = z.object({ + id: z.string(), // di_ + clusterId: z.string(), // partition key (with productId) + productId: z.string(), + + // AI-generated analysis + analysisType: z.enum(['root_cause', 'pattern', 'comparison', 'trend']), + generatedAt: z.string(), + + // LLM output + rootCauseCategory: RootCauseCategorySchema, + hypothesis: z.string(), // Natural language explanation + reasoning: z.string(), // Why LLM thinks this + confidence: z.enum(['high', 'medium', 'low']), + confidenceScore: z.number(), // 0.0-1.0 + + // Evidence + evidence: z.array(EvidenceSchema), + + // Suggested actions + suggestedInvestigation: z.array(z.string()), + potentialFixDirection: z.string().optional(), + similarResolvedIssues: z.array(SimilarResolvedIssueSchema).optional(), + + // Feedback + feedbackStats: FeedbackStatsSchema.default({ helpful: 0, notHelpful: 0, engineerNotes: [] }), + + // LLM metadata + modelUsed: z.string(), // gpt-4o, gpt-4o-mini + promptTokens: z.number(), + completionTokens: z.number(), + + // Correlation IDs for context + correlationIds: z.array(z.string()).optional(), + telemetryEventIds: z.array(z.string()).optional(), + + createdAt: z.string(), + updatedAt: z.string(), + ttl: z.number().default(90 * 86400), +}); + +export type DiagnosticInsightDoc = z.infer; + +// ============================================================================ +// Natural Language Query Types +// ============================================================================ + +export const QueryIntentSchema = z.enum([ + 'root_cause', + 'pattern_search', + 'comparison', + 'trend', + 'impact', +]); +export type QueryIntent = z.infer; + +export const ExtractedEntitiesSchema = z.object({ + products: z.array(z.string()).optional(), + timeRange: z.object({ start: z.string(), end: z.string() }).optional(), + errorTypes: z.array(z.string()).optional(), + platforms: z.array(z.string()).optional(), + userSegments: z.array(z.string()).optional(), + clusterIds: z.array(z.string()).optional(), +}); + +export type ExtractedEntities = z.infer; + +export const SupportingDataSchema = z.object({ + type: z.enum(['cluster', 'telemetry', 'session', 'insight']), + id: z.string(), + relevanceScore: z.number(), +}); + +export type SupportingData = z.infer; + +export const NaturalLanguageQueryDocSchema = z.object({ + id: z.string(), // nq_ + userId: z.string(), // Admin who asked + productId: z.string().optional(), // Optional filter + + // Query + rawQuery: z.string(), // "Why did iOS keyboard crash yesterday?" + parsedIntent: QueryIntentSchema, + extractedEntities: ExtractedEntitiesSchema, + + // Execution + executedQuery: z.string().optional(), // Translated Cosmos query + dataSources: z.array(z.string()).default([]), // Clusters, telemetry, sessions accessed + executionTimeMs: z.number().optional(), + + // Response + aiResponse: z.string().optional(), // Generated answer + confidence: z.number().optional(), // Overall confidence + supportingData: z.array(SupportingDataSchema).optional(), + + // Feedback + userRating: z.enum(['helpful', 'not_helpful']).optional(), + userComment: z.string().optional(), + + createdAt: z.string(), + ttl: z.number().default(30 * 86400), // 30 days +}); + +export type NaturalLanguageQueryDoc = z.infer; + +// ============================================================================ +// Analysis Request/Response Types +// ============================================================================ + +export const AnalyzeClusterRequestSchema = z.object({ + clusterId: z.string(), + analysisType: z.enum(['root_cause', 'pattern', 'comparison', 'trend']).default('root_cause'), + modelPreference: z.enum(['gpt-4o-mini', 'gpt-4o']).default('gpt-4o-mini'), + includeTelemetry: z.boolean().default(true), + includeSimilarClusters: z.boolean().default(true), +}); + +export type AnalyzeClusterRequest = z.infer; + +export const SubmitFeedbackRequestSchema = z.object({ + insightId: z.string(), + rating: z.enum(['helpful', 'not_helpful']), + note: z.string().optional(), +}); + +export type SubmitFeedbackRequest = z.infer; + +export const QueryDiagnosticsRequestSchema = z.object({ + query: z.string(), + productId: z.string().optional(), + timeRange: z.object({ start: z.string(), end: z.string() }).optional(), +}); + +export type QueryDiagnosticsRequest = z.infer; + +export const SearchErrorsRequestSchema = z.object({ + query: z.string(), // Semantic search query + productId: z.string().optional(), + limit: z.number().default(10), + similarityThreshold: z.number().default(0.75), +}); + +export type SearchErrorsRequest = z.infer; + +// ============================================================================ +// Error Event Types (for ingestion from telemetry) +// ============================================================================ + +export const ErrorEventSchema = z.object({ + id: z.string(), + productId: z.string(), + correlationId: z.string().optional(), + + // Error details + errorType: z.string(), + message: z.string(), + stackTrace: z.string().optional(), + + // Context + platform: z.string().optional(), // ios, android, web, desktop + osVersion: z.string().optional(), + appVersion: z.string().optional(), + deviceModel: z.string().optional(), + screen: z.string().optional(), + userId: z.string().optional(), + + // Telemetry linking + precedingEvents: z.array(z.record(z.unknown())).optional(), + + // Timestamp + timestamp: z.string(), +}); + +export type ErrorEvent = z.infer; + +// ============================================================================ +// Proactive Alert Types +// ============================================================================ + +export const AlertTypeSchema = z.enum([ + 'new_cluster', + 'cluster_spike', + 'new_error_type', + 'regression_detected', + 'anomaly_detected', +]); +export type AlertType = z.infer; + +export const ProactiveAlertSchema = z.object({ + id: z.string(), // pa_ + productId: z.string(), + alertType: AlertTypeSchema, + + // Target + clusterId: z.string().optional(), + errorType: z.string().optional(), + + // Alert details + severity: z.enum(['critical', 'high', 'medium', 'low']), + title: z.string(), + description: z.string(), + + // Metrics + baselineCount: z.number().optional(), + currentCount: z.number(), + spikeRatio: z.number().optional(), + + // AI analysis + aiSummary: z.string().optional(), + suggestedAction: z.string().optional(), + + // Status + acknowledgedAt: z.string().optional(), + acknowledgedBy: z.string().optional(), + resolvedAt: z.string().optional(), + + createdAt: z.string(), + ttl: z.number().default(30 * 86400), +}); + +export type ProactiveAlert = z.infer;