@hazeljs/ml 0.2.4 → 0.3.1

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.
Files changed (69) hide show
  1. package/dist/evaluation/metrics.service.test.js +288 -0
  2. package/dist/experiments/__tests__/experiment.decorator.test.d.ts +2 -0
  3. package/dist/experiments/__tests__/experiment.decorator.test.d.ts.map +1 -0
  4. package/dist/experiments/__tests__/experiment.decorator.test.js +121 -0
  5. package/dist/experiments/__tests__/experiment.service.test.d.ts +2 -0
  6. package/dist/experiments/__tests__/experiment.service.test.d.ts.map +1 -0
  7. package/dist/experiments/__tests__/experiment.service.test.js +460 -0
  8. package/dist/experiments/experiment.decorator.d.ts +44 -0
  9. package/dist/experiments/experiment.decorator.d.ts.map +1 -0
  10. package/dist/experiments/experiment.decorator.js +51 -0
  11. package/dist/experiments/experiment.service.d.ts +42 -0
  12. package/dist/experiments/experiment.service.d.ts.map +1 -0
  13. package/dist/experiments/experiment.service.js +355 -0
  14. package/dist/experiments/experiment.types.d.ts +60 -0
  15. package/dist/experiments/experiment.types.d.ts.map +1 -0
  16. package/dist/experiments/experiment.types.js +5 -0
  17. package/dist/experiments/index.d.ts +9 -0
  18. package/dist/experiments/index.d.ts.map +1 -0
  19. package/dist/experiments/index.js +16 -0
  20. package/dist/features/__tests__/feature-view.decorator.test.d.ts +2 -0
  21. package/dist/features/__tests__/feature-view.decorator.test.d.ts.map +1 -0
  22. package/dist/features/__tests__/feature-view.decorator.test.js +168 -0
  23. package/dist/features/__tests__/feature.decorator.test.d.ts +2 -0
  24. package/dist/features/__tests__/feature.decorator.test.d.ts.map +1 -0
  25. package/dist/features/__tests__/feature.decorator.test.js +167 -0
  26. package/dist/features/feature-store.service.d.ts +59 -0
  27. package/dist/features/feature-store.service.d.ts.map +1 -0
  28. package/dist/features/feature-store.service.js +197 -0
  29. package/dist/features/feature-view.decorator.d.ts +52 -0
  30. package/dist/features/feature-view.decorator.d.ts.map +1 -0
  31. package/dist/features/feature-view.decorator.js +54 -0
  32. package/dist/features/feature.decorator.d.ts +42 -0
  33. package/dist/features/feature.decorator.d.ts.map +1 -0
  34. package/dist/features/feature.decorator.js +49 -0
  35. package/dist/features/feature.types.d.ts +93 -0
  36. package/dist/features/feature.types.d.ts.map +1 -0
  37. package/dist/features/feature.types.js +5 -0
  38. package/dist/features/index.d.ts +12 -0
  39. package/dist/features/index.d.ts.map +1 -0
  40. package/dist/features/index.js +29 -0
  41. package/dist/features/offline-store.d.ts +40 -0
  42. package/dist/features/offline-store.d.ts.map +1 -0
  43. package/dist/features/offline-store.js +215 -0
  44. package/dist/features/online-store.d.ts +45 -0
  45. package/dist/features/online-store.d.ts.map +1 -0
  46. package/dist/features/online-store.js +139 -0
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +26 -1
  50. package/dist/monitoring/__tests__/drift.service.test.d.ts +2 -0
  51. package/dist/monitoring/__tests__/drift.service.test.d.ts.map +1 -0
  52. package/dist/monitoring/__tests__/drift.service.test.js +362 -0
  53. package/dist/monitoring/__tests__/monitor.service.test.d.ts +2 -0
  54. package/dist/monitoring/__tests__/monitor.service.test.d.ts.map +1 -0
  55. package/dist/monitoring/__tests__/monitor.service.test.js +360 -0
  56. package/dist/monitoring/drift.service.d.ts +68 -0
  57. package/dist/monitoring/drift.service.d.ts.map +1 -0
  58. package/dist/monitoring/drift.service.js +360 -0
  59. package/dist/monitoring/drift.types.d.ts +44 -0
  60. package/dist/monitoring/drift.types.d.ts.map +1 -0
  61. package/dist/monitoring/drift.types.js +5 -0
  62. package/dist/monitoring/index.d.ts +10 -0
  63. package/dist/monitoring/index.d.ts.map +1 -0
  64. package/dist/monitoring/index.js +13 -0
  65. package/dist/monitoring/monitor.service.d.ts +79 -0
  66. package/dist/monitoring/monitor.service.d.ts.map +1 -0
  67. package/dist/monitoring/monitor.service.js +192 -0
  68. package/dist/training/trainer.service.test.js +105 -0
  69. package/package.json +2 -2
@@ -0,0 +1,360 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const monitor_service_1 = require("../monitor.service");
4
+ const drift_service_1 = require("../drift.service");
5
+ describe('MonitorService', () => {
6
+ let service;
7
+ let driftService;
8
+ beforeEach(() => {
9
+ driftService = new drift_service_1.DriftService();
10
+ service = new monitor_service_1.MonitorService(driftService);
11
+ });
12
+ afterEach(() => {
13
+ service.stop();
14
+ });
15
+ describe('registerModel', () => {
16
+ it('should register a model for monitoring', () => {
17
+ service.registerModel({
18
+ modelName: 'test-model',
19
+ modelVersion: '1.0.0',
20
+ });
21
+ const status = service.getStatus();
22
+ expect(status).toHaveLength(1);
23
+ expect(status[0].modelName).toBe('test-model');
24
+ expect(status[0].modelVersion).toBe('1.0.0');
25
+ });
26
+ it('should register model without version', () => {
27
+ service.registerModel({
28
+ modelName: 'test-model',
29
+ });
30
+ const status = service.getStatus();
31
+ expect(status).toHaveLength(1);
32
+ expect(status[0].modelName).toBe('test-model');
33
+ expect(status[0].modelVersion).toBeUndefined();
34
+ });
35
+ it('should set up periodic checks when interval specified', () => {
36
+ service.registerModel({
37
+ modelName: 'test-model',
38
+ checkIntervalMinutes: 5,
39
+ });
40
+ const status = service.getStatus();
41
+ expect(status[0].isActive).toBe(true);
42
+ expect(status[0].checkInterval).toBe(5);
43
+ });
44
+ it('should not set up periodic checks when interval is 0', () => {
45
+ service.registerModel({
46
+ modelName: 'test-model',
47
+ checkIntervalMinutes: 0,
48
+ });
49
+ const status = service.getStatus();
50
+ expect(status[0].isActive).toBe(false);
51
+ });
52
+ it('should replace old interval when re-registering', () => {
53
+ service.registerModel({
54
+ modelName: 'test-model',
55
+ checkIntervalMinutes: 5,
56
+ });
57
+ service.registerModel({
58
+ modelName: 'test-model',
59
+ checkIntervalMinutes: 10,
60
+ });
61
+ const status = service.getStatus();
62
+ expect(status).toHaveLength(1);
63
+ expect(status[0].checkInterval).toBe(10);
64
+ });
65
+ it('should register with feature drift config', () => {
66
+ service.registerModel({
67
+ modelName: 'test-model',
68
+ featureDrift: {
69
+ method: 'psi',
70
+ threshold: 0.25,
71
+ },
72
+ });
73
+ const status = service.getStatus();
74
+ expect(status).toHaveLength(1);
75
+ });
76
+ it('should register with accuracy monitor', () => {
77
+ service.registerModel({
78
+ modelName: 'test-model',
79
+ accuracyMonitor: {
80
+ threshold: 0.8,
81
+ windowSize: 10,
82
+ },
83
+ });
84
+ const status = service.getStatus();
85
+ expect(status).toHaveLength(1);
86
+ });
87
+ });
88
+ describe('unregisterModel', () => {
89
+ it('should unregister a model', () => {
90
+ service.registerModel({
91
+ modelName: 'test-model',
92
+ modelVersion: '1.0.0',
93
+ });
94
+ service.unregisterModel('test-model', '1.0.0');
95
+ const status = service.getStatus();
96
+ expect(status).toHaveLength(0);
97
+ });
98
+ it('should clear interval when unregistering', () => {
99
+ service.registerModel({
100
+ modelName: 'test-model',
101
+ checkIntervalMinutes: 5,
102
+ });
103
+ service.unregisterModel('test-model');
104
+ const status = service.getStatus();
105
+ expect(status).toHaveLength(0);
106
+ });
107
+ it('should handle unregistering non-existent model', () => {
108
+ expect(() => {
109
+ service.unregisterModel('non-existent');
110
+ }).not.toThrow();
111
+ });
112
+ });
113
+ describe('onAlert', () => {
114
+ it('should add alert handler', () => {
115
+ const handler = jest.fn();
116
+ service.onAlert(handler);
117
+ service.registerModel({
118
+ modelName: 'test-model',
119
+ accuracyMonitor: {
120
+ threshold: 0.9,
121
+ windowSize: 2,
122
+ },
123
+ });
124
+ service.recordAccuracy('test-model', 0.5);
125
+ service.recordAccuracy('test-model', 0.5);
126
+ expect(handler).toHaveBeenCalled();
127
+ });
128
+ it('should support multiple alert handlers', async () => {
129
+ const handler1 = jest.fn();
130
+ const handler2 = jest.fn();
131
+ service.onAlert(handler1);
132
+ service.onAlert(handler2);
133
+ service.registerModel({
134
+ modelName: 'test-model',
135
+ accuracyMonitor: {
136
+ threshold: 0.9,
137
+ windowSize: 2,
138
+ },
139
+ });
140
+ service.recordAccuracy('test-model', 0.5);
141
+ service.recordAccuracy('test-model', 0.5);
142
+ await new Promise((resolve) => setTimeout(resolve, 10));
143
+ expect(handler1).toHaveBeenCalled();
144
+ expect(handler2).toHaveBeenCalled();
145
+ });
146
+ });
147
+ describe('offAlert', () => {
148
+ it('should remove alert handler', () => {
149
+ const handler = jest.fn();
150
+ service.onAlert(handler);
151
+ service.offAlert(handler);
152
+ service.registerModel({
153
+ modelName: 'test-model',
154
+ accuracyMonitor: {
155
+ threshold: 0.9,
156
+ windowSize: 1,
157
+ },
158
+ });
159
+ service.recordAccuracy('test-model', 0.5);
160
+ expect(handler).not.toHaveBeenCalled();
161
+ });
162
+ it('should handle removing non-existent handler', () => {
163
+ const handler = jest.fn();
164
+ expect(() => {
165
+ service.offAlert(handler);
166
+ }).not.toThrow();
167
+ });
168
+ });
169
+ describe('recordPrediction', () => {
170
+ it('should record prediction', () => {
171
+ expect(() => {
172
+ service.recordPrediction('test-model', { feature1: 1.0, feature2: 2.0 }, 'positive');
173
+ }).not.toThrow();
174
+ });
175
+ it('should record numeric prediction', () => {
176
+ expect(() => {
177
+ service.recordPrediction('test-model', { feature1: 1.0 }, 0.95);
178
+ }).not.toThrow();
179
+ });
180
+ });
181
+ describe('recordAccuracy', () => {
182
+ it('should record accuracy', () => {
183
+ service.registerModel({
184
+ modelName: 'test-model',
185
+ });
186
+ expect(() => {
187
+ service.recordAccuracy('test-model', 0.95);
188
+ }).not.toThrow();
189
+ });
190
+ it('should record accuracy with version', () => {
191
+ service.registerModel({
192
+ modelName: 'test-model',
193
+ modelVersion: '1.0.0',
194
+ });
195
+ expect(() => {
196
+ service.recordAccuracy('test-model', 0.95, '1.0.0');
197
+ }).not.toThrow();
198
+ });
199
+ it('should trigger alert when accuracy drops below threshold', () => {
200
+ const handler = jest.fn();
201
+ service.onAlert(handler);
202
+ service.registerModel({
203
+ modelName: 'test-model',
204
+ accuracyMonitor: {
205
+ threshold: 0.9,
206
+ windowSize: 2,
207
+ },
208
+ });
209
+ service.recordAccuracy('test-model', 0.85);
210
+ service.recordAccuracy('test-model', 0.85);
211
+ expect(handler).toHaveBeenCalled();
212
+ const alert = handler.mock.calls[0][0];
213
+ expect(alert.alertType).toBe('accuracy');
214
+ expect(alert.severity).toBe('critical');
215
+ expect(alert.modelName).toBe('test-model');
216
+ });
217
+ it('should not trigger alert when accuracy is above threshold', () => {
218
+ const handler = jest.fn();
219
+ service.onAlert(handler);
220
+ service.registerModel({
221
+ modelName: 'test-model',
222
+ accuracyMonitor: {
223
+ threshold: 0.9,
224
+ windowSize: 2,
225
+ },
226
+ });
227
+ service.recordAccuracy('test-model', 0.95);
228
+ service.recordAccuracy('test-model', 0.95);
229
+ expect(handler).not.toHaveBeenCalled();
230
+ });
231
+ it('should use window size for accuracy calculation', () => {
232
+ const handler = jest.fn();
233
+ service.onAlert(handler);
234
+ service.registerModel({
235
+ modelName: 'test-model',
236
+ accuracyMonitor: {
237
+ threshold: 0.9,
238
+ windowSize: 2,
239
+ },
240
+ });
241
+ service.recordAccuracy('test-model', 0.95);
242
+ service.recordAccuracy('test-model', 0.95);
243
+ service.recordAccuracy('test-model', 0.85);
244
+ service.recordAccuracy('test-model', 0.85);
245
+ expect(handler).toHaveBeenCalled();
246
+ });
247
+ });
248
+ describe('checkModel', () => {
249
+ it('should throw error for unregistered model', async () => {
250
+ await expect(service.checkModel('non-existent')).rejects.toThrow('No monitor registered');
251
+ });
252
+ it('should check model without drift config', async () => {
253
+ service.registerModel({
254
+ modelName: 'test-model',
255
+ });
256
+ const results = await service.checkModel('test-model');
257
+ expect(results).toEqual([]);
258
+ });
259
+ it('should check model with feature drift config', async () => {
260
+ driftService.setReferenceDistribution('feature1', [1, 2, 3, 4, 5]);
261
+ service.registerModel({
262
+ modelName: 'test-model',
263
+ featureDrift: {
264
+ method: 'psi',
265
+ threshold: 0.25,
266
+ windowSize: 100,
267
+ },
268
+ });
269
+ const results = await service.checkModel('test-model');
270
+ expect(Array.isArray(results)).toBe(true);
271
+ });
272
+ it('should emit alert when drift detected', async () => {
273
+ const handler = jest.fn();
274
+ service.onAlert(handler);
275
+ driftService.setReferenceDistribution('feature1', [1, 2, 3, 4, 5]);
276
+ service.registerModel({
277
+ modelName: 'test-model',
278
+ featureDrift: {
279
+ method: 'psi',
280
+ threshold: 0.01,
281
+ windowSize: 100,
282
+ },
283
+ });
284
+ await service.checkModel('test-model');
285
+ // Alert may or may not be called depending on drift detection
286
+ expect(handler).toHaveBeenCalledTimes(0);
287
+ });
288
+ });
289
+ describe('getStatus', () => {
290
+ it('should return empty array when no models registered', () => {
291
+ expect(service.getStatus()).toEqual([]);
292
+ });
293
+ it('should return status for all registered models', () => {
294
+ service.registerModel({
295
+ modelName: 'model1',
296
+ modelVersion: '1.0.0',
297
+ });
298
+ service.registerModel({
299
+ modelName: 'model2',
300
+ checkIntervalMinutes: 5,
301
+ });
302
+ const status = service.getStatus();
303
+ expect(status).toHaveLength(2);
304
+ expect(status[0].modelName).toBe('model1');
305
+ expect(status[1].modelName).toBe('model2');
306
+ expect(status[1].isActive).toBe(true);
307
+ });
308
+ });
309
+ describe('stop', () => {
310
+ it('should stop all monitoring', () => {
311
+ service.registerModel({
312
+ modelName: 'model1',
313
+ checkIntervalMinutes: 5,
314
+ });
315
+ service.registerModel({
316
+ modelName: 'model2',
317
+ checkIntervalMinutes: 10,
318
+ });
319
+ service.stop();
320
+ const status = service.getStatus();
321
+ expect(status[0].isActive).toBe(false);
322
+ expect(status[1].isActive).toBe(false);
323
+ });
324
+ it('should handle stopping when no models registered', () => {
325
+ expect(() => {
326
+ service.stop();
327
+ }).not.toThrow();
328
+ });
329
+ });
330
+ describe('alert handling', () => {
331
+ it('should handle async alert handlers', async () => {
332
+ const handler = jest.fn().mockResolvedValue(undefined);
333
+ service.onAlert(handler);
334
+ service.registerModel({
335
+ modelName: 'test-model',
336
+ accuracyMonitor: {
337
+ threshold: 0.9,
338
+ windowSize: 1,
339
+ },
340
+ });
341
+ service.recordAccuracy('test-model', 0.5);
342
+ await new Promise((resolve) => setTimeout(resolve, 10));
343
+ expect(handler).toHaveBeenCalled();
344
+ });
345
+ it('should handle alert handler errors gracefully', async () => {
346
+ const handler = jest.fn().mockRejectedValue(new Error('Handler error'));
347
+ service.onAlert(handler);
348
+ service.registerModel({
349
+ modelName: 'test-model',
350
+ accuracyMonitor: {
351
+ threshold: 0.9,
352
+ windowSize: 1,
353
+ },
354
+ });
355
+ expect(() => {
356
+ service.recordAccuracy('test-model', 0.5);
357
+ }).not.toThrow();
358
+ });
359
+ });
360
+ });
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Drift Service - Statistical drift detection for ML model monitoring
3
+ */
4
+ import type { DriftConfig, DriftResult, DriftReport, DistributionStats } from './drift.types';
5
+ export declare class DriftService {
6
+ private referenceDistributions;
7
+ /**
8
+ * Set reference distribution for a feature from training data
9
+ */
10
+ setReferenceDistribution(featureName: string, values: number[]): void;
11
+ /**
12
+ * Calculate distribution statistics
13
+ */
14
+ calculateStats(values: number[]): DistributionStats;
15
+ /**
16
+ * Population Stability Index (PSI) - measures shift between two distributions
17
+ * PSI < 0.1: no significant shift
18
+ * PSI 0.1-0.25: moderate shift
19
+ * PSI > 0.25: significant shift
20
+ */
21
+ calculatePSI(reference: number[], current: number[], bins?: number): number;
22
+ /**
23
+ * Kolmogorov-Smirnov test statistic
24
+ * Measures maximum distance between two cumulative distributions
25
+ * Returns D statistic (0-1) and approximate p-value
26
+ */
27
+ calculateKS(reference: number[], current: number[]): {
28
+ d: number;
29
+ pValue: number;
30
+ };
31
+ /**
32
+ * Jensen-Shannon Divergence - symmetric version of KL divergence
33
+ * Range: 0 (identical) to ln(2) ≈ 0.693 (maximally different)
34
+ */
35
+ calculateJSD(reference: number[], current: number[], bins?: number): number;
36
+ /**
37
+ * Chi-square test for categorical features
38
+ */
39
+ calculateChiSquare(reference: Record<string, number>, current: Record<string, number>): {
40
+ chi2: number;
41
+ pValue: number;
42
+ };
43
+ /**
44
+ * Wasserstein distance (Earth Mover's Distance)
45
+ * Measures how much "work" is needed to transform one distribution into another
46
+ */
47
+ calculateWasserstein(reference: number[], current: number[]): number;
48
+ /**
49
+ * Detect drift for numeric features
50
+ */
51
+ detectDrift(featureName: string, currentValues: number[], config: Omit<DriftConfig, 'features' | 'type'>): DriftResult;
52
+ /**
53
+ * Detect drift for categorical features
54
+ */
55
+ detectCategoricalDrift(featureName: string, currentValues: string[], config: Omit<DriftConfig, 'features' | 'type' | 'method'>): DriftResult;
56
+ /**
57
+ * Run full drift detection report on multiple features
58
+ */
59
+ detectDriftReport(features: Record<string, number[]>, config: Pick<DriftConfig, 'method' | 'threshold' | 'windowSize'>): DriftReport;
60
+ /**
61
+ * Detect prediction drift (monitor model output distribution)
62
+ */
63
+ detectPredictionDrift(referencePredictions: number[] | string[], currentPredictions: number[] | string[]): DriftResult;
64
+ private countCategories;
65
+ private chiSquarePValue;
66
+ private normalCDF;
67
+ }
68
+ //# sourceMappingURL=drift.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drift.service.d.ts","sourceRoot":"","sources":["../../src/monitoring/drift.service.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAE9F,qBACa,YAAY;IACvB,OAAO,CAAC,sBAAsB,CAAoC;IAElE;;OAEG;IACH,wBAAwB,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI;IAKrE;;OAEG;IACH,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,iBAAiB;IAmCnD;;;;;OAKG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,SAAK,GAAG,MAAM;IA6BvE;;;;OAIG;IACH,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;IA0BlF;;;OAGG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,SAAK,GAAG,MAAM;IAiCvE;;OAEG;IACH,kBAAkB,CAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;IAkCnC;;;OAGG;IACH,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM;IAcpE;;OAEG;IACH,WAAW,CACT,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,EAAE,EACvB,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,MAAM,CAAC,GAC7C,WAAW;IAwDd;;OAEG;IACH,sBAAsB,CACpB,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,EAAE,EACvB,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,MAAM,GAAG,QAAQ,CAAC,GACxD,WAAW;IA8Bd;;OAEG;IACH,iBAAiB,CACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,EAClC,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,QAAQ,GAAG,WAAW,GAAG,YAAY,CAAC,GAC/D,WAAW;IA0Bd;;OAEG;IACH,qBAAqB,CACnB,oBAAoB,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,EACzC,kBAAkB,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,GACtC,WAAW;IAkCd,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,SAAS;CAiBlB"}