driftdetect 0.4.6 → 0.5.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.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bin/drift.js +27 -1
  3. package/dist/bin/drift.js.map +1 -1
  4. package/dist/commands/approve.d.ts +20 -0
  5. package/dist/commands/approve.d.ts.map +1 -1
  6. package/dist/commands/approve.js +38 -72
  7. package/dist/commands/approve.js.map +1 -1
  8. package/dist/commands/callgraph.js +4 -4
  9. package/dist/commands/callgraph.js.map +1 -1
  10. package/dist/commands/check.d.ts +41 -0
  11. package/dist/commands/check.d.ts.map +1 -1
  12. package/dist/commands/check.js +21 -11
  13. package/dist/commands/check.js.map +1 -1
  14. package/dist/commands/coupling.d.ts +17 -0
  15. package/dist/commands/coupling.d.ts.map +1 -0
  16. package/dist/commands/coupling.js +726 -0
  17. package/dist/commands/coupling.js.map +1 -0
  18. package/dist/commands/error-handling.d.ts +15 -0
  19. package/dist/commands/error-handling.d.ts.map +1 -0
  20. package/dist/commands/error-handling.js +608 -0
  21. package/dist/commands/error-handling.js.map +1 -0
  22. package/dist/commands/export.d.ts +16 -0
  23. package/dist/commands/export.d.ts.map +1 -1
  24. package/dist/commands/export.js +46 -50
  25. package/dist/commands/export.js.map +1 -1
  26. package/dist/commands/files.d.ts +15 -0
  27. package/dist/commands/files.d.ts.map +1 -1
  28. package/dist/commands/files.js +27 -48
  29. package/dist/commands/files.js.map +1 -1
  30. package/dist/commands/ignore.d.ts +20 -0
  31. package/dist/commands/ignore.d.ts.map +1 -1
  32. package/dist/commands/ignore.js +25 -48
  33. package/dist/commands/ignore.js.map +1 -1
  34. package/dist/commands/index.d.ts +6 -0
  35. package/dist/commands/index.d.ts.map +1 -1
  36. package/dist/commands/index.js +7 -0
  37. package/dist/commands/index.js.map +1 -1
  38. package/dist/commands/migrate-storage.d.ts +23 -0
  39. package/dist/commands/migrate-storage.d.ts.map +1 -0
  40. package/dist/commands/migrate-storage.js +337 -0
  41. package/dist/commands/migrate-storage.js.map +1 -0
  42. package/dist/commands/report.d.ts +22 -0
  43. package/dist/commands/report.d.ts.map +1 -1
  44. package/dist/commands/report.js +19 -10
  45. package/dist/commands/report.js.map +1 -1
  46. package/dist/commands/scan.d.ts +37 -0
  47. package/dist/commands/scan.d.ts.map +1 -1
  48. package/dist/commands/scan.js +233 -8
  49. package/dist/commands/scan.js.map +1 -1
  50. package/dist/commands/skills.d.ts +16 -0
  51. package/dist/commands/skills.d.ts.map +1 -0
  52. package/dist/commands/skills.js +409 -0
  53. package/dist/commands/skills.js.map +1 -0
  54. package/dist/commands/status.d.ts +20 -0
  55. package/dist/commands/status.d.ts.map +1 -1
  56. package/dist/commands/status.js +74 -72
  57. package/dist/commands/status.js.map +1 -1
  58. package/dist/commands/test-topology.d.ts +15 -0
  59. package/dist/commands/test-topology.d.ts.map +1 -0
  60. package/dist/commands/test-topology.js +589 -0
  61. package/dist/commands/test-topology.js.map +1 -0
  62. package/dist/commands/where.d.ts +15 -0
  63. package/dist/commands/where.d.ts.map +1 -1
  64. package/dist/commands/where.js +41 -88
  65. package/dist/commands/where.js.map +1 -1
  66. package/dist/commands/wrappers.d.ts +16 -0
  67. package/dist/commands/wrappers.d.ts.map +1 -0
  68. package/dist/commands/wrappers.js +181 -0
  69. package/dist/commands/wrappers.js.map +1 -0
  70. package/dist/services/pattern-service-factory.d.ts +37 -0
  71. package/dist/services/pattern-service-factory.d.ts.map +1 -0
  72. package/dist/services/pattern-service-factory.js +41 -0
  73. package/dist/services/pattern-service-factory.js.map +1 -0
  74. package/dist/services/scanner-service.d.ts +210 -0
  75. package/dist/services/scanner-service.d.ts.map +1 -1
  76. package/dist/services/scanner-service.js +298 -102
  77. package/dist/services/scanner-service.js.map +1 -1
  78. package/dist/workers/detector-worker.d.ts +107 -0
  79. package/dist/workers/detector-worker.d.ts.map +1 -0
  80. package/dist/workers/detector-worker.js +293 -0
  81. package/dist/workers/detector-worker.js.map +1 -0
  82. package/package.json +18 -17
@@ -1,33 +1,28 @@
1
1
  /**
2
- * Scanner Service - Enterprise-grade pattern detection
2
+ * Scanner Service - Enterprise-grade pattern detection with Worker Threads
3
3
  *
4
4
  * Uses the real detectors from driftdetect-detectors to find
5
5
  * high-value architectural patterns and violations.
6
6
  *
7
- * Now includes manifest generation for pattern location discovery.
7
+ * Now uses Piscina worker threads for parallel CPU-bound processing.
8
8
  */
9
9
  import * as fs from 'node:fs/promises';
10
10
  import * as path from 'node:path';
11
+ import * as os from 'node:os';
12
+ import { fileURLToPath } from 'node:url';
11
13
  import { createAllDetectorsArray, getDetectorCounts, } from 'driftdetect-detectors';
12
14
  import { ManifestStore, hashContent, } from 'driftdetect-core';
15
+ // Get the directory of this module for worker path resolution
16
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
17
  // ============================================================================
14
18
  // Location Deduplication
15
19
  // ============================================================================
16
- /**
17
- * Create a unique key for a basic location to enable deduplication
18
- */
19
20
  function locationKey(loc) {
20
21
  return `${loc.file}:${loc.line}:${loc.column}`;
21
22
  }
22
- /**
23
- * Create a unique key for a semantic location to enable deduplication
24
- */
25
23
  function semanticLocationKey(loc) {
26
24
  return `${loc.file}:${loc.range.start}:${loc.range.end}:${loc.name}`;
27
25
  }
28
- /**
29
- * Add a basic location to an array only if it doesn't already exist
30
- */
31
26
  function addUniqueLocation(locations, location, seenKeys) {
32
27
  const key = locationKey(location);
33
28
  const seen = seenKeys || new Set(locations.map(locationKey));
@@ -38,9 +33,6 @@ function addUniqueLocation(locations, location, seenKeys) {
38
33
  locations.push(location);
39
34
  return true;
40
35
  }
41
- /**
42
- * Add a semantic location to an array only if it doesn't already exist
43
- */
44
36
  function addUniqueSemanticLocation(locations, location, seenKeys) {
45
37
  const key = semanticLocationKey(location);
46
38
  const seen = seenKeys || new Set(locations.map(semanticLocationKey));
@@ -87,32 +79,25 @@ function isDetectorApplicable(detector, language) {
87
79
  return info.supportedLanguages.includes(language);
88
80
  }
89
81
  // ============================================================================
90
- // Critical Detectors (highest value)
82
+ // Critical Detectors
91
83
  // ============================================================================
92
84
  const CRITICAL_DETECTOR_IDS = new Set([
93
- // Security - CRITICAL
94
85
  'security/sql-injection',
95
86
  'security/xss-prevention',
96
87
  'security/secret-management',
97
88
  'security/input-sanitization',
98
89
  'security/csrf-protection',
99
- // Auth - CRITICAL
100
90
  'auth/middleware-usage',
101
91
  'auth/token-handling',
102
- // API - HIGH
103
92
  'api/route-structure',
104
93
  'api/error-format',
105
94
  'api/response-envelope',
106
- // Data Access - HIGH
107
95
  'data-access/n-plus-one',
108
96
  'data-access/query-patterns',
109
- // Structural - HIGH
110
97
  'structural/circular-deps',
111
98
  'structural/module-boundaries',
112
- // Errors - HIGH
113
99
  'errors/exception-hierarchy',
114
100
  'errors/try-catch-placement',
115
- // Logging - HIGH
116
101
  'logging/pii-redaction',
117
102
  ]);
118
103
  // ============================================================================
@@ -121,27 +106,34 @@ const CRITICAL_DETECTOR_IDS = new Set([
121
106
  /**
122
107
  * Scanner Service
123
108
  *
124
- * Orchestrates pattern detection across files using real detectors
125
- * from driftdetect-detectors package.
109
+ * Orchestrates pattern detection across files using real detectors.
110
+ * Supports both single-threaded and multi-threaded (Piscina) modes.
126
111
  */
127
112
  export class ScannerService {
128
113
  config;
129
114
  detectors = [];
130
115
  initialized = false;
131
116
  manifestStore = null;
117
+ pool = null;
118
+ PiscinaClass = null;
132
119
  constructor(config) {
133
- this.config = config;
120
+ this.config = {
121
+ ...config,
122
+ // Default to using worker threads
123
+ useWorkerThreads: config.useWorkerThreads ?? true,
124
+ workerThreads: config.workerThreads ?? Math.max(1, os.cpus().length - 1),
125
+ };
134
126
  if (config.generateManifest) {
135
127
  this.manifestStore = new ManifestStore(config.rootDir);
136
128
  }
137
129
  }
138
130
  /**
139
- * Initialize the scanner service - loads all detectors
131
+ * Initialize the scanner service
140
132
  */
141
133
  async initialize() {
142
134
  if (this.initialized)
143
135
  return;
144
- // Create all detectors
136
+ // Create all detectors (needed for counts even in worker mode)
145
137
  this.detectors = await createAllDetectorsArray();
146
138
  // Filter by category if specified
147
139
  if (this.config.categories && this.config.categories.length > 0) {
@@ -155,12 +147,71 @@ export class ScannerService {
155
147
  if (this.config.criticalOnly) {
156
148
  this.detectors = this.detectors.filter(d => CRITICAL_DETECTOR_IDS.has(d.id));
157
149
  }
150
+ // Initialize worker pool if using threads
151
+ if (this.config.useWorkerThreads) {
152
+ try {
153
+ const piscinaModule = await import('piscina');
154
+ // Piscina exports as named export, not default
155
+ this.PiscinaClass = (piscinaModule.Piscina || piscinaModule.default);
156
+ if (!this.PiscinaClass || typeof this.PiscinaClass !== 'function') {
157
+ throw new Error(`Piscina class not found. Module exports: ${Object.keys(piscinaModule).join(', ')}`);
158
+ }
159
+ // Worker path - compiled JS in dist
160
+ const workerPath = path.join(__dirname, '..', 'workers', 'detector-worker.js');
161
+ if (this.config.verbose) {
162
+ console.log(` Worker path: ${workerPath}`);
163
+ }
164
+ this.pool = new this.PiscinaClass({
165
+ filename: workerPath,
166
+ minThreads: this.config.workerThreads, // Keep all workers alive
167
+ maxThreads: this.config.workerThreads,
168
+ idleTimeout: 60000, // 60s idle timeout
169
+ });
170
+ if (this.config.verbose) {
171
+ console.log(` Worker pool initialized with ${this.config.workerThreads} threads`);
172
+ }
173
+ // Warm up all workers in parallel - this loads detectors once per worker
174
+ await this.warmupWorkers();
175
+ }
176
+ catch (error) {
177
+ // Fall back to single-threaded mode
178
+ console.log(` Worker threads unavailable, using single-threaded mode: ${error.message}`);
179
+ if (this.config.verbose) {
180
+ console.log(` Full error: ${error.stack}`);
181
+ }
182
+ this.config.useWorkerThreads = false;
183
+ }
184
+ }
158
185
  // Load existing manifest for incremental scanning
159
186
  if (this.manifestStore) {
160
187
  await this.manifestStore.load();
161
188
  }
162
189
  this.initialized = true;
163
190
  }
191
+ /**
192
+ * Warm up all worker threads by preloading detectors
193
+ * This runs warmup tasks in parallel so all workers load detectors simultaneously
194
+ */
195
+ async warmupWorkers() {
196
+ if (!this.pool)
197
+ return;
198
+ const numWorkers = this.config.workerThreads;
199
+ const warmupTasks = Array(numWorkers).fill(null).map(() => ({
200
+ type: 'warmup',
201
+ categories: this.config.categories,
202
+ criticalOnly: this.config.criticalOnly,
203
+ }));
204
+ if (this.config.verbose) {
205
+ console.log(` Warming up ${numWorkers} workers...`);
206
+ }
207
+ const startTime = Date.now();
208
+ // Run warmup tasks in parallel - each worker gets one task
209
+ await Promise.all(warmupTasks.map(task => this.pool.run(task)));
210
+ const duration = Date.now() - startTime;
211
+ if (this.config.verbose) {
212
+ console.log(` Workers warmed up in ${duration}ms`);
213
+ }
214
+ }
164
215
  /**
165
216
  * Get detector count
166
217
  */
@@ -174,16 +225,211 @@ export class ScannerService {
174
225
  return getDetectorCounts();
175
226
  }
176
227
  /**
177
- * Scan files for patterns using real detectors
228
+ * Check if worker threads are enabled and initialized
229
+ */
230
+ isUsingWorkerThreads() {
231
+ return this.config.useWorkerThreads === true && this.pool !== null;
232
+ }
233
+ /**
234
+ * Get worker thread count
235
+ */
236
+ getWorkerThreadCount() {
237
+ return this.config.workerThreads ?? 0;
238
+ }
239
+ /**
240
+ * Scan files for patterns
178
241
  */
179
242
  async scanFiles(files, projectContext) {
243
+ if (this.config.useWorkerThreads && this.pool) {
244
+ return this.scanFilesWithWorkers(files, projectContext);
245
+ }
246
+ return this.scanFilesSingleThreaded(files, projectContext);
247
+ }
248
+ /**
249
+ * Scan files using worker threads (parallel)
250
+ */
251
+ async scanFilesWithWorkers(files, projectContext) {
252
+ const startTime = Date.now();
253
+ const errors = [];
254
+ // Filter to changed files if incremental
255
+ let filesToScan = files;
256
+ if (this.config.incremental && this.manifestStore) {
257
+ const fullPaths = files.map(f => path.join(this.config.rootDir, f));
258
+ const changedPaths = await this.manifestStore.getChangedFiles(fullPaths);
259
+ filesToScan = changedPaths.map(f => path.relative(this.config.rootDir, f));
260
+ for (const file of filesToScan) {
261
+ this.manifestStore.clearFilePatterns(path.join(this.config.rootDir, file));
262
+ }
263
+ }
264
+ // Create tasks for worker pool
265
+ const tasks = filesToScan.map(file => ({
266
+ file,
267
+ rootDir: this.config.rootDir,
268
+ projectFiles: projectContext.files,
269
+ projectConfig: projectContext.config,
270
+ categories: this.config.categories,
271
+ criticalOnly: this.config.criticalOnly,
272
+ }));
273
+ // Run all tasks in parallel
274
+ const results = await Promise.all(tasks.map(async (task) => {
275
+ try {
276
+ return await this.pool.run(task);
277
+ }
278
+ catch (error) {
279
+ return {
280
+ file: task.file,
281
+ language: null,
282
+ patterns: [],
283
+ violations: [],
284
+ detectorsRan: 0,
285
+ detectorsSkipped: 0,
286
+ duration: 0,
287
+ error: error instanceof Error ? error.message : String(error),
288
+ };
289
+ }
290
+ }));
291
+ // Aggregate results
292
+ return this.aggregateWorkerResults(results, files, startTime, errors);
293
+ }
294
+ /**
295
+ * Aggregate results from worker threads
296
+ */
297
+ async aggregateWorkerResults(results, allFiles, startTime, errors) {
298
+ const fileResults = [];
299
+ const patternMap = new Map();
300
+ const allViolations = [];
301
+ const manifestPatternMap = new Map();
302
+ let totalDetectorsRan = 0;
303
+ let totalDetectorsSkipped = 0;
304
+ for (const result of results) {
305
+ if (result.error) {
306
+ errors.push(`Failed to scan ${result.file}: ${result.error}`);
307
+ }
308
+ totalDetectorsRan += result.detectorsRan;
309
+ totalDetectorsSkipped += result.detectorsSkipped;
310
+ // Convert to FileScanResult
311
+ const filePatterns = result.patterns.map(p => ({
312
+ patternId: p.patternId,
313
+ detectorId: p.detectorId,
314
+ confidence: p.confidence,
315
+ location: p.location,
316
+ }));
317
+ fileResults.push({
318
+ file: result.file,
319
+ patterns: filePatterns,
320
+ violations: result.violations,
321
+ duration: result.duration,
322
+ error: result.error,
323
+ });
324
+ // Aggregate patterns
325
+ for (const match of result.patterns) {
326
+ const key = match.patternId;
327
+ const existing = patternMap.get(key);
328
+ if (existing) {
329
+ addUniqueLocation(existing.locations, match.location);
330
+ existing.occurrences++;
331
+ existing.confidence = Math.max(existing.confidence, match.confidence);
332
+ }
333
+ else {
334
+ patternMap.set(key, {
335
+ patternId: match.patternId,
336
+ detectorId: match.detectorId,
337
+ category: match.category,
338
+ subcategory: match.subcategory,
339
+ name: match.detectorName,
340
+ description: match.detectorDescription,
341
+ locations: [match.location],
342
+ confidence: match.confidence,
343
+ occurrences: 1,
344
+ });
345
+ }
346
+ // Build manifest pattern
347
+ if (this.config.generateManifest && result.language) {
348
+ await this.addToManifest(manifestPatternMap, match, result);
349
+ }
350
+ }
351
+ // Aggregate violations
352
+ for (const violation of result.violations) {
353
+ allViolations.push(violation);
354
+ }
355
+ }
356
+ // Convert pattern map to array
357
+ const patterns = Array.from(patternMap.values());
358
+ // Build and save manifest
359
+ let manifest;
360
+ if (this.config.generateManifest && this.manifestStore) {
361
+ const manifestPatterns = Array.from(manifestPatternMap.values());
362
+ this.manifestStore.updatePatterns(manifestPatterns);
363
+ await this.manifestStore.save();
364
+ manifest = await this.manifestStore.get();
365
+ }
366
+ return {
367
+ files: fileResults,
368
+ patterns,
369
+ violations: allViolations,
370
+ totalPatterns: patterns.reduce((sum, p) => sum + p.occurrences, 0),
371
+ totalViolations: allViolations.length,
372
+ totalFiles: allFiles.length,
373
+ duration: Date.now() - startTime,
374
+ errors,
375
+ detectorStats: {
376
+ total: this.detectors.length,
377
+ ran: totalDetectorsRan,
378
+ skipped: totalDetectorsSkipped,
379
+ },
380
+ workerStats: this.pool ? {
381
+ threadsUsed: this.pool.threads.length,
382
+ tasksCompleted: this.pool.completed,
383
+ } : undefined,
384
+ manifest,
385
+ };
386
+ }
387
+ /**
388
+ * Add pattern match to manifest
389
+ */
390
+ async addToManifest(manifestPatternMap, match, result) {
391
+ try {
392
+ const filePath = path.join(this.config.rootDir, result.file);
393
+ const content = await fs.readFile(filePath, 'utf-8');
394
+ const contentHash = hashContent(content);
395
+ const language = result.language;
396
+ const semanticLoc = this.createSemanticLocation(match.location, content, contentHash, language);
397
+ const manifestKey = `${match.category}/${match.subcategory}/${match.patternId}`;
398
+ const existingManifest = manifestPatternMap.get(manifestKey);
399
+ if (existingManifest) {
400
+ addUniqueSemanticLocation(existingManifest.locations, semanticLoc);
401
+ existingManifest.confidence = Math.max(existingManifest.confidence, match.confidence);
402
+ existingManifest.lastSeen = new Date().toISOString();
403
+ }
404
+ else {
405
+ manifestPatternMap.set(manifestKey, {
406
+ id: manifestKey,
407
+ name: match.detectorName,
408
+ category: match.category,
409
+ subcategory: match.subcategory,
410
+ status: 'discovered',
411
+ confidence: match.confidence,
412
+ locations: [semanticLoc],
413
+ outliers: [],
414
+ description: match.detectorDescription,
415
+ firstSeen: new Date().toISOString(),
416
+ lastSeen: new Date().toISOString(),
417
+ });
418
+ }
419
+ }
420
+ catch {
421
+ // Ignore manifest errors
422
+ }
423
+ }
424
+ /**
425
+ * Scan files single-threaded (fallback)
426
+ */
427
+ async scanFilesSingleThreaded(files, projectContext) {
180
428
  const startTime = Date.now();
181
429
  const fileResults = [];
182
430
  const errors = [];
183
- // Aggregation maps
184
431
  const patternMap = new Map();
185
432
  const allViolations = [];
186
- // Manifest pattern aggregation
187
433
  const manifestPatternMap = new Map();
188
434
  let detectorsRan = 0;
189
435
  let detectorsSkipped = 0;
@@ -193,7 +439,6 @@ export class ScannerService {
193
439
  const fullPaths = files.map(f => path.join(this.config.rootDir, f));
194
440
  const changedPaths = await this.manifestStore.getChangedFiles(fullPaths);
195
441
  filesToScan = changedPaths.map(f => path.relative(this.config.rootDir, f));
196
- // Clear patterns for changed files
197
442
  for (const file of filesToScan) {
198
443
  this.manifestStore.clearFilePatterns(path.join(this.config.rootDir, file));
199
444
  }
@@ -202,7 +447,6 @@ export class ScannerService {
202
447
  const fileStart = Date.now();
203
448
  const filePath = path.join(this.config.rootDir, file);
204
449
  const language = getLanguage(file);
205
- // Skip files we can't detect language for
206
450
  if (!language) {
207
451
  fileResults.push({
208
452
  file,
@@ -217,14 +461,13 @@ export class ScannerService {
217
461
  const contentHash = hashContent(content);
218
462
  const filePatterns = [];
219
463
  const fileViolations = [];
220
- // Create detection context
221
464
  const context = {
222
465
  file,
223
466
  content,
224
467
  language,
225
- ast: null, // AST parsing is optional
226
- imports: [], // Will be populated by detectors that need it
227
- exports: [], // Will be populated by detectors that need it
468
+ ast: null,
469
+ imports: [],
470
+ exports: [],
228
471
  extension: path.extname(file),
229
472
  isTestFile: /\.(test|spec)\.[jt]sx?$/.test(file) || file.includes('__tests__'),
230
473
  isTypeDefinition: file.endsWith('.d.ts'),
@@ -234,7 +477,6 @@ export class ScannerService {
234
477
  config: projectContext.config,
235
478
  },
236
479
  };
237
- // Run applicable detectors
238
480
  for (const detector of this.detectors) {
239
481
  if (!isDetectorApplicable(detector, language)) {
240
482
  detectorsSkipped++;
@@ -244,7 +486,6 @@ export class ScannerService {
244
486
  try {
245
487
  const result = await detector.detect(context);
246
488
  const info = detector.getInfo();
247
- // Process patterns
248
489
  for (const match of result.patterns) {
249
490
  filePatterns.push({
250
491
  patternId: match.patternId,
@@ -252,7 +493,6 @@ export class ScannerService {
252
493
  confidence: match.confidence,
253
494
  location: match.location,
254
495
  });
255
- // Aggregate pattern
256
496
  const key = match.patternId;
257
497
  const existing = patternMap.get(key);
258
498
  if (existing) {
@@ -273,7 +513,6 @@ export class ScannerService {
273
513
  occurrences: 1,
274
514
  });
275
515
  }
276
- // Build manifest pattern with semantic location
277
516
  if (this.config.generateManifest) {
278
517
  const semanticLoc = this.createSemanticLocation(match.location, content, contentHash, language);
279
518
  const manifestKey = `${info.category}/${info.subcategory}/${match.patternId}`;
@@ -300,17 +539,14 @@ export class ScannerService {
300
539
  }
301
540
  }
302
541
  }
303
- // Process violations
304
- // First check the standard violations array
542
+ // Process violations (same as before)
305
543
  let violationsToProcess = result.violations;
306
- // If empty, check for violations in custom metadata (many detectors store them there)
307
544
  if (violationsToProcess.length === 0 && result.metadata?.custom) {
308
545
  const customData = result.metadata.custom;
309
546
  const customViolations = customData['violations'];
310
547
  if (customViolations && Array.isArray(customViolations)) {
311
- // Convert custom violations to standard format
312
548
  violationsToProcess = customViolations.map(cv => {
313
- const violation = {
549
+ const v = {
314
550
  id: `${detector.id}-${cv.file}-${cv.line}-${cv.column}`,
315
551
  patternId: detector.id,
316
552
  severity: cv.severity || 'warning',
@@ -328,9 +564,9 @@ export class ScannerService {
328
564
  occurrences: 1,
329
565
  };
330
566
  if (cv.type) {
331
- violation.explanation = `Violation type: ${cv.type}`;
567
+ v.explanation = `Violation type: ${cv.type}`;
332
568
  }
333
- return violation;
569
+ return v;
334
570
  });
335
571
  }
336
572
  }
@@ -344,39 +580,13 @@ export class ScannerService {
344
580
  line: violation.range.start.line + 1,
345
581
  column: violation.range.start.character + 1,
346
582
  message: violation.message,
347
- explanation: violation.explanation,
348
583
  suggestedFix: violation.expected,
349
584
  };
585
+ if (violation.explanation) {
586
+ aggViolation.explanation = violation.explanation;
587
+ }
350
588
  fileViolations.push(aggViolation);
351
589
  allViolations.push(aggViolation);
352
- // Add violation as outlier to manifest
353
- if (this.config.generateManifest) {
354
- const outlierLoc = this.createSemanticLocation({
355
- file: violation.file,
356
- line: violation.range.start.line + 1,
357
- column: violation.range.start.character + 1,
358
- }, content, contentHash, language, violation.message);
359
- const manifestKey = `${info.category}/${info.subcategory}/${violation.patternId}`;
360
- const existingManifest = manifestPatternMap.get(manifestKey);
361
- if (existingManifest) {
362
- existingManifest.outliers.push(outlierLoc);
363
- }
364
- else {
365
- manifestPatternMap.set(manifestKey, {
366
- id: manifestKey,
367
- name: info.name,
368
- category: info.category,
369
- subcategory: info.subcategory,
370
- status: 'discovered',
371
- confidence: 0.5,
372
- locations: [],
373
- outliers: [outlierLoc],
374
- description: info.description,
375
- firstSeen: new Date().toISOString(),
376
- lastSeen: new Date().toISOString(),
377
- });
378
- }
379
- }
380
590
  }
381
591
  }
382
592
  catch (detectorError) {
@@ -404,9 +614,7 @@ export class ScannerService {
404
614
  });
405
615
  }
406
616
  }
407
- // Convert pattern map to array
408
617
  const patterns = Array.from(patternMap.values());
409
- // Build and save manifest
410
618
  let manifest;
411
619
  if (this.config.generateManifest && this.manifestStore) {
412
620
  const manifestPatterns = Array.from(manifestPatternMap.values());
@@ -414,7 +622,7 @@ export class ScannerService {
414
622
  await this.manifestStore.save();
415
623
  manifest = await this.manifestStore.get();
416
624
  }
417
- const result = {
625
+ return {
418
626
  files: fileResults,
419
627
  patterns,
420
628
  violations: allViolations,
@@ -428,11 +636,8 @@ export class ScannerService {
428
636
  ran: detectorsRan,
429
637
  skipped: detectorsSkipped,
430
638
  },
639
+ manifest,
431
640
  };
432
- if (manifest) {
433
- result.manifest = manifest;
434
- }
435
- return result;
436
641
  }
437
642
  /**
438
643
  * Create a semantic location from a basic location
@@ -440,7 +645,6 @@ export class ScannerService {
440
645
  createSemanticLocation(location, content, hash, language, name) {
441
646
  const lines = content.split('\n');
442
647
  const lineContent = lines[location.line - 1] || '';
443
- // Try to extract semantic info from the line
444
648
  const semanticInfo = this.extractSemanticInfo(lineContent, language);
445
649
  const result = {
446
650
  file: location.file,
@@ -465,89 +669,81 @@ export class ScannerService {
465
669
  */
466
670
  extractSemanticInfo(line, language) {
467
671
  const trimmed = line.trim();
468
- // TypeScript/JavaScript patterns
469
672
  if (language === 'typescript' || language === 'javascript') {
470
- // Class
471
673
  const classMatch = trimmed.match(/^(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/);
472
674
  if (classMatch && classMatch[1]) {
473
675
  return { type: 'class', name: classMatch[1], signature: trimmed.substring(0, 80) };
474
676
  }
475
- // Function
476
677
  const funcMatch = trimmed.match(/^(?:export\s+)?(?:async\s+)?function\s+(\w+)/);
477
678
  if (funcMatch && funcMatch[1]) {
478
679
  return { type: 'function', name: funcMatch[1], signature: trimmed.substring(0, 80) };
479
680
  }
480
- // Arrow function / const
481
681
  const arrowMatch = trimmed.match(/^(?:export\s+)?const\s+(\w+)\s*=/);
482
682
  if (arrowMatch && arrowMatch[1]) {
483
683
  return { type: 'function', name: arrowMatch[1], signature: trimmed.substring(0, 80) };
484
684
  }
485
- // Interface
486
685
  const interfaceMatch = trimmed.match(/^(?:export\s+)?interface\s+(\w+)/);
487
686
  if (interfaceMatch && interfaceMatch[1]) {
488
687
  return { type: 'interface', name: interfaceMatch[1], signature: trimmed.substring(0, 80) };
489
688
  }
490
- // Type
491
689
  const typeMatch = trimmed.match(/^(?:export\s+)?type\s+(\w+)/);
492
690
  if (typeMatch && typeMatch[1]) {
493
691
  return { type: 'type', name: typeMatch[1], signature: trimmed.substring(0, 80) };
494
692
  }
495
693
  }
496
- // Python patterns
497
694
  if (language === 'python') {
498
- // Class
499
695
  const classMatch = trimmed.match(/^class\s+(\w+)/);
500
696
  if (classMatch && classMatch[1]) {
501
697
  return { type: 'class', name: classMatch[1], signature: trimmed.substring(0, 80) };
502
698
  }
503
- // Function/method
504
699
  const defMatch = trimmed.match(/^(?:async\s+)?def\s+(\w+)/);
505
700
  if (defMatch && defMatch[1]) {
506
701
  return { type: 'function', name: defMatch[1], signature: trimmed.substring(0, 80) };
507
702
  }
508
- // Decorator
509
703
  const decoratorMatch = trimmed.match(/^@(\w+)/);
510
704
  if (decoratorMatch && decoratorMatch[1]) {
511
705
  return { type: 'decorator', name: decoratorMatch[1], signature: trimmed };
512
706
  }
513
707
  }
514
- // PHP patterns
515
708
  if (language === 'php') {
516
- // Class
517
709
  const classMatch = trimmed.match(/^(?:abstract\s+|final\s+)?class\s+(\w+)/);
518
710
  if (classMatch && classMatch[1]) {
519
711
  return { type: 'class', name: classMatch[1], signature: trimmed.substring(0, 80) };
520
712
  }
521
- // Interface
522
713
  const interfaceMatch = trimmed.match(/^interface\s+(\w+)/);
523
714
  if (interfaceMatch && interfaceMatch[1]) {
524
715
  return { type: 'interface', name: interfaceMatch[1], signature: trimmed.substring(0, 80) };
525
716
  }
526
- // Trait
527
717
  const traitMatch = trimmed.match(/^trait\s+(\w+)/);
528
718
  if (traitMatch && traitMatch[1]) {
529
719
  return { type: 'class', name: traitMatch[1], signature: trimmed.substring(0, 80) };
530
720
  }
531
- // Function/method
532
721
  const funcMatch = trimmed.match(/^(?:public|protected|private)?\s*(?:static\s+)?function\s+(\w+)/);
533
722
  if (funcMatch && funcMatch[1]) {
534
723
  return { type: 'function', name: funcMatch[1], signature: trimmed.substring(0, 80) };
535
724
  }
536
- // PHP 8 Attribute
537
725
  const attrMatch = trimmed.match(/^#\[(\w+)/);
538
726
  if (attrMatch && attrMatch[1]) {
539
727
  return { type: 'decorator', name: attrMatch[1], signature: trimmed };
540
728
  }
541
729
  }
542
- // Default to block
543
730
  return { type: 'block' };
544
731
  }
545
732
  /**
546
- * Get the manifest store (for external access)
733
+ * Get the manifest store
547
734
  */
548
735
  getManifestStore() {
549
736
  return this.manifestStore;
550
737
  }
738
+ /**
739
+ * Destroy the worker pool
740
+ */
741
+ async destroy() {
742
+ if (this.pool) {
743
+ await this.pool.destroy();
744
+ this.pool = null;
745
+ }
746
+ }
551
747
  }
552
748
  /**
553
749
  * Create a scanner service