driftdetect-dashboard 0.7.0 → 0.8.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 (40) hide show
  1. package/dist/client/assets/{GalaxyTab-Bc9PKsMk.js → GalaxyTab-BsAmWarF.js} +4 -4
  2. package/dist/client/assets/{GalaxyTab-Bc9PKsMk.js.map → GalaxyTab-BsAmWarF.js.map} +1 -1
  3. package/dist/client/assets/main-CVl-4C5E.css +2 -0
  4. package/dist/client/assets/main-QuEZvA3i.js +59 -0
  5. package/dist/client/assets/main-QuEZvA3i.js.map +1 -0
  6. package/dist/client/assets/{sfxr-CO3_Kukg.js → sfxr-Cr89uVKo.js} +2 -2
  7. package/dist/client/assets/{sfxr-CO3_Kukg.js.map → sfxr-Cr89uVKo.js.map} +1 -1
  8. package/dist/client/index.html +2 -2
  9. package/dist/server/api-routes.d.ts.map +1 -1
  10. package/dist/server/quality-gates-api.d.ts.map +1 -0
  11. package/package.json +21 -21
  12. package/LICENSE +0 -21
  13. package/dist/client/assets/main-BdVQNGtK.js +0 -59
  14. package/dist/client/assets/main-BdVQNGtK.js.map +0 -1
  15. package/dist/client/assets/main-NejYwKFb.css +0 -2
  16. package/dist/server/api-routes.d.ts +0 -50
  17. package/dist/server/api-routes.js +0 -634
  18. package/dist/server/api-routes.js.map +0 -1
  19. package/dist/server/dashboard-server.d.ts +0 -64
  20. package/dist/server/dashboard-server.js +0 -154
  21. package/dist/server/dashboard-server.js.map +0 -1
  22. package/dist/server/drift-data-reader.d.ts +0 -522
  23. package/dist/server/drift-data-reader.js +0 -1550
  24. package/dist/server/drift-data-reader.js.map +0 -1
  25. package/dist/server/express-app.d.ts +0 -24
  26. package/dist/server/express-app.js +0 -74
  27. package/dist/server/express-app.js.map +0 -1
  28. package/dist/server/galaxy-data-transformer.d.ts +0 -178
  29. package/dist/server/galaxy-data-transformer.js +0 -562
  30. package/dist/server/galaxy-data-transformer.js.map +0 -1
  31. package/dist/server/index.d.ts +0 -20
  32. package/dist/server/index.js +0 -14
  33. package/dist/server/index.js.map +0 -1
  34. package/dist/server/pattern-watcher.d.ts +0 -55
  35. package/dist/server/pattern-watcher.d.ts.map +0 -1
  36. package/dist/server/pattern-watcher.js +0 -157
  37. package/dist/server/pattern-watcher.js.map +0 -1
  38. package/dist/server/websocket-server.d.ts +0 -83
  39. package/dist/server/websocket-server.js +0 -189
  40. package/dist/server/websocket-server.js.map +0 -1
@@ -1,1550 +0,0 @@
1
- /**
2
- * DriftDataReader
3
- *
4
- * Reads and parses data from the .drift/ folder structure.
5
- * Provides methods for accessing patterns, violations, files, and configuration.
6
- *
7
- * OPTIMIZED: Uses DataLake for fast reads with pre-computed views.
8
- * Falls back to direct file reading when lake data is unavailable.
9
- *
10
- * @requirements 1.6 - THE Dashboard_Server SHALL read pattern and violation data from the existing `.drift/` folder structure
11
- * @requirements 8.1 - THE Dashboard_Server SHALL expose GET `/api/patterns` to list all patterns
12
- * @requirements 8.2 - THE Dashboard_Server SHALL expose GET `/api/patterns/:id` to get pattern details with locations
13
- * @requirements 8.6 - THE Dashboard_Server SHALL expose GET `/api/violations` to list all violations
14
- * @requirements 8.7 - THE Dashboard_Server SHALL expose GET `/api/files` to get the file tree
15
- * @requirements 8.8 - THE Dashboard_Server SHALL expose GET `/api/files/:path` to get patterns and violations for a specific file
16
- * @requirements 8.9 - THE Dashboard_Server SHALL expose GET `/api/stats` to get overview statistics
17
- */
18
- import * as fs from 'node:fs/promises';
19
- import * as path from 'node:path';
20
- import { createDataLake } from 'driftdetect-core';
21
- // ============================================================================
22
- // Constants
23
- // ============================================================================
24
- const PATTERNS_DIR = 'patterns';
25
- const STATUS_DIRS = ['discovered', 'approved', 'ignored'];
26
- const PATTERN_CATEGORIES = [
27
- 'structural',
28
- 'components',
29
- 'styling',
30
- 'api',
31
- 'auth',
32
- 'errors',
33
- 'data-access',
34
- 'testing',
35
- 'logging',
36
- 'security',
37
- 'config',
38
- 'types',
39
- 'performance',
40
- 'accessibility',
41
- 'documentation',
42
- ];
43
- // ============================================================================
44
- // Helper Functions
45
- // ============================================================================
46
- /**
47
- * Check if a file exists
48
- */
49
- async function fileExists(filePath) {
50
- try {
51
- await fs.access(filePath);
52
- return true;
53
- }
54
- catch {
55
- return false;
56
- }
57
- }
58
- /**
59
- * Convert a PatternLocation to SemanticLocation
60
- */
61
- function toSemanticLocation(loc) {
62
- return {
63
- file: loc.file,
64
- range: {
65
- start: { line: loc.line, character: loc.column },
66
- end: { line: loc.endLine ?? loc.line, character: loc.endColumn ?? loc.column },
67
- },
68
- };
69
- }
70
- /**
71
- * Convert an OutlierLocation to OutlierWithDetails
72
- */
73
- function toOutlierWithDetails(outlier) {
74
- return {
75
- file: outlier.file,
76
- range: {
77
- start: { line: outlier.line, character: outlier.column },
78
- end: { line: outlier.endLine ?? outlier.line, character: outlier.endColumn ?? outlier.column },
79
- },
80
- reason: outlier.reason,
81
- deviationScore: outlier.deviationScore,
82
- };
83
- }
84
- /**
85
- * Generate a unique violation ID from pattern and outlier
86
- */
87
- function generateViolationId(patternId, outlier) {
88
- return `${patternId}-${outlier.file}-${outlier.line}-${outlier.column}`;
89
- }
90
- // ============================================================================
91
- // DriftDataReader Class
92
- // ============================================================================
93
- export class DriftDataReader {
94
- driftDir;
95
- patternsDir;
96
- dataLake;
97
- lakeInitialized = false;
98
- constructor(driftDir) {
99
- this.driftDir = driftDir;
100
- this.patternsDir = path.join(driftDir, PATTERNS_DIR);
101
- // Initialize DataLake for optimized reads
102
- // rootDir is the parent of .drift/
103
- const rootDir = path.dirname(driftDir);
104
- this.dataLake = createDataLake({ rootDir });
105
- }
106
- /**
107
- * Get the drift directory path
108
- */
109
- get directory() {
110
- return this.driftDir;
111
- }
112
- /**
113
- * Initialize the data lake (lazy initialization)
114
- */
115
- async initializeLake() {
116
- if (this.lakeInitialized)
117
- return true;
118
- try {
119
- await this.dataLake.initialize();
120
- this.lakeInitialized = true;
121
- return true;
122
- }
123
- catch {
124
- return false;
125
- }
126
- }
127
- /**
128
- * Get all patterns, optionally filtered
129
- * OPTIMIZED: Uses DataLake pattern shards for fast category-based queries
130
- *
131
- * @requirements 8.1 - List all patterns
132
- */
133
- async getPatterns(query) {
134
- // OPTIMIZATION: Try DataLake first for fast reads
135
- if (await this.initializeLake()) {
136
- try {
137
- const lakePatterns = await this.getPatternsFromLake(query);
138
- if (lakePatterns && lakePatterns.length > 0) {
139
- return lakePatterns;
140
- }
141
- }
142
- catch {
143
- // Fall through to direct file reading
144
- }
145
- }
146
- // Fallback: Read patterns from all status directories
147
- const patterns = [];
148
- for (const status of STATUS_DIRS) {
149
- const statusDir = path.join(this.patternsDir, status);
150
- if (!(await fileExists(statusDir))) {
151
- continue;
152
- }
153
- // Dynamically read all JSON files in this status directory
154
- try {
155
- const files = await fs.readdir(statusDir);
156
- const jsonFiles = files.filter(f => f.endsWith('.json'));
157
- for (const jsonFile of jsonFiles) {
158
- const filePath = path.join(statusDir, jsonFile);
159
- const category = jsonFile.replace('.json', '');
160
- try {
161
- const content = await fs.readFile(filePath, 'utf-8');
162
- const patternFile = JSON.parse(content);
163
- for (const stored of patternFile.patterns) {
164
- const dashboardPattern = this.storedToDashboardPattern(stored, category, status);
165
- patterns.push(dashboardPattern);
166
- }
167
- }
168
- catch (error) {
169
- // Skip files that can't be parsed
170
- console.error(`Error reading pattern file ${filePath}:`, error);
171
- }
172
- }
173
- }
174
- catch (error) {
175
- console.error(`Error reading status directory ${statusDir}:`, error);
176
- }
177
- }
178
- // Apply filters if provided
179
- return this.filterPatterns(patterns, query);
180
- }
181
- /**
182
- * Get patterns from DataLake (optimized path)
183
- */
184
- async getPatternsFromLake(query) {
185
- try {
186
- // Build query options for DataLake
187
- const queryOptions = {
188
- limit: 1000, // Get all patterns
189
- };
190
- if (query?.category) {
191
- queryOptions.categories = [query.category];
192
- }
193
- if (query?.status && query.status !== 'all') {
194
- queryOptions.status = query.status;
195
- }
196
- if (query?.minConfidence !== undefined) {
197
- queryOptions.minConfidence = query.minConfidence;
198
- }
199
- if (query?.search) {
200
- queryOptions.search = query.search;
201
- }
202
- const result = await this.dataLake.query.getPatterns(queryOptions);
203
- if (result.items.length === 0 && result.total === 0) {
204
- return null; // No data in lake, fall back to files
205
- }
206
- // Convert lake PatternSummary to DashboardPattern
207
- return result.items.map(p => ({
208
- id: p.id,
209
- name: p.name,
210
- category: p.category,
211
- subcategory: p.subcategory,
212
- status: p.status,
213
- description: '', // PatternSummary doesn't include description
214
- confidence: {
215
- score: p.confidence,
216
- level: p.confidenceLevel,
217
- },
218
- locationCount: p.locationCount,
219
- outlierCount: p.outlierCount,
220
- severity: p.severity || (p.outlierCount > 0 ? 'warning' : 'info'),
221
- metadata: {
222
- firstSeen: new Date().toISOString(),
223
- lastSeen: new Date().toISOString(),
224
- },
225
- }));
226
- }
227
- catch {
228
- return null;
229
- }
230
- }
231
- /**
232
- * Get a single pattern by ID with all locations
233
- *
234
- * @requirements 8.2 - Get pattern details with locations
235
- */
236
- async getPattern(id) {
237
- // Search through all status directories dynamically
238
- for (const status of STATUS_DIRS) {
239
- const statusDir = path.join(this.patternsDir, status);
240
- if (!(await fileExists(statusDir))) {
241
- continue;
242
- }
243
- try {
244
- const files = await fs.readdir(statusDir);
245
- const jsonFiles = files.filter(f => f.endsWith('.json'));
246
- for (const jsonFile of jsonFiles) {
247
- const filePath = path.join(statusDir, jsonFile);
248
- const category = jsonFile.replace('.json', '');
249
- try {
250
- const content = await fs.readFile(filePath, 'utf-8');
251
- const patternFile = JSON.parse(content);
252
- const stored = patternFile.patterns.find((p) => p.id === id);
253
- if (stored) {
254
- return this.storedToDashboardPatternWithLocations(stored, category, status);
255
- }
256
- }
257
- catch (error) {
258
- // Skip files that can't be parsed
259
- console.error(`Error reading pattern file ${filePath}:`, error);
260
- }
261
- }
262
- }
263
- catch (error) {
264
- console.error(`Error reading status directory ${statusDir}:`, error);
265
- }
266
- }
267
- return null;
268
- }
269
- /**
270
- * Get all violations, optionally filtered
271
- *
272
- * Violations are derived from pattern outliers.
273
- *
274
- * @requirements 8.6 - List all violations
275
- */
276
- async getViolations(query) {
277
- const violations = [];
278
- // Read patterns from all status directories dynamically
279
- for (const status of STATUS_DIRS) {
280
- const statusDir = path.join(this.patternsDir, status);
281
- if (!(await fileExists(statusDir))) {
282
- continue;
283
- }
284
- try {
285
- const files = await fs.readdir(statusDir);
286
- const jsonFiles = files.filter(f => f.endsWith('.json'));
287
- for (const jsonFile of jsonFiles) {
288
- const filePath = path.join(statusDir, jsonFile);
289
- try {
290
- const content = await fs.readFile(filePath, 'utf-8');
291
- const patternFile = JSON.parse(content);
292
- for (const stored of patternFile.patterns) {
293
- // Convert outliers to violations
294
- for (const outlier of stored.outliers) {
295
- const violation = this.outlierToViolation(stored, outlier);
296
- violations.push(violation);
297
- }
298
- }
299
- }
300
- catch (error) {
301
- // Skip files that can't be parsed
302
- console.error(`Error reading pattern file ${filePath}:`, error);
303
- }
304
- }
305
- }
306
- catch (error) {
307
- console.error(`Error reading status directory ${statusDir}:`, error);
308
- }
309
- }
310
- // Apply filters if provided
311
- return this.filterViolations(violations, query);
312
- }
313
- /**
314
- * Get dashboard statistics
315
- * OPTIMIZED: Uses DataLake status view for instant response
316
- * @requirements 8.9 - GET `/api/stats` to get overview statistics
317
- */
318
- async getStats() {
319
- // OPTIMIZATION: Try DataLake status view first (instant)
320
- if (await this.initializeLake()) {
321
- try {
322
- const statusView = await this.dataLake.query.getStatus();
323
- if (statusView) {
324
- return this.statusViewToStats(statusView);
325
- }
326
- }
327
- catch {
328
- // Fall through to direct file reading
329
- }
330
- }
331
- // Fallback: Compute from raw pattern files
332
- const patterns = await this.getPatterns();
333
- const violations = await this.getViolations();
334
- // Count patterns by status
335
- const byStatus = {
336
- discovered: 0,
337
- approved: 0,
338
- ignored: 0,
339
- };
340
- for (const pattern of patterns) {
341
- byStatus[pattern.status]++;
342
- }
343
- // Count patterns by category - dynamically from actual patterns
344
- const byCategory = {};
345
- for (const pattern of patterns) {
346
- const category = pattern.category;
347
- byCategory[category] = (byCategory[category] || 0) + 1;
348
- }
349
- // Count violations by severity
350
- const bySeverity = {
351
- error: 0,
352
- warning: 0,
353
- info: 0,
354
- hint: 0,
355
- };
356
- for (const violation of violations) {
357
- const severity = violation.severity;
358
- if (severity in bySeverity) {
359
- bySeverity[severity]++;
360
- }
361
- }
362
- // Collect unique files from patterns and violations
363
- const filesSet = new Set();
364
- for (const pattern of patterns) {
365
- // We need to get the full pattern to access locations
366
- const fullPattern = await this.getPattern(pattern.id);
367
- if (fullPattern) {
368
- for (const loc of fullPattern.locations) {
369
- filesSet.add(loc.file);
370
- }
371
- for (const outlier of fullPattern.outliers) {
372
- filesSet.add(outlier.file);
373
- }
374
- }
375
- }
376
- // Calculate health score
377
- const healthScore = this.calculateHealthScore(violations, patterns);
378
- // Get last scan time from pattern metadata
379
- let lastScan = null;
380
- for (const pattern of patterns) {
381
- if (pattern.metadata.lastSeen) {
382
- if (!lastScan || pattern.metadata.lastSeen > lastScan) {
383
- lastScan = pattern.metadata.lastSeen;
384
- }
385
- }
386
- }
387
- return {
388
- healthScore,
389
- patterns: {
390
- total: patterns.length,
391
- byStatus,
392
- byCategory: byCategory,
393
- },
394
- violations: {
395
- total: violations.length,
396
- bySeverity,
397
- },
398
- files: {
399
- total: filesSet.size,
400
- scanned: filesSet.size,
401
- },
402
- detectors: {
403
- active: Object.keys(byCategory).length, // Count unique categories found
404
- total: Object.keys(byCategory).length,
405
- },
406
- lastScan,
407
- };
408
- }
409
- /**
410
- * Convert StatusView from DataLake to DashboardStats
411
- */
412
- statusViewToStats(view) {
413
- return {
414
- healthScore: view.health.score,
415
- patterns: {
416
- total: view.patterns.total,
417
- byStatus: {
418
- discovered: view.patterns.discovered,
419
- approved: view.patterns.approved,
420
- ignored: view.patterns.ignored,
421
- },
422
- byCategory: view.patterns.byCategory,
423
- },
424
- violations: {
425
- total: view.issues.critical + view.issues.warnings,
426
- bySeverity: {
427
- error: view.issues.critical,
428
- warning: view.issues.warnings,
429
- info: 0,
430
- hint: 0,
431
- },
432
- },
433
- files: {
434
- // StatusView doesn't track files, estimate from patterns
435
- total: 0,
436
- scanned: view.lastScan.filesScanned,
437
- },
438
- detectors: {
439
- active: Object.keys(view.patterns.byCategory).length,
440
- total: Object.keys(view.patterns.byCategory).length,
441
- },
442
- lastScan: view.lastScan.timestamp || null,
443
- };
444
- }
445
- /**
446
- * Get the file tree structure
447
- * @requirements 8.7 - GET `/api/files` to get the file tree
448
- */
449
- async getFileTree() {
450
- const patterns = await this.getPatterns();
451
- const violations = await this.getViolations();
452
- // Collect file information
453
- const fileInfo = new Map();
454
- // Count patterns per file
455
- for (const pattern of patterns) {
456
- const fullPattern = await this.getPattern(pattern.id);
457
- if (fullPattern) {
458
- for (const loc of fullPattern.locations) {
459
- const info = fileInfo.get(loc.file) || { patternCount: 0, violationCount: 0 };
460
- info.patternCount++;
461
- fileInfo.set(loc.file, info);
462
- }
463
- }
464
- }
465
- // Count violations per file and track highest severity
466
- for (const violation of violations) {
467
- const info = fileInfo.get(violation.file) || { patternCount: 0, violationCount: 0 };
468
- info.violationCount++;
469
- // Track highest severity
470
- const violationSeverity = violation.severity;
471
- if (!info.severity || this.compareSeverity(violationSeverity, info.severity) > 0) {
472
- info.severity = violationSeverity;
473
- }
474
- fileInfo.set(violation.file, info);
475
- }
476
- // Build tree structure
477
- return this.buildFileTree(fileInfo);
478
- }
479
- /**
480
- * Get details for a specific file
481
- * @requirements 8.8 - GET `/api/files/:path` to get patterns and violations for a specific file
482
- */
483
- async getFileDetails(filePath) {
484
- const patterns = await this.getPatterns();
485
- const violations = await this.getViolations();
486
- // Find patterns that have locations in this file
487
- const filePatterns = [];
488
- for (const pattern of patterns) {
489
- const fullPattern = await this.getPattern(pattern.id);
490
- if (fullPattern) {
491
- const locationsInFile = fullPattern.locations.filter((loc) => loc.file === filePath);
492
- if (locationsInFile.length > 0) {
493
- filePatterns.push({
494
- id: pattern.id,
495
- name: pattern.name,
496
- category: pattern.category,
497
- locations: locationsInFile,
498
- });
499
- }
500
- }
501
- }
502
- // Find violations in this file
503
- const fileViolations = violations.filter((v) => v.file === filePath);
504
- // If no patterns or violations found, return null
505
- if (filePatterns.length === 0 && fileViolations.length === 0) {
506
- return null;
507
- }
508
- // Determine language from file extension
509
- const language = this.getLanguageFromPath(filePath);
510
- return {
511
- path: filePath,
512
- language,
513
- lineCount: 0, // We don't have access to actual file content
514
- patterns: filePatterns,
515
- violations: fileViolations,
516
- };
517
- }
518
- /**
519
- * Get configuration
520
- * @requirements 8.10 - GET `/api/config` to get configuration
521
- */
522
- async getConfig() {
523
- const configPath = path.join(this.driftDir, 'config.json');
524
- if (!(await fileExists(configPath))) {
525
- // Return default config if none exists
526
- return this.getDefaultConfig();
527
- }
528
- try {
529
- const content = await fs.readFile(configPath, 'utf-8');
530
- return JSON.parse(content);
531
- }
532
- catch (error) {
533
- console.error('Error reading config:', error);
534
- return this.getDefaultConfig();
535
- }
536
- }
537
- /**
538
- * Update configuration
539
- * @requirements 8.11 - PUT `/api/config` to update configuration
540
- */
541
- async updateConfig(partial) {
542
- const configPath = path.join(this.driftDir, 'config.json');
543
- const currentConfig = await this.getConfig();
544
- // Merge the partial config with current config
545
- const newConfig = {
546
- ...currentConfig,
547
- ...partial,
548
- detectors: partial.detectors ?? currentConfig.detectors,
549
- severityOverrides: {
550
- ...currentConfig.severityOverrides,
551
- ...partial.severityOverrides,
552
- },
553
- ignorePatterns: partial.ignorePatterns ?? currentConfig.ignorePatterns,
554
- };
555
- await fs.writeFile(configPath, JSON.stringify(newConfig, null, 2));
556
- }
557
- /**
558
- * Approve a pattern - changes status to 'approved'
559
- * @requirements 4.4 - Approve pattern
560
- * @requirements 8.3 - POST `/api/patterns/:id/approve` to approve a pattern
561
- */
562
- async approvePattern(id) {
563
- await this.changePatternStatus(id, 'approved');
564
- }
565
- /**
566
- * Ignore a pattern - changes status to 'ignored'
567
- * @requirements 4.5 - Ignore pattern
568
- * @requirements 8.4 - POST `/api/patterns/:id/ignore` to ignore a pattern
569
- */
570
- async ignorePattern(id) {
571
- await this.changePatternStatus(id, 'ignored');
572
- }
573
- /**
574
- * Delete a pattern - removes from storage
575
- * @requirements 4.6 - Delete pattern
576
- * @requirements 8.5 - DELETE `/api/patterns/:id` to delete a pattern
577
- */
578
- async deletePattern(id) {
579
- // Find the pattern and its location
580
- const location = await this.findPatternLocation(id);
581
- if (!location) {
582
- throw new Error(`Pattern not found: ${id}`);
583
- }
584
- const { filePath } = location;
585
- // Read the pattern file
586
- const content = await fs.readFile(filePath, 'utf-8');
587
- const patternFile = JSON.parse(content);
588
- // Remove the pattern
589
- patternFile.patterns = patternFile.patterns.filter((p) => p.id !== id);
590
- patternFile.lastUpdated = new Date().toISOString();
591
- // Write back or delete file if empty
592
- if (patternFile.patterns.length === 0) {
593
- await fs.unlink(filePath);
594
- }
595
- else {
596
- await fs.writeFile(filePath, JSON.stringify(patternFile, null, 2));
597
- }
598
- }
599
- // ==========================================================================
600
- // Private Helper Methods
601
- // ==========================================================================
602
- /**
603
- * Convert a StoredPattern to DashboardPattern
604
- */
605
- storedToDashboardPattern(stored, category, status) {
606
- return {
607
- id: stored.id,
608
- name: stored.name,
609
- category,
610
- subcategory: stored.subcategory,
611
- status,
612
- description: stored.description,
613
- confidence: {
614
- score: stored.confidence.score,
615
- level: stored.confidence.level,
616
- },
617
- locationCount: stored.locations.length,
618
- outlierCount: stored.outliers.length,
619
- severity: stored.severity,
620
- metadata: {
621
- firstSeen: stored.metadata.firstSeen,
622
- lastSeen: stored.metadata.lastSeen,
623
- tags: stored.metadata.tags,
624
- },
625
- };
626
- }
627
- /**
628
- * Convert a StoredPattern to DashboardPatternWithLocations
629
- */
630
- storedToDashboardPatternWithLocations(stored, category, status) {
631
- const base = this.storedToDashboardPattern(stored, category, status);
632
- return {
633
- ...base,
634
- locations: stored.locations.map(toSemanticLocation),
635
- outliers: stored.outliers.map(toOutlierWithDetails),
636
- };
637
- }
638
- /**
639
- * Convert an outlier to a violation
640
- */
641
- outlierToViolation(pattern, outlier) {
642
- return {
643
- id: generateViolationId(pattern.id, outlier),
644
- patternId: pattern.id,
645
- patternName: pattern.name,
646
- severity: pattern.severity,
647
- file: outlier.file,
648
- range: {
649
- start: { line: outlier.line, character: outlier.column },
650
- end: { line: outlier.endLine ?? outlier.line, character: outlier.endColumn ?? outlier.column },
651
- },
652
- message: outlier.reason,
653
- expected: pattern.description,
654
- actual: outlier.reason,
655
- };
656
- }
657
- /**
658
- * Filter patterns based on query
659
- */
660
- filterPatterns(patterns, query) {
661
- if (!query) {
662
- return patterns;
663
- }
664
- return patterns.filter((pattern) => {
665
- // Filter by category
666
- if (query.category && pattern.category !== query.category) {
667
- return false;
668
- }
669
- // Filter by status
670
- if (query.status && pattern.status !== query.status) {
671
- return false;
672
- }
673
- // Filter by minimum confidence
674
- if (query.minConfidence !== undefined && pattern.confidence.score < query.minConfidence) {
675
- return false;
676
- }
677
- // Filter by search term (name or description)
678
- if (query.search) {
679
- const searchLower = query.search.toLowerCase();
680
- const nameMatch = pattern.name.toLowerCase().includes(searchLower);
681
- const descMatch = pattern.description.toLowerCase().includes(searchLower);
682
- if (!nameMatch && !descMatch) {
683
- return false;
684
- }
685
- }
686
- return true;
687
- });
688
- }
689
- /**
690
- * Filter violations based on query
691
- */
692
- filterViolations(violations, query) {
693
- if (!query) {
694
- return violations;
695
- }
696
- return violations.filter((violation) => {
697
- // Filter by severity
698
- if (query.severity && violation.severity !== query.severity) {
699
- return false;
700
- }
701
- // Filter by file
702
- if (query.file && violation.file !== query.file) {
703
- return false;
704
- }
705
- // Filter by pattern ID
706
- if (query.patternId && violation.patternId !== query.patternId) {
707
- return false;
708
- }
709
- // Filter by search term (message or pattern name)
710
- if (query.search) {
711
- const searchLower = query.search.toLowerCase();
712
- const messageMatch = violation.message.toLowerCase().includes(searchLower);
713
- const nameMatch = violation.patternName.toLowerCase().includes(searchLower);
714
- if (!messageMatch && !nameMatch) {
715
- return false;
716
- }
717
- }
718
- return true;
719
- });
720
- }
721
- /**
722
- * Calculate health score based on violations and patterns
723
- *
724
- * Health score formula:
725
- * - Base score starts at 100
726
- * - Deduct for violations by severity (error: -10, warning: -3, info: -1, hint: 0)
727
- * - Bonus for approved patterns (shows intentional architecture)
728
- * - Clamp to 0-100
729
- */
730
- calculateHealthScore(violations, patterns) {
731
- let score = 100;
732
- // Deduct for violations by severity
733
- for (const violation of violations) {
734
- switch (violation.severity) {
735
- case 'error':
736
- score -= 10;
737
- break;
738
- case 'warning':
739
- score -= 3;
740
- break;
741
- case 'info':
742
- score -= 1;
743
- break;
744
- // hint doesn't deduct
745
- }
746
- }
747
- // Bonus for approved patterns (shows intentional architecture)
748
- if (patterns.length > 0) {
749
- const approvedCount = patterns.filter((p) => p.status === 'approved').length;
750
- const approvalRate = approvedCount / patterns.length;
751
- score += approvalRate * 10;
752
- }
753
- // Clamp to 0-100
754
- return Math.max(0, Math.min(100, Math.round(score)));
755
- }
756
- /**
757
- * Build a hierarchical file tree from file information
758
- */
759
- buildFileTree(fileInfo) {
760
- // First pass: collect all unique directory paths and files
761
- const nodeMap = new Map();
762
- for (const [filePath, info] of fileInfo) {
763
- const parts = filePath.split('/').filter(Boolean);
764
- if (parts.length === 0)
765
- continue;
766
- let currentPath = '';
767
- // Create directory nodes
768
- for (let i = 0; i < parts.length - 1; i++) {
769
- const part = parts[i];
770
- currentPath = currentPath ? `${currentPath}/${part}` : part;
771
- if (!nodeMap.has(currentPath)) {
772
- nodeMap.set(currentPath, {
773
- name: part,
774
- path: currentPath,
775
- type: 'directory',
776
- children: [],
777
- patternCount: 0,
778
- violationCount: 0,
779
- });
780
- }
781
- }
782
- // Create file node
783
- const fileName = parts[parts.length - 1];
784
- const fullPath = parts.join('/');
785
- const fileNode = {
786
- name: fileName,
787
- path: fullPath,
788
- type: 'file',
789
- patternCount: info.patternCount,
790
- violationCount: info.violationCount,
791
- };
792
- if (info.severity) {
793
- fileNode.severity = info.severity;
794
- }
795
- nodeMap.set(fullPath, fileNode);
796
- }
797
- // Second pass: build parent-child relationships and aggregate counts
798
- for (const [nodePath, node] of nodeMap) {
799
- if (node.type === 'file') {
800
- // Find parent directory
801
- const parts = nodePath.split('/');
802
- if (parts.length > 1) {
803
- const parentPath = parts.slice(0, -1).join('/');
804
- const parent = nodeMap.get(parentPath);
805
- if (parent && parent.children) {
806
- parent.children.push(node);
807
- // Aggregate counts to parent
808
- if (parent.patternCount !== undefined && node.patternCount !== undefined) {
809
- parent.patternCount += node.patternCount;
810
- }
811
- if (parent.violationCount !== undefined && node.violationCount !== undefined) {
812
- parent.violationCount += node.violationCount;
813
- }
814
- // Track highest severity
815
- if (node.severity) {
816
- if (!parent.severity || this.compareSeverity(node.severity, parent.severity) > 0) {
817
- parent.severity = node.severity;
818
- }
819
- }
820
- }
821
- }
822
- }
823
- }
824
- // Third pass: link directories to their parents
825
- for (const [nodePath, node] of nodeMap) {
826
- if (node.type === 'directory') {
827
- const parts = nodePath.split('/');
828
- if (parts.length > 1) {
829
- const parentPath = parts.slice(0, -1).join('/');
830
- const parent = nodeMap.get(parentPath);
831
- if (parent && parent.children) {
832
- // Check if not already added
833
- if (!parent.children.some(c => c.path === node.path)) {
834
- parent.children.push(node);
835
- }
836
- // Aggregate counts to parent
837
- if (parent.patternCount !== undefined && node.patternCount !== undefined) {
838
- parent.patternCount += node.patternCount;
839
- }
840
- if (parent.violationCount !== undefined && node.violationCount !== undefined) {
841
- parent.violationCount += node.violationCount;
842
- }
843
- // Track highest severity
844
- if (node.severity) {
845
- if (!parent.severity || this.compareSeverity(node.severity, parent.severity) > 0) {
846
- parent.severity = node.severity;
847
- }
848
- }
849
- }
850
- }
851
- }
852
- }
853
- // Get root nodes (nodes without parents)
854
- const rootNodes = [];
855
- for (const [nodePath, node] of nodeMap) {
856
- const parts = nodePath.split('/');
857
- if (parts.length === 1) {
858
- rootNodes.push(node);
859
- }
860
- }
861
- // Sort and return
862
- return this.sortFileTree(rootNodes);
863
- }
864
- /**
865
- * Sort file tree: directories first, then alphabetically
866
- */
867
- sortFileTree(nodes) {
868
- return nodes
869
- .map((node) => {
870
- if (node.children && node.children.length > 0) {
871
- return {
872
- ...node,
873
- children: this.sortFileTree(node.children),
874
- };
875
- }
876
- return node;
877
- })
878
- .sort((a, b) => {
879
- // Directories first
880
- if (a.type !== b.type) {
881
- return a.type === 'directory' ? -1 : 1;
882
- }
883
- // Then alphabetically
884
- return a.name.localeCompare(b.name);
885
- });
886
- }
887
- /**
888
- * Compare severity levels
889
- * Returns positive if a > b, negative if a < b, 0 if equal
890
- */
891
- compareSeverity(a, b) {
892
- const order = {
893
- error: 4,
894
- warning: 3,
895
- info: 2,
896
- hint: 1,
897
- };
898
- return order[a] - order[b];
899
- }
900
- /**
901
- * Get programming language from file path
902
- */
903
- getLanguageFromPath(filePath) {
904
- const ext = path.extname(filePath).toLowerCase();
905
- const languageMap = {
906
- '.ts': 'typescript',
907
- '.tsx': 'typescript',
908
- '.js': 'javascript',
909
- '.jsx': 'javascript',
910
- '.py': 'python',
911
- '.rb': 'ruby',
912
- '.java': 'java',
913
- '.go': 'go',
914
- '.rs': 'rust',
915
- '.c': 'c',
916
- '.cpp': 'cpp',
917
- '.h': 'c',
918
- '.hpp': 'cpp',
919
- '.cs': 'csharp',
920
- '.php': 'php',
921
- '.swift': 'swift',
922
- '.kt': 'kotlin',
923
- '.scala': 'scala',
924
- '.vue': 'vue',
925
- '.svelte': 'svelte',
926
- '.html': 'html',
927
- '.css': 'css',
928
- '.scss': 'scss',
929
- '.less': 'less',
930
- '.json': 'json',
931
- '.yaml': 'yaml',
932
- '.yml': 'yaml',
933
- '.xml': 'xml',
934
- '.md': 'markdown',
935
- '.sql': 'sql',
936
- '.sh': 'bash',
937
- '.bash': 'bash',
938
- '.zsh': 'zsh',
939
- };
940
- return languageMap[ext] || 'plaintext';
941
- }
942
- /**
943
- * Change pattern status (move between status directories)
944
- */
945
- async changePatternStatus(id, newStatus) {
946
- // Find the pattern and its current location
947
- const location = await this.findPatternLocation(id);
948
- if (!location) {
949
- throw new Error(`Pattern not found: ${id}`);
950
- }
951
- const { status: currentStatus, category, filePath, pattern } = location;
952
- // If already in the target status, nothing to do
953
- if (currentStatus === newStatus) {
954
- return;
955
- }
956
- // Read the source pattern file
957
- const sourceContent = await fs.readFile(filePath, 'utf-8');
958
- const sourceFile = JSON.parse(sourceContent);
959
- // Remove pattern from source file
960
- sourceFile.patterns = sourceFile.patterns.filter((p) => p.id !== id);
961
- sourceFile.lastUpdated = new Date().toISOString();
962
- // Write back source file or delete if empty
963
- if (sourceFile.patterns.length === 0) {
964
- await fs.unlink(filePath);
965
- }
966
- else {
967
- await fs.writeFile(filePath, JSON.stringify(sourceFile, null, 2));
968
- }
969
- // Add pattern to target status directory
970
- const targetDir = path.join(this.patternsDir, newStatus);
971
- const targetPath = path.join(targetDir, `${category}.json`);
972
- // Ensure target directory exists
973
- await fs.mkdir(targetDir, { recursive: true });
974
- // Read or create target file
975
- let targetFile;
976
- if (await fileExists(targetPath)) {
977
- const targetContent = await fs.readFile(targetPath, 'utf-8');
978
- targetFile = JSON.parse(targetContent);
979
- }
980
- else {
981
- targetFile = {
982
- version: '1.0.0',
983
- category,
984
- patterns: [],
985
- lastUpdated: new Date().toISOString(),
986
- };
987
- }
988
- // Update pattern metadata if approving
989
- const updatedPattern = { ...pattern };
990
- if (newStatus === 'approved') {
991
- updatedPattern.metadata = {
992
- ...updatedPattern.metadata,
993
- approvedAt: new Date().toISOString(),
994
- };
995
- }
996
- // Add pattern to target file
997
- targetFile.patterns.push(updatedPattern);
998
- targetFile.lastUpdated = new Date().toISOString();
999
- // Write target file
1000
- await fs.writeFile(targetPath, JSON.stringify(targetFile, null, 2));
1001
- }
1002
- /**
1003
- * Find a pattern's location in the file system
1004
- */
1005
- async findPatternLocation(id) {
1006
- for (const status of STATUS_DIRS) {
1007
- const statusDir = path.join(this.patternsDir, status);
1008
- if (!(await fileExists(statusDir))) {
1009
- continue;
1010
- }
1011
- try {
1012
- const files = await fs.readdir(statusDir);
1013
- const jsonFiles = files.filter(f => f.endsWith('.json'));
1014
- for (const jsonFile of jsonFiles) {
1015
- const filePath = path.join(statusDir, jsonFile);
1016
- const category = jsonFile.replace('.json', '');
1017
- try {
1018
- const content = await fs.readFile(filePath, 'utf-8');
1019
- const patternFile = JSON.parse(content);
1020
- const pattern = patternFile.patterns.find((p) => p.id === id);
1021
- if (pattern) {
1022
- return { status, category, filePath, pattern };
1023
- }
1024
- }
1025
- catch (error) {
1026
- // Skip files that can't be parsed
1027
- console.error(`Error reading pattern file ${filePath}:`, error);
1028
- }
1029
- }
1030
- }
1031
- catch (error) {
1032
- console.error(`Error reading status directory ${statusDir}:`, error);
1033
- }
1034
- }
1035
- return null;
1036
- }
1037
- /**
1038
- * Get default configuration
1039
- */
1040
- getDefaultConfig() {
1041
- return {
1042
- version: '1.0.0',
1043
- detectors: PATTERN_CATEGORIES.map((category) => ({
1044
- id: category,
1045
- name: category.charAt(0).toUpperCase() + category.slice(1).replace(/-/g, ' '),
1046
- enabled: true,
1047
- category,
1048
- })),
1049
- severityOverrides: {},
1050
- ignorePatterns: ['node_modules/**', 'dist/**', '.git/**'],
1051
- };
1052
- }
1053
- /**
1054
- * Get code snippet from a file at a specific line with context
1055
- */
1056
- async getCodeSnippet(filePath, line, contextLines = 3) {
1057
- // driftDir is .drift/, so workspace root is the parent
1058
- const workspaceRoot = path.dirname(this.driftDir);
1059
- const fullPath = path.join(workspaceRoot, filePath);
1060
- try {
1061
- const content = await fs.readFile(fullPath, 'utf-8');
1062
- const lines = content.split('\n');
1063
- const startLine = Math.max(1, line - contextLines);
1064
- const endLine = Math.min(lines.length, line + contextLines);
1065
- const snippetLines = lines.slice(startLine - 1, endLine);
1066
- const code = snippetLines.join('\n');
1067
- return {
1068
- code,
1069
- startLine,
1070
- endLine,
1071
- language: this.getLanguageFromPath(filePath),
1072
- };
1073
- }
1074
- catch (error) {
1075
- console.error(`Error reading file ${fullPath}:`, error);
1076
- return null;
1077
- }
1078
- }
1079
- // ==========================================================================
1080
- // Contract Methods (BE↔FE mismatch detection)
1081
- // ==========================================================================
1082
- /**
1083
- * Get all contracts, optionally filtered
1084
- */
1085
- async getContracts(query) {
1086
- const contracts = [];
1087
- const contractsDir = path.join(this.driftDir, 'contracts');
1088
- const statusDirs = ['discovered', 'verified', 'mismatch', 'ignored'];
1089
- for (const status of statusDirs) {
1090
- const statusDir = path.join(contractsDir, status);
1091
- if (!(await fileExists(statusDir))) {
1092
- continue;
1093
- }
1094
- const filePath = path.join(statusDir, 'contracts.json');
1095
- if (!(await fileExists(filePath))) {
1096
- continue;
1097
- }
1098
- try {
1099
- const content = await fs.readFile(filePath, 'utf-8');
1100
- const contractFile = JSON.parse(content);
1101
- for (const stored of contractFile.contracts) {
1102
- contracts.push({
1103
- ...stored,
1104
- status,
1105
- mismatchCount: stored.mismatches?.length || 0,
1106
- });
1107
- }
1108
- }
1109
- catch (error) {
1110
- console.error(`Error reading contract file ${filePath}:`, error);
1111
- }
1112
- }
1113
- // Apply filters
1114
- return this.filterContracts(contracts, query);
1115
- }
1116
- /**
1117
- * Get a single contract by ID
1118
- */
1119
- async getContract(id) {
1120
- const contractsDir = path.join(this.driftDir, 'contracts');
1121
- const statusDirs = ['discovered', 'verified', 'mismatch', 'ignored'];
1122
- for (const status of statusDirs) {
1123
- const filePath = path.join(contractsDir, status, 'contracts.json');
1124
- if (!(await fileExists(filePath))) {
1125
- continue;
1126
- }
1127
- try {
1128
- const content = await fs.readFile(filePath, 'utf-8');
1129
- const contractFile = JSON.parse(content);
1130
- const contract = contractFile.contracts.find((c) => c.id === id);
1131
- if (contract) {
1132
- return {
1133
- ...contract,
1134
- status,
1135
- mismatchCount: contract.mismatches?.length || 0,
1136
- };
1137
- }
1138
- }
1139
- catch (error) {
1140
- console.error(`Error reading contract file ${filePath}:`, error);
1141
- }
1142
- }
1143
- return null;
1144
- }
1145
- /**
1146
- * Get contract statistics
1147
- */
1148
- async getContractStats() {
1149
- const contracts = await this.getContracts();
1150
- const byStatus = {
1151
- discovered: 0,
1152
- verified: 0,
1153
- mismatch: 0,
1154
- ignored: 0,
1155
- };
1156
- const byMethod = {
1157
- GET: 0,
1158
- POST: 0,
1159
- PUT: 0,
1160
- PATCH: 0,
1161
- DELETE: 0,
1162
- };
1163
- let totalMismatches = 0;
1164
- const mismatchesByType = {};
1165
- for (const contract of contracts) {
1166
- const statusKey = contract.status;
1167
- const methodKey = contract.method;
1168
- if (statusKey in byStatus) {
1169
- byStatus[statusKey] = (byStatus[statusKey] || 0) + 1;
1170
- }
1171
- if (methodKey in byMethod) {
1172
- byMethod[methodKey] = (byMethod[methodKey] || 0) + 1;
1173
- }
1174
- totalMismatches += contract.mismatchCount;
1175
- for (const mismatch of contract.mismatches || []) {
1176
- mismatchesByType[mismatch.mismatchType] = (mismatchesByType[mismatch.mismatchType] || 0) + 1;
1177
- }
1178
- }
1179
- return {
1180
- totalContracts: contracts.length,
1181
- byStatus,
1182
- byMethod,
1183
- totalMismatches,
1184
- mismatchesByType,
1185
- };
1186
- }
1187
- /**
1188
- * Verify a contract
1189
- */
1190
- async verifyContract(id) {
1191
- await this.changeContractStatus(id, 'verified');
1192
- }
1193
- /**
1194
- * Ignore a contract
1195
- */
1196
- async ignoreContract(id) {
1197
- await this.changeContractStatus(id, 'ignored');
1198
- }
1199
- /**
1200
- * Change contract status
1201
- */
1202
- async changeContractStatus(id, newStatus) {
1203
- const contractsDir = path.join(this.driftDir, 'contracts');
1204
- const statusDirs = ['discovered', 'verified', 'mismatch', 'ignored'];
1205
- let foundContract = null;
1206
- // Find the contract
1207
- for (const status of statusDirs) {
1208
- const filePath = path.join(contractsDir, status, 'contracts.json');
1209
- if (!(await fileExists(filePath))) {
1210
- continue;
1211
- }
1212
- try {
1213
- const content = await fs.readFile(filePath, 'utf-8');
1214
- const contractFile = JSON.parse(content);
1215
- const contractIndex = contractFile.contracts.findIndex((c) => c.id === id);
1216
- if (contractIndex !== -1) {
1217
- foundContract = contractFile.contracts[contractIndex];
1218
- // Remove from current file
1219
- contractFile.contracts.splice(contractIndex, 1);
1220
- contractFile.lastUpdated = new Date().toISOString();
1221
- if (contractFile.contracts.length === 0) {
1222
- await fs.unlink(filePath);
1223
- }
1224
- else {
1225
- await fs.writeFile(filePath, JSON.stringify(contractFile, null, 2));
1226
- }
1227
- break;
1228
- }
1229
- }
1230
- catch (error) {
1231
- console.error(`Error reading contract file ${filePath}:`, error);
1232
- }
1233
- }
1234
- if (!foundContract) {
1235
- throw new Error(`Contract not found: ${id}`);
1236
- }
1237
- // Add to new status directory
1238
- const targetDir = path.join(contractsDir, newStatus);
1239
- const targetPath = path.join(targetDir, 'contracts.json');
1240
- await fs.mkdir(targetDir, { recursive: true });
1241
- let targetFile;
1242
- if (await fileExists(targetPath)) {
1243
- const content = await fs.readFile(targetPath, 'utf-8');
1244
- targetFile = JSON.parse(content);
1245
- }
1246
- else {
1247
- targetFile = {
1248
- version: '1.0.0',
1249
- status: newStatus,
1250
- contracts: [],
1251
- lastUpdated: new Date().toISOString(),
1252
- };
1253
- }
1254
- // Update metadata
1255
- foundContract.metadata = {
1256
- ...foundContract.metadata,
1257
- lastSeen: new Date().toISOString(),
1258
- };
1259
- if (newStatus === 'verified') {
1260
- foundContract.metadata.verifiedAt = new Date().toISOString();
1261
- }
1262
- targetFile.contracts.push(foundContract);
1263
- targetFile.lastUpdated = new Date().toISOString();
1264
- await fs.writeFile(targetPath, JSON.stringify(targetFile, null, 2));
1265
- }
1266
- /**
1267
- * Filter contracts based on query
1268
- */
1269
- filterContracts(contracts, query) {
1270
- if (!query)
1271
- return contracts;
1272
- return contracts.filter((contract) => {
1273
- if (query.status && contract.status !== query.status)
1274
- return false;
1275
- if (query.method && contract.method !== query.method)
1276
- return false;
1277
- if (query.hasMismatches !== undefined) {
1278
- const hasMismatches = contract.mismatchCount > 0;
1279
- if (query.hasMismatches !== hasMismatches)
1280
- return false;
1281
- }
1282
- if (query.search && !contract.endpoint.toLowerCase().includes(query.search.toLowerCase())) {
1283
- return false;
1284
- }
1285
- return true;
1286
- });
1287
- }
1288
- // ==========================================================================
1289
- // Trend / History Methods
1290
- // ==========================================================================
1291
- /**
1292
- * Get trend summary for pattern regressions and improvements
1293
- * OPTIMIZED: Uses DataLake trends view for instant response
1294
- */
1295
- async getTrends(period = '7d') {
1296
- // OPTIMIZATION: Try DataLake trends view first
1297
- if (await this.initializeLake()) {
1298
- try {
1299
- const trendsView = await this.dataLake.views.getTrendsView();
1300
- if (trendsView) {
1301
- return this.trendsViewToSummary(trendsView, period);
1302
- }
1303
- }
1304
- catch {
1305
- // Fall through to direct file reading
1306
- }
1307
- }
1308
- // Fallback: Read from history snapshots directly
1309
- const historyDir = path.join(this.driftDir, 'history', 'snapshots');
1310
- if (!(await fileExists(historyDir))) {
1311
- return null;
1312
- }
1313
- const days = period === '7d' ? 7 : period === '30d' ? 30 : 90;
1314
- const now = new Date();
1315
- const startDate = new Date(now);
1316
- startDate.setDate(startDate.getDate() - days);
1317
- // Get snapshots
1318
- const files = await fs.readdir(historyDir);
1319
- const jsonFiles = files.filter(f => f.endsWith('.json')).sort();
1320
- if (jsonFiles.length < 2) {
1321
- return null; // Need at least 2 snapshots to calculate trends
1322
- }
1323
- // Get latest and comparison snapshot
1324
- const latestFile = jsonFiles[jsonFiles.length - 1];
1325
- const latestContent = await fs.readFile(path.join(historyDir, latestFile), 'utf-8');
1326
- const latestSnapshot = JSON.parse(latestContent);
1327
- // Find snapshot closest to start date
1328
- const startDateStr = startDate.toISOString().split('T')[0];
1329
- let comparisonFile = jsonFiles[0];
1330
- for (const file of jsonFiles) {
1331
- const fileDate = file.replace('.json', '');
1332
- if (fileDate <= startDateStr) {
1333
- comparisonFile = file;
1334
- }
1335
- else {
1336
- break;
1337
- }
1338
- }
1339
- const comparisonContent = await fs.readFile(path.join(historyDir, comparisonFile), 'utf-8');
1340
- const comparisonSnapshot = JSON.parse(comparisonContent);
1341
- // Calculate trends
1342
- return this.calculateTrendSummary(latestSnapshot, comparisonSnapshot, period);
1343
- }
1344
- /**
1345
- * Convert TrendsView from DataLake to TrendSummary
1346
- * TrendsView has: generatedAt, period, overallTrend, healthDelta, regressions, improvements, stableCount, categoryTrends
1347
- */
1348
- trendsViewToSummary(view, period) {
1349
- // Calculate date range from period
1350
- const now = new Date();
1351
- const days = period === '7d' ? 7 : period === '30d' ? 30 : 90;
1352
- const startDate = new Date(now);
1353
- startDate.setDate(startDate.getDate() - days);
1354
- // Convert TrendItem[] to PatternTrend[]
1355
- const regressions = (view.regressions || []).map(r => ({
1356
- patternId: r.patternId,
1357
- patternName: r.patternName,
1358
- category: r.category,
1359
- type: 'regression',
1360
- metric: r.metric,
1361
- previousValue: r.previousValue,
1362
- currentValue: r.currentValue,
1363
- change: r.change,
1364
- changePercent: r.previousValue > 0 ? (r.change / r.previousValue) * 100 : 0,
1365
- severity: r.severity,
1366
- firstSeen: view.generatedAt,
1367
- details: `${r.metric} changed from ${r.previousValue} to ${r.currentValue}`,
1368
- }));
1369
- const improvements = (view.improvements || []).map(i => ({
1370
- patternId: i.patternId,
1371
- patternName: i.patternName,
1372
- category: i.category,
1373
- type: 'improvement',
1374
- metric: i.metric,
1375
- previousValue: i.previousValue,
1376
- currentValue: i.currentValue,
1377
- change: i.change,
1378
- changePercent: i.previousValue > 0 ? (i.change / i.previousValue) * 100 : 0,
1379
- severity: i.severity,
1380
- firstSeen: view.generatedAt,
1381
- details: `${i.metric} improved from ${i.previousValue} to ${i.currentValue}`,
1382
- }));
1383
- // Convert CategoryTrend to TrendSummary format
1384
- const categoryTrends = {};
1385
- for (const [category, trend] of Object.entries(view.categoryTrends || {})) {
1386
- categoryTrends[category] = {
1387
- trend: trend.trend,
1388
- avgConfidenceChange: trend.avgConfidenceChange,
1389
- complianceChange: trend.complianceChange,
1390
- };
1391
- }
1392
- return {
1393
- period,
1394
- startDate: startDate.toISOString().split('T')[0],
1395
- endDate: now.toISOString().split('T')[0],
1396
- regressions,
1397
- improvements,
1398
- stable: view.stableCount || 0,
1399
- overallTrend: view.overallTrend,
1400
- healthDelta: view.healthDelta || 0,
1401
- categoryTrends,
1402
- };
1403
- }
1404
- /**
1405
- * Get historical snapshots for charting
1406
- */
1407
- async getSnapshots(limit = 30) {
1408
- const historyDir = path.join(this.driftDir, 'history', 'snapshots');
1409
- if (!(await fileExists(historyDir))) {
1410
- return [];
1411
- }
1412
- const files = await fs.readdir(historyDir);
1413
- const jsonFiles = files.filter(f => f.endsWith('.json')).sort().slice(-limit);
1414
- const snapshots = [];
1415
- for (const file of jsonFiles) {
1416
- try {
1417
- const content = await fs.readFile(path.join(historyDir, file), 'utf-8');
1418
- snapshots.push(JSON.parse(content));
1419
- }
1420
- catch (error) {
1421
- console.error(`Error reading snapshot ${file}:`, error);
1422
- }
1423
- }
1424
- return snapshots;
1425
- }
1426
- /**
1427
- * Calculate trend summary between two snapshots
1428
- */
1429
- calculateTrendSummary(current, previous, period) {
1430
- const regressions = [];
1431
- const improvements = [];
1432
- const previousMap = new Map(previous.patterns.map(p => [p.patternId, p]));
1433
- // Thresholds
1434
- const CONFIDENCE_THRESHOLD = 0.05;
1435
- const COMPLIANCE_THRESHOLD = 0.10;
1436
- const OUTLIER_THRESHOLD = 3;
1437
- for (const currentPattern of current.patterns) {
1438
- const prevPattern = previousMap.get(currentPattern.patternId);
1439
- if (!prevPattern)
1440
- continue;
1441
- // Check confidence change
1442
- const confidenceChange = currentPattern.confidence - prevPattern.confidence;
1443
- if (Math.abs(confidenceChange) >= CONFIDENCE_THRESHOLD) {
1444
- const trend = {
1445
- patternId: currentPattern.patternId,
1446
- patternName: currentPattern.patternName,
1447
- category: currentPattern.category,
1448
- type: confidenceChange < 0 ? 'regression' : 'improvement',
1449
- metric: 'confidence',
1450
- previousValue: prevPattern.confidence,
1451
- currentValue: currentPattern.confidence,
1452
- change: confidenceChange,
1453
- changePercent: (confidenceChange / prevPattern.confidence) * 100,
1454
- severity: confidenceChange <= -0.15 ? 'critical' : confidenceChange < 0 ? 'warning' : 'info',
1455
- firstSeen: previous.timestamp,
1456
- details: `Confidence ${confidenceChange < 0 ? 'dropped' : 'improved'} from ${(prevPattern.confidence * 100).toFixed(0)}% to ${(currentPattern.confidence * 100).toFixed(0)}%`,
1457
- };
1458
- if (trend.type === 'regression') {
1459
- regressions.push(trend);
1460
- }
1461
- else {
1462
- improvements.push(trend);
1463
- }
1464
- }
1465
- // Check compliance change
1466
- const complianceChange = currentPattern.complianceRate - prevPattern.complianceRate;
1467
- if (Math.abs(complianceChange) >= COMPLIANCE_THRESHOLD) {
1468
- const trend = {
1469
- patternId: currentPattern.patternId,
1470
- patternName: currentPattern.patternName,
1471
- category: currentPattern.category,
1472
- type: complianceChange < 0 ? 'regression' : 'improvement',
1473
- metric: 'compliance',
1474
- previousValue: prevPattern.complianceRate,
1475
- currentValue: currentPattern.complianceRate,
1476
- change: complianceChange,
1477
- changePercent: prevPattern.complianceRate > 0 ? (complianceChange / prevPattern.complianceRate) * 100 : 0,
1478
- severity: complianceChange <= -0.20 ? 'critical' : complianceChange < 0 ? 'warning' : 'info',
1479
- firstSeen: previous.timestamp,
1480
- details: `Compliance ${complianceChange < 0 ? 'dropped' : 'improved'} from ${(prevPattern.complianceRate * 100).toFixed(0)}% to ${(currentPattern.complianceRate * 100).toFixed(0)}%`,
1481
- };
1482
- if (trend.type === 'regression') {
1483
- regressions.push(trend);
1484
- }
1485
- else {
1486
- improvements.push(trend);
1487
- }
1488
- }
1489
- // Check outlier increase
1490
- const outlierChange = currentPattern.outlierCount - prevPattern.outlierCount;
1491
- if (outlierChange >= OUTLIER_THRESHOLD) {
1492
- regressions.push({
1493
- patternId: currentPattern.patternId,
1494
- patternName: currentPattern.patternName,
1495
- category: currentPattern.category,
1496
- type: 'regression',
1497
- metric: 'outliers',
1498
- previousValue: prevPattern.outlierCount,
1499
- currentValue: currentPattern.outlierCount,
1500
- change: outlierChange,
1501
- changePercent: prevPattern.outlierCount > 0 ? (outlierChange / prevPattern.outlierCount) * 100 : 100,
1502
- severity: outlierChange >= 10 ? 'critical' : 'warning',
1503
- firstSeen: previous.timestamp,
1504
- details: `${outlierChange} new outliers (${prevPattern.outlierCount} → ${currentPattern.outlierCount})`,
1505
- });
1506
- }
1507
- }
1508
- // Calculate category trends
1509
- const categoryTrends = {};
1510
- const categories = new Set([
1511
- ...Object.keys(current.summary.byCategory),
1512
- ...Object.keys(previous.summary.byCategory),
1513
- ]);
1514
- for (const category of categories) {
1515
- const currentCat = current.summary.byCategory[category];
1516
- const prevCat = previous.summary.byCategory[category];
1517
- if (currentCat && prevCat) {
1518
- const avgConfidenceChange = currentCat.avgConfidence - prevCat.avgConfidence;
1519
- const complianceChange = currentCat.complianceRate - prevCat.complianceRate;
1520
- categoryTrends[category] = {
1521
- trend: avgConfidenceChange > 0.02 ? 'improving'
1522
- : avgConfidenceChange < -0.02 ? 'declining'
1523
- : 'stable',
1524
- avgConfidenceChange,
1525
- complianceChange,
1526
- };
1527
- }
1528
- }
1529
- // Calculate overall trend
1530
- const healthDelta = current.summary.overallComplianceRate - previous.summary.overallComplianceRate;
1531
- const overallTrend = healthDelta > 0.02 ? 'improving'
1532
- : healthDelta < -0.02 ? 'declining'
1533
- : 'stable';
1534
- // Count stable patterns
1535
- const changedPatternIds = new Set([...regressions, ...improvements].map(t => t.patternId));
1536
- const stableCount = current.patterns.filter(p => !changedPatternIds.has(p.patternId)).length;
1537
- return {
1538
- period,
1539
- startDate: previous.date,
1540
- endDate: current.date,
1541
- regressions,
1542
- improvements,
1543
- stable: stableCount,
1544
- overallTrend,
1545
- healthDelta,
1546
- categoryTrends,
1547
- };
1548
- }
1549
- }
1550
- //# sourceMappingURL=drift-data-reader.js.map