project-graph-mcp 1.5.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +128 -8
  2. package/package.json +12 -8
  3. package/src/.project-graph-cache.json +1 -1
  4. package/src/analysis/analysis-cache.js +7 -0
  5. package/src/analysis/complexity.js +14 -0
  6. package/src/analysis/custom-rules.js +36 -0
  7. package/src/analysis/db-analysis.js +9 -0
  8. package/src/analysis/dead-code.js +19 -0
  9. package/src/analysis/full-analysis.js +18 -0
  10. package/src/analysis/jsdoc-checker.js +24 -0
  11. package/src/analysis/jsdoc-generator.js +10 -0
  12. package/src/analysis/large-files.js +11 -0
  13. package/src/analysis/outdated-patterns.js +12 -0
  14. package/src/analysis/similar-functions.js +16 -0
  15. package/src/analysis/test-annotations.js +21 -0
  16. package/src/analysis/type-checker.js +8 -0
  17. package/src/analysis/undocumented.js +14 -0
  18. package/src/cli/cli-handlers.js +4 -0
  19. package/src/cli/cli.js +5 -0
  20. package/src/compact/ai-context.js +7 -0
  21. package/src/compact/compact.js +18 -0
  22. package/src/compact/compress.js +13 -0
  23. package/src/compact/ctx-to-jsdoc.js +29 -0
  24. package/src/compact/doc-dialect.js +30 -0
  25. package/src/compact/expand.js +37 -0
  26. package/src/compact/framework-references.js +5 -0
  27. package/src/compact/instructions.js +3 -0
  28. package/src/compact/mode-config.js +8 -0
  29. package/src/compact/validate-pipeline.js +9 -0
  30. package/src/core/event-bus.js +9 -0
  31. package/src/core/filters.js +14 -0
  32. package/src/core/graph-builder.js +12 -0
  33. package/src/core/parser.js +31 -0
  34. package/src/core/workspace.js +8 -0
  35. package/src/lang/lang-go.js +17 -0
  36. package/src/lang/lang-python.js +12 -0
  37. package/src/lang/lang-sql.js +23 -0
  38. package/src/lang/lang-typescript.js +9 -0
  39. package/src/lang/lang-utils.js +4 -0
  40. package/src/mcp/mcp-server.js +17 -0
  41. package/src/mcp/tool-defs.js +3 -0
  42. package/src/mcp/tools.js +25 -0
  43. package/src/network/backend-lifecycle.js +19 -0
  44. package/src/network/backend.js +5 -0
  45. package/src/network/local-gateway.js +23 -0
  46. package/src/network/mdns.js +13 -0
  47. package/src/network/server.js +10 -0
  48. package/src/network/web-server.js +34 -0
  49. package/web/.project-graph-cache.json +1 -0
  50. package/web/app.js +16 -0
  51. package/web/components/code-block.js +3 -0
  52. package/web/components/quick-open.js +5 -0
  53. package/web/dashboard-state.js +3 -0
  54. package/web/dashboard.html +27 -0
  55. package/web/dashboard.js +8 -0
  56. package/web/highlight.js +13 -0
  57. package/web/index.html +35 -0
  58. package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
  59. package/web/panels/ActionBoard/ActionBoard.js +4 -0
  60. package/web/panels/ActionBoard/ActionBoard.tpl.js +1 -0
  61. package/web/panels/EventItem/EventItem.css.js +1 -0
  62. package/web/panels/EventItem/EventItem.js +4 -0
  63. package/web/panels/EventItem/EventItem.tpl.js +1 -0
  64. package/web/panels/ProjectItem/ProjectItem.css.js +1 -0
  65. package/web/panels/ProjectItem/ProjectItem.js +5 -0
  66. package/web/panels/ProjectItem/ProjectItem.tpl.js +1 -0
  67. package/web/panels/ProjectList/ProjectList.css.js +1 -0
  68. package/web/panels/ProjectList/ProjectList.js +4 -0
  69. package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
  70. package/web/panels/SettingsPanel/.project-graph-cache.json +1 -0
  71. package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
  72. package/web/panels/SettingsPanel/SettingsPanel.js +7 -0
  73. package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
  74. package/web/panels/code-viewer.js +5 -0
  75. package/web/panels/ctx-panel.js +4 -0
  76. package/web/panels/dep-graph.js +6 -0
  77. package/web/panels/file-tree.js +188 -0
  78. package/web/panels/health-panel.js +3 -0
  79. package/web/panels/live-monitor.js +3 -0
  80. package/web/state.js +17 -0
  81. package/web/style.css +157 -0
  82. package/references/symbiote-3x.md +0 -834
  83. package/src/ai-context.js +0 -113
  84. package/src/analysis-cache.js +0 -155
  85. package/src/cli-handlers.js +0 -271
  86. package/src/cli.js +0 -95
  87. package/src/compact.js +0 -207
  88. package/src/complexity.js +0 -237
  89. package/src/compress.js +0 -319
  90. package/src/ctx-to-jsdoc.js +0 -514
  91. package/src/custom-rules.js +0 -584
  92. package/src/db-analysis.js +0 -194
  93. package/src/dead-code.js +0 -468
  94. package/src/doc-dialect.js +0 -716
  95. package/src/filters.js +0 -227
  96. package/src/framework-references.js +0 -177
  97. package/src/full-analysis.js +0 -470
  98. package/src/graph-builder.js +0 -299
  99. package/src/instructions.js +0 -73
  100. package/src/jsdoc-checker.js +0 -351
  101. package/src/jsdoc-generator.js +0 -203
  102. package/src/lang-go.js +0 -285
  103. package/src/lang-python.js +0 -197
  104. package/src/lang-sql.js +0 -309
  105. package/src/lang-typescript.js +0 -190
  106. package/src/lang-utils.js +0 -124
  107. package/src/large-files.js +0 -163
  108. package/src/mcp-server.js +0 -675
  109. package/src/mode-config.js +0 -127
  110. package/src/outdated-patterns.js +0 -296
  111. package/src/parser.js +0 -662
  112. package/src/server.js +0 -28
  113. package/src/similar-functions.js +0 -279
  114. package/src/test-annotations.js +0 -323
  115. package/src/tool-defs.js +0 -793
  116. package/src/tools.js +0 -470
  117. package/src/type-checker.js +0 -188
  118. package/src/undocumented.js +0 -259
  119. package/src/workspace.js +0 -70
  120. /package/{AGENT_ROLE.md → docs/examples/AGENT_ROLE.md} +0 -0
  121. /package/{AGENT_ROLE_MINIMAL.md → docs/examples/AGENT_ROLE_MINIMAL.md} +0 -0
@@ -1,584 +0,0 @@
1
- /**
2
- * Custom Rules System
3
- * Configurable code analysis rules with JSON storage
4
- */
5
-
6
- import { readFileSync, writeFileSync, readdirSync, existsSync, statSync } from 'fs';
7
- import { join, relative, dirname, resolve } from 'path';
8
- import { fileURLToPath } from 'url';
9
- import { shouldExcludeDir, shouldExcludeFile, parseGitignore } from './filters.js';
10
-
11
- const __dirname = dirname(fileURLToPath(import.meta.url));
12
- const RULES_DIR = join(__dirname, '..', 'rules');
13
-
14
- /** @type {string[]} Patterns from .graphignore */
15
- let graphignorePatterns = [];
16
-
17
- /**
18
- * Parse .graphignore file - searches current and parent directories
19
- * @param {string} startDir
20
- */
21
- function parseGraphignore(startDir) {
22
- graphignorePatterns = [];
23
-
24
- // Search up the directory tree for .graphignore
25
- let dir = startDir;
26
- while (dir !== dirname(dir)) {
27
- const ignorePath = join(dir, '.graphignore');
28
- if (existsSync(ignorePath)) {
29
- try {
30
- const content = readFileSync(ignorePath, 'utf-8');
31
- graphignorePatterns = content
32
- .split('\n')
33
- .map(line => line.trim())
34
- .filter(line => line && !line.startsWith('#'));
35
- return;
36
- } catch (e) { }
37
- }
38
- dir = dirname(dir);
39
- }
40
- }
41
-
42
- /**
43
- * Check if file matches .graphignore patterns
44
- * @param {string} relativePath
45
- * @returns {boolean}
46
- */
47
- function isGraphignored(relativePath) {
48
- const basename = relativePath.split('/').pop();
49
-
50
- for (const pattern of graphignorePatterns) {
51
- // Simple glob matching
52
- if (pattern.endsWith('*')) {
53
- const prefix = pattern.slice(0, -1);
54
- if (relativePath.startsWith(prefix) || basename.startsWith(prefix)) return true;
55
- } else if (pattern.startsWith('*')) {
56
- const suffix = pattern.slice(1);
57
- if (relativePath.endsWith(suffix) || basename.endsWith(suffix)) return true;
58
- } else {
59
- // Exact match on path or basename
60
- if (relativePath.includes(pattern) || basename === pattern) return true;
61
- }
62
- }
63
- return false;
64
- }
65
-
66
- /**
67
- * @typedef {Object} Rule
68
- * @property {string} id
69
- * @property {string} name
70
- * @property {string} description
71
- * @property {string} pattern - String or regex pattern to search
72
- * @property {string} patternType - 'string' | 'regex'
73
- * @property {string} replacement - Suggested fix
74
- * @property {string} severity - 'error' | 'warning' | 'info'
75
- * @property {string} filePattern - Glob pattern for files
76
- * @property {string[]} [exclude] - Patterns to exclude
77
- * @property {string} [contextRequired] - HTML tag context required (e.g. '<template>')
78
- */
79
-
80
- /**
81
- * @typedef {Object} RuleSet
82
- * @property {string} name
83
- * @property {string} description
84
- * @property {Rule[]} rules
85
- */
86
-
87
- /**
88
- * @typedef {Object} Violation
89
- * @property {string} ruleId
90
- * @property {string} ruleName
91
- * @property {string} severity
92
- * @property {string} file
93
- * @property {number} line
94
- * @property {string} match
95
- * @property {string} replacement
96
- */
97
-
98
- /**
99
- * Load rule sets from rules directory
100
- * @returns {Object<string, RuleSet>}
101
- */
102
- function loadRuleSets() {
103
- const ruleSets = {};
104
-
105
- if (!existsSync(RULES_DIR)) return ruleSets;
106
-
107
- for (const file of readdirSync(RULES_DIR)) {
108
- if (!file.endsWith('.json')) continue;
109
- try {
110
- const content = readFileSync(join(RULES_DIR, file), 'utf-8');
111
- const ruleSet = JSON.parse(content);
112
- ruleSets[ruleSet.name] = ruleSet;
113
- } catch (e) { }
114
- }
115
-
116
- return ruleSets;
117
- }
118
-
119
- /**
120
- * Save rule set to file
121
- * @param {RuleSet} ruleSet
122
- */
123
- function saveRuleSet(ruleSet) {
124
- const filePath = join(RULES_DIR, `${ruleSet.name}.json`);
125
- writeFileSync(filePath, JSON.stringify(ruleSet, null, 2));
126
- }
127
-
128
- /**
129
- * Find all files matching pattern
130
- * @param {string} dir
131
- * @param {string} filePattern
132
- * @param {string} rootDir
133
- * @returns {string[]}
134
- */
135
- function findFiles(dir, filePattern, rootDir = dir) {
136
- if (dir === rootDir) {
137
- parseGitignore(rootDir);
138
- parseGraphignore(rootDir);
139
- }
140
- const files = [];
141
- const ext = filePattern.replace('*', '');
142
-
143
- try {
144
- for (const entry of readdirSync(dir)) {
145
- const fullPath = join(dir, entry);
146
- const relativePath = relative(rootDir, fullPath);
147
- const stat = statSync(fullPath);
148
-
149
- if (stat.isDirectory()) {
150
- if (!shouldExcludeDir(entry, relativePath)) {
151
- files.push(...findFiles(fullPath, filePattern, rootDir));
152
- }
153
- } else if (entry.endsWith(ext)) {
154
- if (!shouldExcludeFile(entry, relativePath) && !isGraphignored(relativePath)) {
155
- files.push(fullPath);
156
- }
157
- }
158
- }
159
- } catch (e) { }
160
-
161
- return files;
162
- }
163
-
164
- /**
165
- * Check if file matches exclude patterns
166
- * @param {string} filePath
167
- * @param {string[]} excludePatterns
168
- * @returns {boolean}
169
- */
170
- function isExcluded(filePath, excludePatterns = []) {
171
- for (const pattern of excludePatterns) {
172
- const ext = pattern.replace('*', '');
173
- if (filePath.endsWith(ext)) return true;
174
- }
175
- return false;
176
- }
177
-
178
- /**
179
- * Check if a position in a line is inside a string or comment
180
- * @param {string} line - The line of code
181
- * @param {number} matchIndex - Position of the match
182
- * @returns {boolean}
183
- */
184
- function isInStringOrComment(line, matchIndex) {
185
- // Check if in single-line comment
186
- const commentIndex = line.indexOf('//');
187
- if (commentIndex !== -1 && matchIndex > commentIndex) {
188
- return true;
189
- }
190
-
191
- // Check if in string literal (simplified - handles most cases)
192
- let inString = false;
193
- let stringChar = null;
194
-
195
- for (let i = 0; i < matchIndex; i++) {
196
- const char = line[i];
197
- const prevChar = i > 0 ? line[i - 1] : '';
198
-
199
- if (!inString && (char === '"' || char === "'" || char === '`')) {
200
- inString = true;
201
- stringChar = char;
202
- } else if (inString && char === stringChar && prevChar !== '\\') {
203
- inString = false;
204
- stringChar = null;
205
- }
206
- }
207
-
208
- return inString;
209
- }
210
-
211
- /**
212
- * Check if a line index is within an HTML context block (e.g. <template>...</template>)
213
- * @param {string[]} lines - All file lines
214
- * @param {number} lineIndex - Current line index
215
- * @param {string} contextTag - Tag to check (e.g. '<template>')
216
- * @returns {boolean}
217
- */
218
- function isWithinContext(lines, lineIndex, contextTag) {
219
- const openTag = contextTag;
220
- const tagName = openTag.replace(/[<>]/g, '');
221
- const closeTag = `</${tagName}>`;
222
- let depth = 0;
223
-
224
- for (let i = 0; i <= lineIndex; i++) {
225
- const line = lines[i];
226
- // Count all opens/closes on this line
227
- let pos = 0;
228
- while (pos < line.length) {
229
- const openIdx = line.indexOf(openTag, pos);
230
- const closeIdx = line.indexOf(closeTag, pos);
231
-
232
- if (openIdx === -1 && closeIdx === -1) break;
233
-
234
- if (openIdx !== -1 && (closeIdx === -1 || openIdx < closeIdx)) {
235
- depth++;
236
- pos = openIdx + openTag.length;
237
- } else {
238
- depth--;
239
- pos = closeIdx + closeTag.length;
240
- }
241
- }
242
- }
243
-
244
- return depth > 0;
245
- }
246
-
247
- /**
248
- * Check file against rule
249
- * @param {string} filePath
250
- * @param {Rule} rule
251
- * @param {string} rootDir - Root directory for relative path calculation
252
- * @returns {Violation[]}
253
- */
254
- function checkFileAgainstRule(filePath, rule, rootDir) {
255
- if (isExcluded(filePath, rule.exclude)) return [];
256
-
257
- const violations = [];
258
- const content = readFileSync(filePath, 'utf-8');
259
- const lines = content.split('\n');
260
- const relPath = relative(rootDir, filePath);
261
-
262
- for (let i = 0; i < lines.length; i++) {
263
- const line = lines[i];
264
- let matches = false;
265
- let matchText = '';
266
-
267
- if (rule.patternType === 'regex') {
268
- try {
269
- const regex = new RegExp(rule.pattern, 'g');
270
- let match;
271
- while ((match = regex.exec(line)) !== null) {
272
- if (!isInStringOrComment(line, match.index)) {
273
- matches = true;
274
- matchText = match[0];
275
- break;
276
- }
277
- }
278
- } catch (e) { }
279
- } else {
280
- const matchIndex = line.indexOf(rule.pattern);
281
- if (matchIndex !== -1 && !isInStringOrComment(line, matchIndex)) {
282
- matches = true;
283
- matchText = rule.pattern;
284
- }
285
- }
286
-
287
- // Skip if context required but not within that context
288
- if (matches && rule.contextRequired) {
289
- if (!isWithinContext(lines, i, rule.contextRequired)) {
290
- continue;
291
- }
292
- }
293
-
294
- if (matches) {
295
- violations.push({
296
- ruleId: rule.id,
297
- ruleName: rule.name,
298
- severity: rule.severity,
299
- file: relPath,
300
- line: i + 1,
301
- match: matchText,
302
- replacement: rule.replacement,
303
- });
304
- }
305
- }
306
-
307
- return violations;
308
- }
309
-
310
- /**
311
- * Get all available custom rules
312
- * @returns {Promise<{ruleSets: Object, totalRules: number}>}
313
- */
314
- export async function getCustomRules() {
315
- const ruleSets = loadRuleSets();
316
- let totalRules = 0;
317
-
318
- const summary = {};
319
- for (const [name, ruleSet] of Object.entries(ruleSets)) {
320
- summary[name] = {
321
- description: ruleSet.description,
322
- ruleCount: ruleSet.rules.length,
323
- rules: ruleSet.rules.map(r => ({
324
- id: r.id,
325
- name: r.name,
326
- severity: r.severity,
327
- })),
328
- };
329
- totalRules += ruleSet.rules.length;
330
- }
331
-
332
- return { ruleSets: summary, totalRules };
333
- }
334
-
335
- /**
336
- * Add or update a custom rule
337
- * @param {string} ruleSetName
338
- * @param {Rule} rule
339
- * @returns {Promise<{success: boolean, message: string}>}
340
- */
341
- export async function setCustomRule(ruleSetName, rule) {
342
- const ruleSets = loadRuleSets();
343
-
344
- // Create new ruleset if doesn't exist
345
- if (!ruleSets[ruleSetName]) {
346
- ruleSets[ruleSetName] = {
347
- name: ruleSetName,
348
- description: `Custom rules for ${ruleSetName}`,
349
- rules: [],
350
- };
351
- }
352
-
353
- const ruleSet = ruleSets[ruleSetName];
354
- const existingIndex = ruleSet.rules.findIndex(r => r.id === rule.id);
355
-
356
- if (existingIndex >= 0) {
357
- ruleSet.rules[existingIndex] = rule;
358
- } else {
359
- ruleSet.rules.push(rule);
360
- }
361
-
362
- saveRuleSet(ruleSet);
363
-
364
- return {
365
- success: true,
366
- message: existingIndex >= 0
367
- ? `Updated rule "${rule.id}" in ${ruleSetName}`
368
- : `Added rule "${rule.id}" to ${ruleSetName}`,
369
- };
370
- }
371
-
372
- /**
373
- * Delete a custom rule
374
- * @param {string} ruleSetName
375
- * @param {string} ruleId
376
- * @returns {Promise<{success: boolean, message: string}>}
377
- */
378
- export async function deleteCustomRule(ruleSetName, ruleId) {
379
- const ruleSets = loadRuleSets();
380
-
381
- if (!ruleSets[ruleSetName]) {
382
- return { success: false, message: `Ruleset "${ruleSetName}" not found` };
383
- }
384
-
385
- const ruleSet = ruleSets[ruleSetName];
386
- const index = ruleSet.rules.findIndex(r => r.id === ruleId);
387
-
388
- if (index < 0) {
389
- return { success: false, message: `Rule "${ruleId}" not found` };
390
- }
391
-
392
- ruleSet.rules.splice(index, 1);
393
- saveRuleSet(ruleSet);
394
-
395
- return { success: true, message: `Deleted rule "${ruleId}" from ${ruleSetName}` };
396
- }
397
-
398
- /**
399
- * Detect which rulesets apply to a project
400
- * @param {string} dir
401
- * @returns {{detected: string[], reasons: Object<string, string>}}
402
- */
403
- export function detectProjectRuleSets(dir) {
404
- const ruleSets = loadRuleSets();
405
- const detected = [];
406
- const reasons = {};
407
-
408
- // Check package.json
409
- let packageDeps = [];
410
- try {
411
- const pkgPath = join(dir, 'package.json');
412
- if (existsSync(pkgPath)) {
413
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
414
- packageDeps = [
415
- ...Object.keys(pkg.dependencies || {}),
416
- ...Object.keys(pkg.devDependencies || {}),
417
- ];
418
- }
419
- } catch (e) { }
420
-
421
- for (const [name, ruleSet] of Object.entries(ruleSets)) {
422
- if (!ruleSet.detect) continue;
423
- const detect = ruleSet.detect;
424
-
425
- // Check packageJson deps
426
- if (detect.packageJson) {
427
- for (const dep of detect.packageJson) {
428
- if (packageDeps.includes(dep)) {
429
- detected.push(name);
430
- reasons[name] = `Found "${dep}" in package.json`;
431
- break;
432
- }
433
- }
434
- }
435
-
436
- // Skip further checks if already detected
437
- if (detected.includes(name)) continue;
438
-
439
- // Check for import patterns in source files
440
- if (detect.imports || detect.patterns) {
441
- const jsFiles = findFiles(dir, '*.js');
442
-
443
- fileLoop:
444
- for (const file of jsFiles.slice(0, 50)) { // Limit for performance
445
- try {
446
- const content = readFileSync(file, 'utf-8');
447
-
448
- if (detect.imports) {
449
- for (const pattern of detect.imports) {
450
- if (content.includes(pattern)) {
451
- detected.push(name);
452
- reasons[name] = `Found "${pattern}" in ${relative(dir, file)}`;
453
- break fileLoop;
454
- }
455
- }
456
- }
457
-
458
- if (detect.patterns) {
459
- for (const pattern of detect.patterns) {
460
- if (content.includes(pattern)) {
461
- detected.push(name);
462
- reasons[name] = `Found "${pattern}" in ${relative(dir, file)}`;
463
- break fileLoop;
464
- }
465
- }
466
- }
467
- } catch (e) { }
468
- }
469
- }
470
- }
471
-
472
- return { detected, reasons };
473
- }
474
-
475
- /**
476
- * Check directory against custom rules
477
- * @param {string} dir
478
- * @param {Object} [options]
479
- * @param {string} [options.ruleSet] - Specific ruleset to use
480
- * @param {string} [options.severity] - Filter by severity
481
- * @param {boolean} [options.autoDetect] - Auto-detect applicable rulesets
482
- * @returns {Promise<{total: number, bySeverity: Object, byRule: Object, violations: Violation[], detected?: Object}>}
483
- */
484
- export async function checkCustomRules(dir, options = {}) {
485
- const resolvedDir = resolve(dir);
486
- const ruleSets = loadRuleSets();
487
- let allRules = [];
488
- let detectionResult = null;
489
-
490
- // Collect rules
491
- if (options.ruleSet) {
492
- if (ruleSets[options.ruleSet]) {
493
- allRules = ruleSets[options.ruleSet].rules;
494
- }
495
- } else if (options.autoDetect !== false) {
496
- // Auto-detect by default
497
- detectionResult = detectProjectRuleSets(dir);
498
-
499
- if (detectionResult.detected.length > 0) {
500
- for (const name of detectionResult.detected) {
501
- if (ruleSets[name]) {
502
- allRules.push(...ruleSets[name].rules);
503
- }
504
- }
505
- }
506
-
507
- // Always add universal rulesets (alwaysApply: true)
508
- for (const [name, rs] of Object.entries(ruleSets)) {
509
- if (rs.alwaysApply && !detectionResult.detected.includes(name)) {
510
- allRules.push(...rs.rules);
511
- }
512
- }
513
- } else {
514
- for (const ruleSet of Object.values(ruleSets)) {
515
- allRules.push(...ruleSet.rules);
516
- }
517
- }
518
-
519
- // Group rules by file pattern
520
- const rulesByPattern = {};
521
- for (const rule of allRules) {
522
- const pattern = rule.filePattern || '*.js';
523
- if (!rulesByPattern[pattern]) rulesByPattern[pattern] = [];
524
- rulesByPattern[pattern].push(rule);
525
- }
526
-
527
- // Find and check files
528
- const allViolations = [];
529
-
530
- for (const [pattern, rules] of Object.entries(rulesByPattern)) {
531
- const files = findFiles(dir, pattern);
532
-
533
- for (const file of files) {
534
- for (const rule of rules) {
535
- const violations = checkFileAgainstRule(file, rule, resolvedDir);
536
- allViolations.push(...violations);
537
- }
538
- }
539
- }
540
-
541
- // Deduplicate violations across rulesets (same file:line:match)
542
- const seen = new Set();
543
- const deduped = allViolations.filter(v => {
544
- const key = `${v.file}:${v.line}:${v.match}`;
545
- if (seen.has(key)) return false;
546
- seen.add(key);
547
- return true;
548
- });
549
-
550
- // Filter by severity if specified
551
- let filtered = deduped;
552
- if (options.severity) {
553
- filtered = deduped.filter(v => v.severity === options.severity);
554
- }
555
-
556
- // Sort by severity, then file
557
- const severityOrder = { error: 0, warning: 1, info: 2 };
558
- filtered.sort((a, b) => {
559
- const sevDiff = severityOrder[a.severity] - severityOrder[b.severity];
560
- if (sevDiff !== 0) return sevDiff;
561
- return a.file.localeCompare(b.file);
562
- });
563
-
564
- // Calculate stats
565
- const bySeverity = {
566
- error: filtered.filter(v => v.severity === 'error').length,
567
- warning: filtered.filter(v => v.severity === 'warning').length,
568
- info: filtered.filter(v => v.severity === 'info').length,
569
- };
570
-
571
- const byRule = {};
572
- for (const v of filtered) {
573
- byRule[v.ruleId] = (byRule[v.ruleId] || 0) + 1;
574
- }
575
-
576
- return {
577
- basePath: dir,
578
- total: filtered.length,
579
- bySeverity,
580
- byRule,
581
- violations: filtered.slice(0, 50),
582
- ...(detectionResult && { detected: detectionResult }),
583
- };
584
- }