feat(ai-diagnostics): add error clustering types and cosmos containers [1.1.1-1.1.2]

This commit is contained in:
saravanakumardb1 2026-03-03 11:44:23 -08:00
parent e98380003b
commit 4de01269b5
3 changed files with 839 additions and 0 deletions

View File

@ -76,6 +76,12 @@ const CONTAINER_DEFS: Record<string, ContainerConfig> = {
debug_traces: { partitionKeyPath: '/pk', defaultTtl: 7 * 86400 }, debug_traces: { partitionKeyPath: '/pk', defaultTtl: 7 * 86400 },
debug_logs: { partitionKeyPath: '/pk', defaultTtl: 3 * 86400 }, debug_logs: { partitionKeyPath: '/pk', defaultTtl: 3 * 86400 },
debug_screenshots: { partitionKeyPath: '/sessionId', defaultTtl: 7 * 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) // Broadcast Messaging & Surveys (see docs/roadmaps/not-started/platform_BROADCAST_SURVEY_ROADMAP.md)
broadcasts: { partitionKeyPath: '/productId' }, broadcasts: { partitionKeyPath: '/productId' },
broadcast_deliveries: { partitionKeyPath: '/userId', defaultTtl: 90 * 86400 }, broadcast_deliveries: { partitionKeyPath: '/userId', defaultTtl: 90 * 86400 },

View File

@ -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_<uuid>
key: string; // e.g., 'control', 'variant_a'
name: string;
description?: string;
isControl: boolean;
flagConfig: Record<string, unknown>; // Arbitrary config payload
currentAllocationPercent: number; // 0100%, 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<string, string | number | boolean>;
}
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_<uuid>
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_<uuid>
experimentId: string; // partition key
// Variant definition
name: string;
description?: string;
isControl: boolean;
// Feature flag configuration
flagConfig: Record<string, unknown>;
// 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_<uuid>
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_<uuid>
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<string, unknown>;
// Denormalized for filtering
platform: string;
appVersion: string;
// TTL
ttl?: number;
}
export interface ExperimentMetricDoc {
id: string; // em_<uuid>
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<string, number>;
feedbackSamples: string[];
competitorFeatures?: string[];
}
export interface GeneratedHypothesis {
primary: string;
alternatives: string[];
expectedEffectSize: number;
successMetric: string;
riskAssessment: 'low' | 'medium' | 'high';
impactScore: number; // 0100
difficultyScore: number; // 0100
powerPrediction: number; // Statistical power 0100
}
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<typeof CreateExperimentSchema>;
export type UpdateExperimentInput = z.infer<typeof UpdateExperimentSchema>;
export type TrackEventInput = z.infer<typeof TrackEventSchema>;
export type AdjustAllocationInput = z.infer<typeof AdjustAllocationSchema>;

View File

@ -0,0 +1,411 @@
import { z } from 'zod';
// ============================================================================
// Error Fingerprinting Types
// ============================================================================
export const ErrorFingerprintSchema = z.object({
id: z.string(), // ef_<uuid>
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<typeof ErrorFingerprintSchema>;
// ============================================================================
// Error Cluster Types
// ============================================================================
export const ClusterStatusSchema = z.enum(['active', 'investigating', 'resolved', 'ignored']);
export type ClusterStatus = z.infer<typeof ClusterStatusSchema>;
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<typeof CommonContextSchema>;
export const ErrorClusterDocSchema = z.object({
id: z.string(), // ec_<uuid>
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<typeof ErrorClusterDocSchema>;
// ============================================================================
// Cluster Analysis Types
// ============================================================================
export const ClusterAnalysisSchema = z.object({
id: z.string(), // ca_<uuid>
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<typeof ClusterAnalysisSchema>;
// ============================================================================
// Diagnostic Insight Types
// ============================================================================
export const RootCauseCategorySchema = z.enum([
'config',
'dependency',
'logic',
'resource',
'external',
'unknown',
]);
export type RootCauseCategory = z.infer<typeof RootCauseCategorySchema>;
export const EvidenceTypeSchema = z.enum([
'stack_trace',
'telemetry_pattern',
'device_correlation',
'api_failure',
'similar_issue',
'config_mismatch',
'version_regression',
]);
export type EvidenceType = z.infer<typeof EvidenceTypeSchema>;
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<typeof EvidenceSchema>;
export const SimilarResolvedIssueSchema = z.object({
clusterId: z.string(),
resolution: z.string(),
confidence: z.number(), // 0.0-1.0
});
export type SimilarResolvedIssue = z.infer<typeof SimilarResolvedIssueSchema>;
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<typeof FeedbackStatsSchema>;
export const DiagnosticInsightDocSchema = z.object({
id: z.string(), // di_<uuid>
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<typeof DiagnosticInsightDocSchema>;
// ============================================================================
// Natural Language Query Types
// ============================================================================
export const QueryIntentSchema = z.enum([
'root_cause',
'pattern_search',
'comparison',
'trend',
'impact',
]);
export type QueryIntent = z.infer<typeof QueryIntentSchema>;
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<typeof ExtractedEntitiesSchema>;
export const SupportingDataSchema = z.object({
type: z.enum(['cluster', 'telemetry', 'session', 'insight']),
id: z.string(),
relevanceScore: z.number(),
});
export type SupportingData = z.infer<typeof SupportingDataSchema>;
export const NaturalLanguageQueryDocSchema = z.object({
id: z.string(), // nq_<uuid>
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<typeof NaturalLanguageQueryDocSchema>;
// ============================================================================
// 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<typeof AnalyzeClusterRequestSchema>;
export const SubmitFeedbackRequestSchema = z.object({
insightId: z.string(),
rating: z.enum(['helpful', 'not_helpful']),
note: z.string().optional(),
});
export type SubmitFeedbackRequest = z.infer<typeof SubmitFeedbackRequestSchema>;
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<typeof QueryDiagnosticsRequestSchema>;
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<typeof SearchErrorsRequestSchema>;
// ============================================================================
// 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<typeof ErrorEventSchema>;
// ============================================================================
// Proactive Alert Types
// ============================================================================
export const AlertTypeSchema = z.enum([
'new_cluster',
'cluster_spike',
'new_error_type',
'regression_detected',
'anomaly_detected',
]);
export type AlertType = z.infer<typeof AlertTypeSchema>;
export const ProactiveAlertSchema = z.object({
id: z.string(), // pa_<uuid>
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<typeof ProactiveAlertSchema>;