@equilateral_ai/mindmeld 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +300 -0
- package/hooks/README.md +494 -0
- package/hooks/pre-compact.js +392 -0
- package/hooks/session-start.js +264 -0
- package/package.json +90 -0
- package/scripts/harvest.js +561 -0
- package/scripts/init-project.js +437 -0
- package/scripts/inject.js +388 -0
- package/src/collaboration/CollaborationPrompt.js +460 -0
- package/src/core/AlertEngine.js +813 -0
- package/src/core/AlertNotifier.js +363 -0
- package/src/core/CorrelationAnalyzer.js +774 -0
- package/src/core/CurationEngine.js +688 -0
- package/src/core/LLMPatternDetector.js +508 -0
- package/src/core/LoadBearingDetector.js +242 -0
- package/src/core/NotificationService.js +1032 -0
- package/src/core/PatternValidator.js +355 -0
- package/src/core/README.md +160 -0
- package/src/core/RapportOrchestrator.js +446 -0
- package/src/core/RelevanceDetector.js +577 -0
- package/src/core/StandardsIngestion.js +575 -0
- package/src/core/TeamLoadBearingDetector.js +431 -0
- package/src/database/dbOperations.js +105 -0
- package/src/handlers/activity/activityGetMe.js +98 -0
- package/src/handlers/activity/activityGetTeam.js +130 -0
- package/src/handlers/alerts/alertsAcknowledge.js +91 -0
- package/src/handlers/alerts/alertsGet.js +250 -0
- package/src/handlers/collaborators/collaboratorAdd.js +201 -0
- package/src/handlers/collaborators/collaboratorInvite.js +218 -0
- package/src/handlers/collaborators/collaboratorList.js +88 -0
- package/src/handlers/collaborators/collaboratorRemove.js +127 -0
- package/src/handlers/collaborators/inviteAccept.js +122 -0
- package/src/handlers/context/contextGet.js +57 -0
- package/src/handlers/context/invariantsGet.js +74 -0
- package/src/handlers/context/loopsGet.js +82 -0
- package/src/handlers/context/notesCreate.js +74 -0
- package/src/handlers/context/purposeGet.js +78 -0
- package/src/handlers/correlations/correlationsDeveloperGet.js +226 -0
- package/src/handlers/correlations/correlationsGet.js +93 -0
- package/src/handlers/correlations/correlationsProjectGet.js +161 -0
- package/src/handlers/github/githubConnectionStatus.js +49 -0
- package/src/handlers/github/githubDiscoverPatterns.js +364 -0
- package/src/handlers/github/githubOAuthCallback.js +166 -0
- package/src/handlers/github/githubOAuthStart.js +59 -0
- package/src/handlers/github/githubPatternsReview.js +109 -0
- package/src/handlers/github/githubReposList.js +105 -0
- package/src/handlers/helpers/checkSuperAdmin.js +85 -0
- package/src/handlers/helpers/dbOperations.js +53 -0
- package/src/handlers/helpers/errorHandler.js +49 -0
- package/src/handlers/helpers/index.js +106 -0
- package/src/handlers/helpers/lambdaWrapper.js +60 -0
- package/src/handlers/helpers/responseUtil.js +55 -0
- package/src/handlers/helpers/subscriptionTiers.js +1168 -0
- package/src/handlers/notifications/getPreferences.js +84 -0
- package/src/handlers/notifications/sendNotification.js +170 -0
- package/src/handlers/notifications/updatePreferences.js +316 -0
- package/src/handlers/patterns/patternUsagePost.js +182 -0
- package/src/handlers/patterns/patternViolationPost.js +185 -0
- package/src/handlers/projects/projectCreate.js +107 -0
- package/src/handlers/projects/projectDelete.js +82 -0
- package/src/handlers/projects/projectGet.js +95 -0
- package/src/handlers/projects/projectUpdate.js +118 -0
- package/src/handlers/reports/aiLeverage.js +206 -0
- package/src/handlers/reports/engineeringInvestment.js +132 -0
- package/src/handlers/reports/riskForecast.js +186 -0
- package/src/handlers/reports/standardsRoi.js +162 -0
- package/src/handlers/scheduled/analyzeCorrelations.js +178 -0
- package/src/handlers/scheduled/analyzeGitHistory.js +510 -0
- package/src/handlers/scheduled/generateAlerts.js +135 -0
- package/src/handlers/scheduled/refreshActivity.js +21 -0
- package/src/handlers/scheduled/scanCompliance.js +334 -0
- package/src/handlers/sessions/sessionEndPost.js +180 -0
- package/src/handlers/sessions/sessionStandardsPost.js +135 -0
- package/src/handlers/stripe/addonManagePost.js +240 -0
- package/src/handlers/stripe/billingPortalPost.js +93 -0
- package/src/handlers/stripe/enterpriseCheckoutPost.js +272 -0
- package/src/handlers/stripe/seatsUpdatePost.js +185 -0
- package/src/handlers/stripe/subscriptionCancelDelete.js +169 -0
- package/src/handlers/stripe/subscriptionCreatePost.js +221 -0
- package/src/handlers/stripe/subscriptionUpdatePut.js +163 -0
- package/src/handlers/stripe/webhookPost.js +454 -0
- package/src/handlers/users/cognitoPostConfirmation.js +150 -0
- package/src/handlers/users/userEntitlementsGet.js +89 -0
- package/src/handlers/users/userGet.js +114 -0
- package/src/handlers/webhooks/githubWebhook.js +223 -0
- package/src/index.js +969 -0
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rapport v3 - Alert Engine
|
|
3
|
+
*
|
|
4
|
+
* Purpose: Identifies struggling developers and generates attention alerts
|
|
5
|
+
*
|
|
6
|
+
* Alert Types:
|
|
7
|
+
* - stale_commits: No commits in X days (had previous activity)
|
|
8
|
+
* - low_conversion: Low session-to-commit conversion rate
|
|
9
|
+
* - no_ai_usage: Active committer not using AI assistance
|
|
10
|
+
* - high_violation_rate: Developer frequently violating standards
|
|
11
|
+
* - stalled_patterns: Developer's patterns not maturing/being used
|
|
12
|
+
* - declining_activity: Activity trending downward
|
|
13
|
+
*
|
|
14
|
+
* Severity Levels:
|
|
15
|
+
* - info: Informational, no action required
|
|
16
|
+
* - warning: Attention recommended
|
|
17
|
+
* - critical: Immediate attention required
|
|
18
|
+
*
|
|
19
|
+
* Based on: /Users/jamesford/Source/rapport/docs/PHASE_7_ENGINEERING_INTELLIGENCE.md
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Database operations are injected at runtime.
|
|
24
|
+
* When used from Lambda handlers, executeQuery comes from helpers.
|
|
25
|
+
* When used from CLI/scripts, it can be provided via constructor.
|
|
26
|
+
*/
|
|
27
|
+
let executeQuery = null;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Set the database query function
|
|
31
|
+
* @param {Function} queryFn - Function that executes database queries
|
|
32
|
+
*/
|
|
33
|
+
function setExecuteQuery(queryFn) {
|
|
34
|
+
executeQuery = queryFn;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get the database query function, throwing if not set
|
|
39
|
+
*/
|
|
40
|
+
function getExecuteQuery() {
|
|
41
|
+
if (!executeQuery) {
|
|
42
|
+
throw new Error('AlertEngine: executeQuery not initialized. Call setExecuteQuery() first.');
|
|
43
|
+
}
|
|
44
|
+
return executeQuery;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Lazy-load AlertNotifier to avoid circular dependencies
|
|
48
|
+
let AlertNotifierModule = null;
|
|
49
|
+
function getAlertNotifierModule() {
|
|
50
|
+
if (!AlertNotifierModule) {
|
|
51
|
+
AlertNotifierModule = require('./AlertNotifier');
|
|
52
|
+
}
|
|
53
|
+
return AlertNotifierModule;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Default threshold configuration
|
|
58
|
+
*/
|
|
59
|
+
const DEFAULT_THRESHOLDS = {
|
|
60
|
+
// Stale commits thresholds (days)
|
|
61
|
+
staleCommits: {
|
|
62
|
+
enabled: true,
|
|
63
|
+
infoDays: 7,
|
|
64
|
+
warningDays: 10,
|
|
65
|
+
criticalDays: 14,
|
|
66
|
+
requirePreviousActivity: true
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// Session-to-commit conversion thresholds (percentage)
|
|
70
|
+
lowConversion: {
|
|
71
|
+
enabled: true,
|
|
72
|
+
minSessions: 5, // Minimum sessions to evaluate
|
|
73
|
+
infoThreshold: 30, // Below 30% = info
|
|
74
|
+
warningThreshold: 15, // Below 15% = warning
|
|
75
|
+
criticalThreshold: 5 // Below 5% = critical
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
// No AI usage thresholds
|
|
79
|
+
noAiUsage: {
|
|
80
|
+
enabled: true,
|
|
81
|
+
minCommits: 5, // Minimum commits to trigger (active developer)
|
|
82
|
+
lookbackDays: 30
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// High violation rate thresholds (percentage of sessions with violations)
|
|
86
|
+
highViolationRate: {
|
|
87
|
+
enabled: true,
|
|
88
|
+
minSessions: 5, // Minimum sessions to evaluate
|
|
89
|
+
infoThreshold: 30, // Above 30% violation rate = info
|
|
90
|
+
warningThreshold: 50, // Above 50% = warning
|
|
91
|
+
criticalThreshold: 70 // Above 70% = critical
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
// Stalled patterns thresholds
|
|
95
|
+
stalledPatterns: {
|
|
96
|
+
enabled: true,
|
|
97
|
+
minPatterns: 3, // Minimum patterns discovered
|
|
98
|
+
staleDays: 30, // No usage/promotion in X days
|
|
99
|
+
minProvisionalDays: 14 // Provisional for too long
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// Declining activity thresholds
|
|
103
|
+
decliningActivity: {
|
|
104
|
+
enabled: true,
|
|
105
|
+
lookbackWeeks: 4, // Compare 4 weeks
|
|
106
|
+
declineThreshold: 50 // 50% decline triggers alert
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
// Alert aggregation settings
|
|
110
|
+
aggregation: {
|
|
111
|
+
cooldownHours: 24, // Don't create duplicate alerts within cooldown
|
|
112
|
+
maxAlertsPerUser: 5, // Maximum active alerts per user
|
|
113
|
+
expirationDays: 7 // Auto-expire alerts after X days
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
class AlertEngine {
|
|
118
|
+
constructor(config = {}) {
|
|
119
|
+
this.thresholds = this.mergeThresholds(DEFAULT_THRESHOLDS, config.thresholds || {});
|
|
120
|
+
this.dryRun = config.dryRun || false;
|
|
121
|
+
this.enableNotifications = config.enableNotifications !== false; // Default to enabled
|
|
122
|
+
this.alertNotifier = null; // Lazy-initialized
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get or create AlertNotifier instance
|
|
127
|
+
*/
|
|
128
|
+
getNotifier() {
|
|
129
|
+
if (!this.alertNotifier && this.enableNotifications) {
|
|
130
|
+
const module = getAlertNotifierModule();
|
|
131
|
+
// Ensure AlertNotifier has access to executeQuery
|
|
132
|
+
module.setExecuteQuery(executeQuery);
|
|
133
|
+
this.alertNotifier = new module.AlertNotifier();
|
|
134
|
+
}
|
|
135
|
+
return this.alertNotifier;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Deep merge threshold configurations
|
|
140
|
+
*/
|
|
141
|
+
mergeThresholds(defaults, overrides) {
|
|
142
|
+
const result = { ...defaults };
|
|
143
|
+
for (const key of Object.keys(overrides)) {
|
|
144
|
+
if (typeof overrides[key] === 'object' && !Array.isArray(overrides[key])) {
|
|
145
|
+
result[key] = { ...defaults[key], ...overrides[key] };
|
|
146
|
+
} else {
|
|
147
|
+
result[key] = overrides[key];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Generate all alerts for a company or all companies
|
|
155
|
+
*
|
|
156
|
+
* @param {string|null} companyId - Optional company ID to filter
|
|
157
|
+
* @returns {Promise<Object>} Alert generation summary
|
|
158
|
+
*/
|
|
159
|
+
async generateAlerts(companyId = null) {
|
|
160
|
+
const summary = {
|
|
161
|
+
startedAt: new Date().toISOString(),
|
|
162
|
+
alertsCreated: 0,
|
|
163
|
+
alertsSkipped: 0,
|
|
164
|
+
alertsExpired: 0,
|
|
165
|
+
byType: {},
|
|
166
|
+
errors: []
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
console.log('[AlertEngine] Starting alert generation...');
|
|
171
|
+
|
|
172
|
+
// Refresh activity views first
|
|
173
|
+
await this.refreshActivityViews();
|
|
174
|
+
|
|
175
|
+
// Generate each alert type
|
|
176
|
+
if (this.thresholds.staleCommits.enabled) {
|
|
177
|
+
const count = await this.generateStaleCommitAlerts(companyId);
|
|
178
|
+
summary.byType.stale_commits = count;
|
|
179
|
+
summary.alertsCreated += count;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (this.thresholds.lowConversion.enabled) {
|
|
183
|
+
const count = await this.generateLowConversionAlerts(companyId);
|
|
184
|
+
summary.byType.low_conversion = count;
|
|
185
|
+
summary.alertsCreated += count;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (this.thresholds.noAiUsage.enabled) {
|
|
189
|
+
const count = await this.generateNoAiUsageAlerts(companyId);
|
|
190
|
+
summary.byType.no_ai_usage = count;
|
|
191
|
+
summary.alertsCreated += count;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (this.thresholds.highViolationRate.enabled) {
|
|
195
|
+
const count = await this.generateHighViolationAlerts(companyId);
|
|
196
|
+
summary.byType.high_violation_rate = count;
|
|
197
|
+
summary.alertsCreated += count;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (this.thresholds.stalledPatterns.enabled) {
|
|
201
|
+
const count = await this.generateStalledPatternAlerts(companyId);
|
|
202
|
+
summary.byType.stalled_patterns = count;
|
|
203
|
+
summary.alertsCreated += count;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (this.thresholds.decliningActivity.enabled) {
|
|
207
|
+
const count = await this.generateDecliningActivityAlerts(companyId);
|
|
208
|
+
summary.byType.declining_activity = count;
|
|
209
|
+
summary.alertsCreated += count;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Auto-expire old alerts
|
|
213
|
+
summary.alertsExpired = await this.expireOldAlerts();
|
|
214
|
+
|
|
215
|
+
// Send notifications for new alerts
|
|
216
|
+
if (summary.alertsCreated > 0 && this.enableNotifications && !this.dryRun) {
|
|
217
|
+
try {
|
|
218
|
+
const notificationResults = await this.sendAlertNotifications(companyId);
|
|
219
|
+
summary.notifications = notificationResults;
|
|
220
|
+
} catch (error) {
|
|
221
|
+
console.error('[AlertEngine] Failed to send notifications:', error);
|
|
222
|
+
summary.notifications = { error: error.message };
|
|
223
|
+
// Don't fail the whole operation if notifications fail
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
summary.completedAt = new Date().toISOString();
|
|
228
|
+
console.log(`[AlertEngine] Generated ${summary.alertsCreated} alerts, expired ${summary.alertsExpired}`);
|
|
229
|
+
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.error('[AlertEngine] Error generating alerts:', error);
|
|
232
|
+
summary.errors.push(error.message);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return summary;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Send notifications for newly created alerts
|
|
240
|
+
*
|
|
241
|
+
* @param {string|null} companyId - Optional company filter
|
|
242
|
+
* @returns {Promise<Object>} Notification results
|
|
243
|
+
*/
|
|
244
|
+
async sendAlertNotifications(companyId = null) {
|
|
245
|
+
const notifier = this.getNotifier();
|
|
246
|
+
if (!notifier) {
|
|
247
|
+
return { skipped: true, reason: 'notifications_disabled' };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Get alerts created in the last minute (newly generated)
|
|
251
|
+
const query = `
|
|
252
|
+
SELECT
|
|
253
|
+
aa.alert_id,
|
|
254
|
+
aa.email_address,
|
|
255
|
+
aa.company_id,
|
|
256
|
+
u."User_Display_Name" as user_name,
|
|
257
|
+
aa.alert_type,
|
|
258
|
+
aa.severity,
|
|
259
|
+
aa.details,
|
|
260
|
+
aa.created_at
|
|
261
|
+
FROM rapport.attention_alerts aa
|
|
262
|
+
JOIN "Users" u ON aa.email_address = u."Email_Address"
|
|
263
|
+
WHERE aa.created_at > NOW() - INTERVAL '1 minute'
|
|
264
|
+
AND aa.status = 'active'
|
|
265
|
+
${companyId ? 'AND aa.company_id = $1' : ''}
|
|
266
|
+
ORDER BY aa.created_at DESC
|
|
267
|
+
`;
|
|
268
|
+
|
|
269
|
+
const params = companyId ? [companyId] : [];
|
|
270
|
+
const result = await executeQuery(query, params);
|
|
271
|
+
|
|
272
|
+
if (result.rows.length === 0) {
|
|
273
|
+
return { notified: 0, skipped: 0, failed: 0 };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return await notifier.notifyForAlerts(result.rows);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Refresh materialized views for accurate data
|
|
281
|
+
*/
|
|
282
|
+
async refreshActivityViews() {
|
|
283
|
+
try {
|
|
284
|
+
await executeQuery('SELECT rapport.refresh_developer_activity()');
|
|
285
|
+
console.log('[AlertEngine] Developer activity view refreshed');
|
|
286
|
+
} catch (error) {
|
|
287
|
+
// View might not exist yet, that's OK
|
|
288
|
+
console.log('[AlertEngine] Could not refresh activity view:', error.message);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Generate stale commits alerts
|
|
294
|
+
* Developers who haven't committed in X days
|
|
295
|
+
*/
|
|
296
|
+
async generateStaleCommitAlerts(companyId = null) {
|
|
297
|
+
const config = this.thresholds.staleCommits;
|
|
298
|
+
const cooldownHours = this.thresholds.aggregation.cooldownHours;
|
|
299
|
+
|
|
300
|
+
const query = `
|
|
301
|
+
INSERT INTO rapport.attention_alerts (
|
|
302
|
+
email_address, company_id, alert_type, severity, details, expires_at
|
|
303
|
+
)
|
|
304
|
+
SELECT
|
|
305
|
+
email_address,
|
|
306
|
+
company_id,
|
|
307
|
+
'stale_commits',
|
|
308
|
+
CASE
|
|
309
|
+
WHEN days_since_commit >= $1 THEN 'critical'
|
|
310
|
+
WHEN days_since_commit >= $2 THEN 'warning'
|
|
311
|
+
ELSE 'info'
|
|
312
|
+
END,
|
|
313
|
+
jsonb_build_object(
|
|
314
|
+
'days_since_commit', days_since_commit,
|
|
315
|
+
'last_commit', last_commit,
|
|
316
|
+
'commits_30d', commits_30d,
|
|
317
|
+
'generated_by', 'AlertEngine'
|
|
318
|
+
),
|
|
319
|
+
NOW() + INTERVAL '${this.thresholds.aggregation.expirationDays} days'
|
|
320
|
+
FROM rapport.mv_developer_activity
|
|
321
|
+
WHERE days_since_commit >= $3
|
|
322
|
+
${config.requirePreviousActivity ? 'AND commits_30d > 0' : ''}
|
|
323
|
+
${companyId ? 'AND company_id = $4' : ''}
|
|
324
|
+
AND NOT EXISTS (
|
|
325
|
+
SELECT 1 FROM rapport.attention_alerts aa
|
|
326
|
+
WHERE aa.email_address = mv_developer_activity.email_address
|
|
327
|
+
AND aa.alert_type = 'stale_commits'
|
|
328
|
+
AND (aa.status = 'active' OR aa.created_at > NOW() - INTERVAL '${cooldownHours} hours')
|
|
329
|
+
)
|
|
330
|
+
RETURNING alert_id
|
|
331
|
+
`;
|
|
332
|
+
|
|
333
|
+
const params = [
|
|
334
|
+
config.criticalDays,
|
|
335
|
+
config.warningDays,
|
|
336
|
+
config.infoDays
|
|
337
|
+
];
|
|
338
|
+
if (companyId) params.push(companyId);
|
|
339
|
+
|
|
340
|
+
if (this.dryRun) {
|
|
341
|
+
console.log('[AlertEngine] [DRY RUN] Would generate stale_commits alerts');
|
|
342
|
+
return 0;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const result = await executeQuery(query, params);
|
|
346
|
+
console.log(`[AlertEngine] Generated ${result.rowCount} stale_commits alerts`);
|
|
347
|
+
return result.rowCount;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Generate low conversion alerts
|
|
352
|
+
* Developers with low session-to-commit conversion rate
|
|
353
|
+
*/
|
|
354
|
+
async generateLowConversionAlerts(companyId = null) {
|
|
355
|
+
const config = this.thresholds.lowConversion;
|
|
356
|
+
const cooldownHours = this.thresholds.aggregation.cooldownHours;
|
|
357
|
+
|
|
358
|
+
const query = `
|
|
359
|
+
INSERT INTO rapport.attention_alerts (
|
|
360
|
+
email_address, company_id, alert_type, severity, details, expires_at
|
|
361
|
+
)
|
|
362
|
+
SELECT
|
|
363
|
+
email_address,
|
|
364
|
+
company_id,
|
|
365
|
+
'low_conversion',
|
|
366
|
+
CASE
|
|
367
|
+
WHEN session_to_commit_conversion_pct < $1 THEN 'critical'
|
|
368
|
+
WHEN session_to_commit_conversion_pct < $2 THEN 'warning'
|
|
369
|
+
ELSE 'info'
|
|
370
|
+
END,
|
|
371
|
+
jsonb_build_object(
|
|
372
|
+
'sessions_30d', sessions_30d,
|
|
373
|
+
'commits_30d', commits_30d,
|
|
374
|
+
'conversion_pct', session_to_commit_conversion_pct,
|
|
375
|
+
'generated_by', 'AlertEngine'
|
|
376
|
+
),
|
|
377
|
+
NOW() + INTERVAL '${this.thresholds.aggregation.expirationDays} days'
|
|
378
|
+
FROM rapport.mv_developer_activity
|
|
379
|
+
WHERE sessions_30d >= $3
|
|
380
|
+
AND session_to_commit_conversion_pct < $4
|
|
381
|
+
${companyId ? 'AND company_id = $5' : ''}
|
|
382
|
+
AND NOT EXISTS (
|
|
383
|
+
SELECT 1 FROM rapport.attention_alerts aa
|
|
384
|
+
WHERE aa.email_address = mv_developer_activity.email_address
|
|
385
|
+
AND aa.alert_type = 'low_conversion'
|
|
386
|
+
AND (aa.status = 'active' OR aa.created_at > NOW() - INTERVAL '${cooldownHours} hours')
|
|
387
|
+
)
|
|
388
|
+
RETURNING alert_id
|
|
389
|
+
`;
|
|
390
|
+
|
|
391
|
+
const params = [
|
|
392
|
+
config.criticalThreshold,
|
|
393
|
+
config.warningThreshold,
|
|
394
|
+
config.minSessions,
|
|
395
|
+
config.infoThreshold
|
|
396
|
+
];
|
|
397
|
+
if (companyId) params.push(companyId);
|
|
398
|
+
|
|
399
|
+
if (this.dryRun) {
|
|
400
|
+
console.log('[AlertEngine] [DRY RUN] Would generate low_conversion alerts');
|
|
401
|
+
return 0;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const result = await executeQuery(query, params);
|
|
405
|
+
console.log(`[AlertEngine] Generated ${result.rowCount} low_conversion alerts`);
|
|
406
|
+
return result.rowCount;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Generate no AI usage alerts
|
|
411
|
+
* Developers actively committing but not using AI sessions
|
|
412
|
+
*/
|
|
413
|
+
async generateNoAiUsageAlerts(companyId = null) {
|
|
414
|
+
const config = this.thresholds.noAiUsage;
|
|
415
|
+
const cooldownHours = this.thresholds.aggregation.cooldownHours;
|
|
416
|
+
|
|
417
|
+
const query = `
|
|
418
|
+
INSERT INTO rapport.attention_alerts (
|
|
419
|
+
email_address, company_id, alert_type, severity, details, expires_at
|
|
420
|
+
)
|
|
421
|
+
SELECT
|
|
422
|
+
email_address,
|
|
423
|
+
company_id,
|
|
424
|
+
'no_ai_usage',
|
|
425
|
+
'info',
|
|
426
|
+
jsonb_build_object(
|
|
427
|
+
'sessions_30d', sessions_30d,
|
|
428
|
+
'commits_30d', commits_30d,
|
|
429
|
+
'note', 'Developer has commits but no AI sessions',
|
|
430
|
+
'generated_by', 'AlertEngine'
|
|
431
|
+
),
|
|
432
|
+
NOW() + INTERVAL '${this.thresholds.aggregation.expirationDays} days'
|
|
433
|
+
FROM rapport.mv_developer_activity
|
|
434
|
+
WHERE sessions_30d = 0
|
|
435
|
+
AND commits_30d >= $1
|
|
436
|
+
${companyId ? 'AND company_id = $2' : ''}
|
|
437
|
+
AND NOT EXISTS (
|
|
438
|
+
SELECT 1 FROM rapport.attention_alerts aa
|
|
439
|
+
WHERE aa.email_address = mv_developer_activity.email_address
|
|
440
|
+
AND aa.alert_type = 'no_ai_usage'
|
|
441
|
+
AND (aa.status = 'active' OR aa.created_at > NOW() - INTERVAL '${cooldownHours} hours')
|
|
442
|
+
)
|
|
443
|
+
RETURNING alert_id
|
|
444
|
+
`;
|
|
445
|
+
|
|
446
|
+
const params = [config.minCommits];
|
|
447
|
+
if (companyId) params.push(companyId);
|
|
448
|
+
|
|
449
|
+
if (this.dryRun) {
|
|
450
|
+
console.log('[AlertEngine] [DRY RUN] Would generate no_ai_usage alerts');
|
|
451
|
+
return 0;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const result = await executeQuery(query, params);
|
|
455
|
+
console.log(`[AlertEngine] Generated ${result.rowCount} no_ai_usage alerts`);
|
|
456
|
+
return result.rowCount;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Generate high violation rate alerts
|
|
461
|
+
* Developers with high rate of standards violations
|
|
462
|
+
*/
|
|
463
|
+
async generateHighViolationAlerts(companyId = null) {
|
|
464
|
+
const config = this.thresholds.highViolationRate;
|
|
465
|
+
const cooldownHours = this.thresholds.aggregation.cooldownHours;
|
|
466
|
+
|
|
467
|
+
// First, calculate violation rates per developer
|
|
468
|
+
const query = `
|
|
469
|
+
INSERT INTO rapport.attention_alerts (
|
|
470
|
+
email_address, company_id, alert_type, severity, details, expires_at
|
|
471
|
+
)
|
|
472
|
+
SELECT
|
|
473
|
+
s.email_address,
|
|
474
|
+
da.company_id,
|
|
475
|
+
'high_violation_rate',
|
|
476
|
+
CASE
|
|
477
|
+
WHEN (COUNT(*) FILTER (WHERE ss.violated = true)::decimal / NULLIF(COUNT(*), 0) * 100) >= $1 THEN 'critical'
|
|
478
|
+
WHEN (COUNT(*) FILTER (WHERE ss.violated = true)::decimal / NULLIF(COUNT(*), 0) * 100) >= $2 THEN 'warning'
|
|
479
|
+
ELSE 'info'
|
|
480
|
+
END,
|
|
481
|
+
jsonb_build_object(
|
|
482
|
+
'total_sessions', COUNT(DISTINCT s.session_id),
|
|
483
|
+
'standards_shown', COUNT(*),
|
|
484
|
+
'violations', COUNT(*) FILTER (WHERE ss.violated = true),
|
|
485
|
+
'violation_rate', ROUND((COUNT(*) FILTER (WHERE ss.violated = true)::decimal / NULLIF(COUNT(*), 0) * 100), 1),
|
|
486
|
+
'generated_by', 'AlertEngine'
|
|
487
|
+
),
|
|
488
|
+
NOW() + INTERVAL '${this.thresholds.aggregation.expirationDays} days'
|
|
489
|
+
FROM rapport.sessions s
|
|
490
|
+
JOIN rapport.session_standards ss ON s.session_id = ss.session_id
|
|
491
|
+
LEFT JOIN rapport.mv_developer_activity da ON s.email_address = da.email_address
|
|
492
|
+
WHERE s.started_at > NOW() - INTERVAL '30 days'
|
|
493
|
+
${companyId ? 'AND da.company_id = $4' : ''}
|
|
494
|
+
GROUP BY s.email_address, da.company_id
|
|
495
|
+
HAVING COUNT(DISTINCT s.session_id) >= $3
|
|
496
|
+
AND (COUNT(*) FILTER (WHERE ss.violated = true)::decimal / NULLIF(COUNT(*), 0) * 100) >= $5
|
|
497
|
+
AND NOT EXISTS (
|
|
498
|
+
SELECT 1 FROM rapport.attention_alerts aa
|
|
499
|
+
WHERE aa.email_address = s.email_address
|
|
500
|
+
AND aa.alert_type = 'high_violation_rate'
|
|
501
|
+
AND (aa.status = 'active' OR aa.created_at > NOW() - INTERVAL '${cooldownHours} hours')
|
|
502
|
+
)
|
|
503
|
+
RETURNING alert_id
|
|
504
|
+
`;
|
|
505
|
+
|
|
506
|
+
const params = [
|
|
507
|
+
config.criticalThreshold,
|
|
508
|
+
config.warningThreshold,
|
|
509
|
+
config.minSessions,
|
|
510
|
+
config.infoThreshold
|
|
511
|
+
];
|
|
512
|
+
if (companyId) params.splice(3, 0, companyId);
|
|
513
|
+
|
|
514
|
+
if (this.dryRun) {
|
|
515
|
+
console.log('[AlertEngine] [DRY RUN] Would generate high_violation_rate alerts');
|
|
516
|
+
return 0;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
const result = await executeQuery(query, params);
|
|
521
|
+
console.log(`[AlertEngine] Generated ${result.rowCount} high_violation_rate alerts`);
|
|
522
|
+
return result.rowCount;
|
|
523
|
+
} catch (error) {
|
|
524
|
+
// session_standards table might not have data yet
|
|
525
|
+
console.log('[AlertEngine] Could not generate violation alerts:', error.message);
|
|
526
|
+
return 0;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Generate stalled pattern alerts
|
|
532
|
+
* Developers with patterns that aren't maturing or being used
|
|
533
|
+
*/
|
|
534
|
+
async generateStalledPatternAlerts(companyId = null) {
|
|
535
|
+
const config = this.thresholds.stalledPatterns;
|
|
536
|
+
const cooldownHours = this.thresholds.aggregation.cooldownHours;
|
|
537
|
+
|
|
538
|
+
const query = `
|
|
539
|
+
INSERT INTO rapport.attention_alerts (
|
|
540
|
+
email_address, company_id, alert_type, severity, details, expires_at
|
|
541
|
+
)
|
|
542
|
+
SELECT
|
|
543
|
+
p.discovered_by as email_address,
|
|
544
|
+
proj.company_id,
|
|
545
|
+
'stalled_patterns',
|
|
546
|
+
'warning',
|
|
547
|
+
jsonb_build_object(
|
|
548
|
+
'total_patterns', COUNT(*),
|
|
549
|
+
'provisional_patterns', COUNT(*) FILTER (WHERE p.maturity = 'provisional'),
|
|
550
|
+
'stale_patterns', COUNT(*) FILTER (WHERE p.last_used < NOW() - INTERVAL '${config.staleDays} days' OR p.last_used IS NULL),
|
|
551
|
+
'oldest_provisional_days', EXTRACT(DAY FROM NOW() - MIN(p.discovered_at) FILTER (WHERE p.maturity = 'provisional')),
|
|
552
|
+
'generated_by', 'AlertEngine'
|
|
553
|
+
),
|
|
554
|
+
NOW() + INTERVAL '${this.thresholds.aggregation.expirationDays} days'
|
|
555
|
+
FROM rapport.patterns p
|
|
556
|
+
JOIN rapport.projects proj ON p.project_id = proj.project_id
|
|
557
|
+
WHERE p.discovered_by IS NOT NULL
|
|
558
|
+
${companyId ? 'AND proj.company_id = $2' : ''}
|
|
559
|
+
GROUP BY p.discovered_by, proj.company_id
|
|
560
|
+
HAVING COUNT(*) >= $1
|
|
561
|
+
AND (
|
|
562
|
+
-- Too many provisional patterns that haven't matured
|
|
563
|
+
COUNT(*) FILTER (
|
|
564
|
+
WHERE p.maturity = 'provisional'
|
|
565
|
+
AND p.discovered_at < NOW() - INTERVAL '${config.minProvisionalDays} days'
|
|
566
|
+
) > COUNT(*) / 2
|
|
567
|
+
OR
|
|
568
|
+
-- Patterns not being used
|
|
569
|
+
COUNT(*) FILTER (
|
|
570
|
+
WHERE p.last_used < NOW() - INTERVAL '${config.staleDays} days'
|
|
571
|
+
OR p.last_used IS NULL
|
|
572
|
+
) > COUNT(*) / 2
|
|
573
|
+
)
|
|
574
|
+
AND NOT EXISTS (
|
|
575
|
+
SELECT 1 FROM rapport.attention_alerts aa
|
|
576
|
+
WHERE aa.email_address = p.discovered_by
|
|
577
|
+
AND aa.alert_type = 'stalled_patterns'
|
|
578
|
+
AND (aa.status = 'active' OR aa.created_at > NOW() - INTERVAL '${cooldownHours} hours')
|
|
579
|
+
)
|
|
580
|
+
RETURNING alert_id
|
|
581
|
+
`;
|
|
582
|
+
|
|
583
|
+
const params = [config.minPatterns];
|
|
584
|
+
if (companyId) params.push(companyId);
|
|
585
|
+
|
|
586
|
+
if (this.dryRun) {
|
|
587
|
+
console.log('[AlertEngine] [DRY RUN] Would generate stalled_patterns alerts');
|
|
588
|
+
return 0;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
try {
|
|
592
|
+
const result = await executeQuery(query, params);
|
|
593
|
+
console.log(`[AlertEngine] Generated ${result.rowCount} stalled_patterns alerts`);
|
|
594
|
+
return result.rowCount;
|
|
595
|
+
} catch (error) {
|
|
596
|
+
console.log('[AlertEngine] Could not generate stalled pattern alerts:', error.message);
|
|
597
|
+
return 0;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Generate declining activity alerts
|
|
603
|
+
* Developers whose activity is trending downward
|
|
604
|
+
*/
|
|
605
|
+
async generateDecliningActivityAlerts(companyId = null) {
|
|
606
|
+
const config = this.thresholds.decliningActivity;
|
|
607
|
+
const cooldownHours = this.thresholds.aggregation.cooldownHours;
|
|
608
|
+
|
|
609
|
+
// Compare current week to average of previous weeks
|
|
610
|
+
const query = `
|
|
611
|
+
WITH weekly_sessions AS (
|
|
612
|
+
SELECT
|
|
613
|
+
email_address,
|
|
614
|
+
DATE_TRUNC('week', started_at) as week,
|
|
615
|
+
COUNT(*) as session_count
|
|
616
|
+
FROM rapport.sessions
|
|
617
|
+
WHERE started_at > NOW() - INTERVAL '${config.lookbackWeeks} weeks'
|
|
618
|
+
GROUP BY email_address, DATE_TRUNC('week', started_at)
|
|
619
|
+
),
|
|
620
|
+
developer_trends AS (
|
|
621
|
+
SELECT
|
|
622
|
+
email_address,
|
|
623
|
+
AVG(session_count) FILTER (
|
|
624
|
+
WHERE week < DATE_TRUNC('week', NOW())
|
|
625
|
+
) as avg_previous_weeks,
|
|
626
|
+
SUM(session_count) FILTER (
|
|
627
|
+
WHERE week = DATE_TRUNC('week', NOW())
|
|
628
|
+
) as current_week
|
|
629
|
+
FROM weekly_sessions
|
|
630
|
+
GROUP BY email_address
|
|
631
|
+
HAVING AVG(session_count) FILTER (WHERE week < DATE_TRUNC('week', NOW())) > 0
|
|
632
|
+
)
|
|
633
|
+
INSERT INTO rapport.attention_alerts (
|
|
634
|
+
email_address, company_id, alert_type, severity, details, expires_at
|
|
635
|
+
)
|
|
636
|
+
SELECT
|
|
637
|
+
dt.email_address,
|
|
638
|
+
da.company_id,
|
|
639
|
+
'declining_activity',
|
|
640
|
+
CASE
|
|
641
|
+
WHEN COALESCE(dt.current_week, 0) = 0 THEN 'warning'
|
|
642
|
+
ELSE 'info'
|
|
643
|
+
END,
|
|
644
|
+
jsonb_build_object(
|
|
645
|
+
'avg_previous_weeks', ROUND(dt.avg_previous_weeks, 1),
|
|
646
|
+
'current_week', COALESCE(dt.current_week, 0),
|
|
647
|
+
'decline_pct', ROUND((1 - COALESCE(dt.current_week, 0)::decimal / dt.avg_previous_weeks) * 100, 1),
|
|
648
|
+
'generated_by', 'AlertEngine'
|
|
649
|
+
),
|
|
650
|
+
NOW() + INTERVAL '${this.thresholds.aggregation.expirationDays} days'
|
|
651
|
+
FROM developer_trends dt
|
|
652
|
+
LEFT JOIN rapport.mv_developer_activity da ON dt.email_address = da.email_address
|
|
653
|
+
WHERE (1 - COALESCE(dt.current_week, 0)::decimal / dt.avg_previous_weeks) * 100 >= $1
|
|
654
|
+
${companyId ? 'AND da.company_id = $2' : ''}
|
|
655
|
+
AND NOT EXISTS (
|
|
656
|
+
SELECT 1 FROM rapport.attention_alerts aa
|
|
657
|
+
WHERE aa.email_address = dt.email_address
|
|
658
|
+
AND aa.alert_type = 'declining_activity'
|
|
659
|
+
AND (aa.status = 'active' OR aa.created_at > NOW() - INTERVAL '${cooldownHours} hours')
|
|
660
|
+
)
|
|
661
|
+
RETURNING alert_id
|
|
662
|
+
`;
|
|
663
|
+
|
|
664
|
+
const params = [config.declineThreshold];
|
|
665
|
+
if (companyId) params.push(companyId);
|
|
666
|
+
|
|
667
|
+
if (this.dryRun) {
|
|
668
|
+
console.log('[AlertEngine] [DRY RUN] Would generate declining_activity alerts');
|
|
669
|
+
return 0;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
try {
|
|
673
|
+
const result = await executeQuery(query, params);
|
|
674
|
+
console.log(`[AlertEngine] Generated ${result.rowCount} declining_activity alerts`);
|
|
675
|
+
return result.rowCount;
|
|
676
|
+
} catch (error) {
|
|
677
|
+
console.log('[AlertEngine] Could not generate declining activity alerts:', error.message);
|
|
678
|
+
return 0;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Expire old alerts that have passed their expiration date
|
|
684
|
+
*/
|
|
685
|
+
async expireOldAlerts() {
|
|
686
|
+
if (this.dryRun) {
|
|
687
|
+
console.log('[AlertEngine] [DRY RUN] Would expire old alerts');
|
|
688
|
+
return 0;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const result = await executeQuery(`
|
|
692
|
+
UPDATE rapport.attention_alerts
|
|
693
|
+
SET status = 'resolved', resolved_at = NOW()
|
|
694
|
+
WHERE expires_at < NOW()
|
|
695
|
+
AND status = 'active'
|
|
696
|
+
RETURNING alert_id
|
|
697
|
+
`);
|
|
698
|
+
|
|
699
|
+
console.log(`[AlertEngine] Expired ${result.rowCount} old alerts`);
|
|
700
|
+
return result.rowCount;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Get alert statistics for a company
|
|
705
|
+
*
|
|
706
|
+
* @param {string} companyId - Company ID
|
|
707
|
+
* @returns {Promise<Object>} Alert statistics
|
|
708
|
+
*/
|
|
709
|
+
async getAlertStats(companyId) {
|
|
710
|
+
const result = await executeQuery(`
|
|
711
|
+
SELECT
|
|
712
|
+
alert_type,
|
|
713
|
+
severity,
|
|
714
|
+
status,
|
|
715
|
+
COUNT(*) as count
|
|
716
|
+
FROM rapport.attention_alerts
|
|
717
|
+
WHERE company_id = $1
|
|
718
|
+
AND created_at > NOW() - INTERVAL '30 days'
|
|
719
|
+
GROUP BY alert_type, severity, status
|
|
720
|
+
ORDER BY alert_type, severity, status
|
|
721
|
+
`, [companyId]);
|
|
722
|
+
|
|
723
|
+
const stats = {
|
|
724
|
+
byType: {},
|
|
725
|
+
bySeverity: { info: 0, warning: 0, critical: 0 },
|
|
726
|
+
byStatus: { active: 0, acknowledged: 0, resolved: 0 },
|
|
727
|
+
total: 0
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
for (const row of result.rows) {
|
|
731
|
+
// By type
|
|
732
|
+
if (!stats.byType[row.alert_type]) {
|
|
733
|
+
stats.byType[row.alert_type] = 0;
|
|
734
|
+
}
|
|
735
|
+
stats.byType[row.alert_type] += parseInt(row.count);
|
|
736
|
+
|
|
737
|
+
// By severity
|
|
738
|
+
if (stats.bySeverity[row.severity] !== undefined) {
|
|
739
|
+
stats.bySeverity[row.severity] += parseInt(row.count);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// By status
|
|
743
|
+
if (stats.byStatus[row.status] !== undefined) {
|
|
744
|
+
stats.byStatus[row.status] += parseInt(row.count);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
stats.total += parseInt(row.count);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return stats;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Get struggling developers for a company
|
|
755
|
+
*
|
|
756
|
+
* @param {string} companyId - Company ID
|
|
757
|
+
* @returns {Promise<Array>} List of developers with alert summaries
|
|
758
|
+
*/
|
|
759
|
+
async getStrugglingDevelopers(companyId) {
|
|
760
|
+
const result = await executeQuery(`
|
|
761
|
+
SELECT
|
|
762
|
+
aa.email_address,
|
|
763
|
+
u."User_Display_Name" as display_name,
|
|
764
|
+
array_agg(DISTINCT aa.alert_type) as alert_types,
|
|
765
|
+
MAX(aa.severity) as max_severity,
|
|
766
|
+
COUNT(*) as active_alert_count,
|
|
767
|
+
MIN(aa.created_at) as first_alert_at
|
|
768
|
+
FROM rapport.attention_alerts aa
|
|
769
|
+
JOIN "Users" u ON aa.email_address = u."Email_Address"
|
|
770
|
+
WHERE aa.company_id = $1
|
|
771
|
+
AND aa.status = 'active'
|
|
772
|
+
GROUP BY aa.email_address, u."User_Display_Name"
|
|
773
|
+
ORDER BY
|
|
774
|
+
CASE MAX(aa.severity)
|
|
775
|
+
WHEN 'critical' THEN 1
|
|
776
|
+
WHEN 'warning' THEN 2
|
|
777
|
+
ELSE 3
|
|
778
|
+
END,
|
|
779
|
+
COUNT(*) DESC
|
|
780
|
+
`, [companyId]);
|
|
781
|
+
|
|
782
|
+
return result.rows.map(row => ({
|
|
783
|
+
email: row.email_address,
|
|
784
|
+
displayName: row.display_name,
|
|
785
|
+
alertTypes: row.alert_types,
|
|
786
|
+
maxSeverity: row.max_severity,
|
|
787
|
+
activeAlertCount: parseInt(row.active_alert_count),
|
|
788
|
+
firstAlertAt: row.first_alert_at
|
|
789
|
+
}));
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Get configuration (for debugging/admin)
|
|
794
|
+
*/
|
|
795
|
+
getConfiguration() {
|
|
796
|
+
return {
|
|
797
|
+
thresholds: this.thresholds,
|
|
798
|
+
dryRun: this.dryRun
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Update thresholds at runtime
|
|
804
|
+
*
|
|
805
|
+
* @param {Object} newThresholds - New threshold values
|
|
806
|
+
*/
|
|
807
|
+
updateThresholds(newThresholds) {
|
|
808
|
+
this.thresholds = this.mergeThresholds(this.thresholds, newThresholds);
|
|
809
|
+
console.log('[AlertEngine] Thresholds updated:', this.thresholds);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
module.exports = { AlertEngine, DEFAULT_THRESHOLDS, setExecuteQuery };
|