@sun-asterisk/sunlint 1.3.34 → 1.3.35

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 (90) hide show
  1. package/core/architecture-integration.js +16 -7
  2. package/core/auto-performance-manager.js +1 -1
  3. package/core/cli-action-handler.js +92 -2
  4. package/core/cli-program.js +96 -138
  5. package/core/file-targeting-service.js +62 -4
  6. package/core/git-utils.js +19 -12
  7. package/core/github-annotate-service.js +326 -11
  8. package/core/html-report-generator.js +326 -731
  9. package/core/impact-integration.js +433 -0
  10. package/core/output-service.js +293 -21
  11. package/core/scoring-service.js +3 -2
  12. package/engines/arch-detect/core/analyzer.js +413 -0
  13. package/engines/arch-detect/core/index.js +22 -0
  14. package/engines/arch-detect/engine/hybrid-detector.js +176 -0
  15. package/engines/arch-detect/engine/index.js +24 -0
  16. package/engines/arch-detect/engine/rule-executor.js +228 -0
  17. package/engines/arch-detect/engine/score-calculator.js +214 -0
  18. package/engines/arch-detect/engine/violation-detector.js +616 -0
  19. package/engines/arch-detect/index.js +50 -0
  20. package/engines/arch-detect/rules/base-rule.js +187 -0
  21. package/engines/arch-detect/rules/index.js +35 -0
  22. package/engines/arch-detect/rules/layered/index.js +28 -0
  23. package/engines/arch-detect/rules/layered/l001-presentation-layer.js +237 -0
  24. package/engines/arch-detect/rules/layered/l002-business-layer.js +215 -0
  25. package/engines/arch-detect/rules/layered/l003-data-layer.js +229 -0
  26. package/engines/arch-detect/rules/layered/l004-model-layer.js +204 -0
  27. package/engines/arch-detect/rules/layered/l005-layer-separation.js +215 -0
  28. package/engines/arch-detect/rules/layered/l006-dependency-direction.js +221 -0
  29. package/engines/arch-detect/rules/layered/layered-rules-collection.js +445 -0
  30. package/engines/arch-detect/rules/modular/index.js +27 -0
  31. package/engines/arch-detect/rules/modular/m001-feature-modules.js +238 -0
  32. package/engines/arch-detect/rules/modular/m002-core-module.js +169 -0
  33. package/engines/arch-detect/rules/modular/m003-module-declaration.js +186 -0
  34. package/engines/arch-detect/rules/modular/m004-public-api.js +171 -0
  35. package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +220 -0
  36. package/engines/arch-detect/rules/modular/modular-rules-collection.js +357 -0
  37. package/engines/arch-detect/rules/presentation/index.js +27 -0
  38. package/engines/arch-detect/rules/presentation/pr001-view-layer.js +221 -0
  39. package/engines/arch-detect/rules/presentation/pr002-presentation-logic.js +192 -0
  40. package/engines/arch-detect/rules/presentation/pr004-data-binding.js +187 -0
  41. package/engines/arch-detect/rules/presentation/pr006-router-layer.js +185 -0
  42. package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +181 -0
  43. package/engines/arch-detect/rules/presentation/presentation-rules-collection.js +507 -0
  44. package/engines/arch-detect/rules/project-scanner/index.js +31 -0
  45. package/engines/arch-detect/rules/project-scanner/ps001-project-root.js +213 -0
  46. package/engines/arch-detect/rules/project-scanner/ps002-language-detection.js +192 -0
  47. package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +339 -0
  48. package/engines/arch-detect/rules/project-scanner/ps004-build-system.js +171 -0
  49. package/engines/arch-detect/rules/project-scanner/ps005-source-directory.js +163 -0
  50. package/engines/arch-detect/rules/project-scanner/ps006-test-directory.js +184 -0
  51. package/engines/arch-detect/rules/project-scanner/ps007-documentation.js +149 -0
  52. package/engines/arch-detect/rules/project-scanner/ps008-cicd-detection.js +163 -0
  53. package/engines/arch-detect/rules/project-scanner/ps009-code-quality.js +152 -0
  54. package/engines/arch-detect/rules/project-scanner/ps010-statistics.js +180 -0
  55. package/engines/arch-detect/rules/rule-registry.js +111 -0
  56. package/engines/arch-detect/types/context.types.js +60 -0
  57. package/engines/arch-detect/types/enums.js +161 -0
  58. package/engines/arch-detect/types/index.js +25 -0
  59. package/engines/arch-detect/types/result.types.js +7 -0
  60. package/engines/arch-detect/types/rule.types.js +7 -0
  61. package/engines/arch-detect/utils/file-scanner.js +411 -0
  62. package/engines/arch-detect/utils/index.js +23 -0
  63. package/engines/arch-detect/utils/pattern-matcher.js +328 -0
  64. package/engines/impact/cli.js +106 -0
  65. package/engines/impact/config/default-config.js +54 -0
  66. package/engines/impact/core/change-detector.js +258 -0
  67. package/engines/impact/core/detectors/database-detector.js +1317 -0
  68. package/engines/impact/core/detectors/endpoint-detector.js +55 -0
  69. package/engines/impact/core/impact-analyzer.js +124 -0
  70. package/engines/impact/core/report-generator.js +462 -0
  71. package/engines/impact/core/utils/ast-parser.js +241 -0
  72. package/engines/impact/core/utils/dependency-graph.js +159 -0
  73. package/engines/impact/core/utils/file-utils.js +116 -0
  74. package/engines/impact/core/utils/git-utils.js +203 -0
  75. package/engines/impact/core/utils/logger.js +13 -0
  76. package/engines/impact/core/utils/method-call-graph.js +1192 -0
  77. package/engines/impact/index.js +135 -0
  78. package/engines/impact/package.json +29 -0
  79. package/package.json +18 -43
  80. package/scripts/build-release.sh +0 -0
  81. package/scripts/copy-impact-analyzer.js +135 -0
  82. package/scripts/install.sh +0 -0
  83. package/scripts/manual-release.sh +0 -0
  84. package/scripts/pre-release-test.sh +0 -0
  85. package/scripts/prepare-release.sh +0 -0
  86. package/scripts/quick-performance-test.js +0 -0
  87. package/scripts/setup-github-registry.sh +0 -0
  88. package/scripts/trigger-release.sh +0 -0
  89. package/scripts/verify-install.sh +0 -0
  90. package/templates/combined-report.html +1418 -0
@@ -0,0 +1,433 @@
1
+ /**
2
+ * Impact Integration for SunLint
3
+ * Wraps impact-analyzer module for seamless integration
4
+ * Following Rule C005: Single responsibility - handle impact analysis integration
5
+ */
6
+
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+ const chalk = require('chalk');
10
+ const { execSync } = require('child_process');
11
+
12
+ class ImpactIntegration {
13
+ constructor(options = {}) {
14
+ this.options = options;
15
+ this.impactModule = null;
16
+ }
17
+
18
+ /**
19
+ * Load impact analyzer module
20
+ * Note: impact-analyzer is ESM, so we use dynamic import
21
+ */
22
+ async loadImpactModule() {
23
+ if (this.impactModule) {
24
+ return this.impactModule;
25
+ }
26
+
27
+ // Priority 1: Try bundled version (engines/impact)
28
+ const bundledPath = path.join(__dirname, '..', 'engines', 'impact');
29
+ if (fs.existsSync(path.join(bundledPath, 'index.js'))) {
30
+ try {
31
+ // For ESM modules, we need to use dynamic import
32
+ const modulePath = path.join(bundledPath, 'core', 'impact-analyzer.js');
33
+ if (fs.existsSync(modulePath)) {
34
+ this.impactModule = {
35
+ bundledPath,
36
+ type: 'bundled'
37
+ };
38
+ if (this.options.verbose) {
39
+ console.log(chalk.gray('📦 Found bundled impact-analyzer'));
40
+ }
41
+ return this.impactModule;
42
+ }
43
+ } catch (error) {
44
+ if (this.options.verbose) {
45
+ console.log(chalk.yellow(`⚠️ Failed to load bundled: ${error.message}`));
46
+ }
47
+ }
48
+ }
49
+
50
+ // Priority 2: Try local development path
51
+ const devPaths = [
52
+ path.join(__dirname, '..', '..', '..', '..', 'impact-analyzer'),
53
+ path.join(__dirname, '..', '..', '..', 'impact-analyzer'),
54
+ ];
55
+
56
+ for (const devPath of devPaths) {
57
+ if (fs.existsSync(path.join(devPath, 'index.js'))) {
58
+ this.impactModule = {
59
+ bundledPath: devPath,
60
+ type: 'development'
61
+ };
62
+ if (this.options.verbose) {
63
+ console.log(chalk.gray(`📦 Found impact-analyzer at: ${devPath}`));
64
+ }
65
+ return this.impactModule;
66
+ }
67
+ }
68
+
69
+ throw new Error(
70
+ 'Impact analyzer module not found. Run "npm run build" in sunlint directory.'
71
+ );
72
+ }
73
+
74
+ /**
75
+ * Check if git repository
76
+ */
77
+ isGitRepo(projectPath) {
78
+ try {
79
+ execSync('git rev-parse --git-dir', {
80
+ cwd: projectPath,
81
+ stdio: 'pipe'
82
+ });
83
+ return true;
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Check if git ref exists
91
+ */
92
+ refExists(ref, projectPath) {
93
+ try {
94
+ execSync(`git rev-parse ${ref}`, {
95
+ cwd: projectPath,
96
+ stdio: 'pipe'
97
+ });
98
+ return true;
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Get changed files from git
106
+ */
107
+ getChangedFiles(baseRef, projectPath) {
108
+ try {
109
+ const result = execSync(`git diff --name-only ${baseRef}`, {
110
+ cwd: projectPath,
111
+ encoding: 'utf-8'
112
+ });
113
+ return result.trim().split('\n').filter(Boolean);
114
+ } catch (error) {
115
+ throw new Error(`Failed to get changed files: ${error.message}`);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Run impact analysis on project
121
+ * @param {string} projectPath - Path to analyze
122
+ * @returns {Object} Impact analysis results
123
+ */
124
+ async analyze(projectPath) {
125
+ await this.loadImpactModule();
126
+
127
+ const absolutePath = path.resolve(projectPath);
128
+ const baseRef = this.options.impactBase || 'HEAD~1';
129
+
130
+ // Validation
131
+ if (!fs.existsSync(absolutePath)) {
132
+ throw new Error(`Directory does not exist: ${absolutePath}`);
133
+ }
134
+
135
+ if (!this.isGitRepo(absolutePath)) {
136
+ throw new Error(`Not a git repository: ${absolutePath}`);
137
+ }
138
+
139
+ if (!this.refExists(baseRef, absolutePath)) {
140
+ throw new Error(`Git ref does not exist: ${baseRef}`);
141
+ }
142
+
143
+ if (this.options.verbose) {
144
+ console.log(chalk.blue(`\n🔍 Impact Analysis`));
145
+ console.log(chalk.gray(` Path: ${absolutePath}`));
146
+ console.log(chalk.gray(` Base: ${baseRef}`));
147
+ }
148
+
149
+ // Get changed files
150
+ const changedFiles = this.getChangedFiles(baseRef, absolutePath);
151
+
152
+ if (changedFiles.length === 0) {
153
+ return this.createEmptyResult(baseRef);
154
+ }
155
+
156
+ // Categorize changes
157
+ const categorizedChanges = this.categorizeChanges(changedFiles, absolutePath);
158
+
159
+ // Calculate impact score
160
+ const impactScore = this.calculateImpactScore(categorizedChanges);
161
+ const severity = this.getSeverity(impactScore);
162
+
163
+ // Build result
164
+ const result = {
165
+ summary: {
166
+ baseRef,
167
+ totalChanges: changedFiles.length,
168
+ impactScore,
169
+ severity,
170
+ categories: {
171
+ api: categorizedChanges.api.length,
172
+ database: categorizedChanges.database.length,
173
+ core: categorizedChanges.core.length,
174
+ ui: categorizedChanges.ui.length,
175
+ tests: categorizedChanges.tests.length,
176
+ config: categorizedChanges.config.length,
177
+ other: categorizedChanges.other.length,
178
+ }
179
+ },
180
+ changes: categorizedChanges,
181
+ changedFiles,
182
+ violations: this.generateViolations(categorizedChanges, impactScore),
183
+ };
184
+
185
+ // Generate markdown report if requested
186
+ if (this.options.impactReport) {
187
+ result.markdownReport = this.generateMarkdownReport(result);
188
+ }
189
+
190
+ return result;
191
+ }
192
+
193
+ /**
194
+ * Categorize changed files by type
195
+ */
196
+ categorizeChanges(files, basePath) {
197
+ const categories = {
198
+ api: [],
199
+ database: [],
200
+ core: [],
201
+ ui: [],
202
+ tests: [],
203
+ config: [],
204
+ other: [],
205
+ };
206
+
207
+ const patterns = {
208
+ api: [/controller/i, /route/i, /endpoint/i, /api\//i, /handler/i],
209
+ database: [/repository/i, /model/i, /schema/i, /migration/i, /entity/i, /dao/i],
210
+ core: [/service/i, /usecase/i, /domain/i, /core\//i, /lib\//i],
211
+ ui: [/component/i, /view/i, /page/i, /screen/i, /widget/i, /\.tsx$/i, /\.vue$/i],
212
+ tests: [/test/i, /spec/i, /__tests__/i],
213
+ config: [/config/i, /\.json$/i, /\.ya?ml$/i, /\.env/i, /dockerfile/i],
214
+ };
215
+
216
+ for (const file of files) {
217
+ let categorized = false;
218
+
219
+ for (const [category, regexes] of Object.entries(patterns)) {
220
+ if (regexes.some(regex => regex.test(file))) {
221
+ categories[category].push(file);
222
+ categorized = true;
223
+ break;
224
+ }
225
+ }
226
+
227
+ if (!categorized) {
228
+ categories.other.push(file);
229
+ }
230
+ }
231
+
232
+ return categories;
233
+ }
234
+
235
+ /**
236
+ * Calculate impact score based on changes
237
+ */
238
+ calculateImpactScore(categories) {
239
+ const weights = {
240
+ api: 30,
241
+ database: 35,
242
+ core: 25,
243
+ ui: 10,
244
+ tests: 5,
245
+ config: 15,
246
+ other: 5,
247
+ };
248
+
249
+ let score = 0;
250
+ let maxScore = 0;
251
+
252
+ for (const [category, files] of Object.entries(categories)) {
253
+ const weight = weights[category] || 5;
254
+ const fileCount = files.length;
255
+
256
+ // Diminishing returns for many files in same category
257
+ const categoryScore = Math.min(fileCount * weight, weight * 3);
258
+ score += categoryScore;
259
+ maxScore += weight * 3;
260
+ }
261
+
262
+ // Normalize to 0-100
263
+ return Math.min(Math.round((score / maxScore) * 100), 100);
264
+ }
265
+
266
+ /**
267
+ * Get severity based on impact score
268
+ */
269
+ getSeverity(score) {
270
+ if (score >= 70) return 'critical';
271
+ if (score >= 50) return 'high';
272
+ if (score >= 30) return 'medium';
273
+ return 'low';
274
+ }
275
+
276
+ /**
277
+ * Generate violations for SunLint format
278
+ */
279
+ generateViolations(categories, impactScore) {
280
+ const violations = [];
281
+
282
+ // Add violations for high-impact changes
283
+ if (categories.api.length > 0) {
284
+ violations.push({
285
+ ruleId: 'IMPACT-API',
286
+ severity: categories.api.length > 3 ? 'error' : 'warning',
287
+ message: `API changes detected: ${categories.api.length} file(s) modified`,
288
+ file: categories.api[0],
289
+ line: 1,
290
+ column: 1,
291
+ category: 'impact',
292
+ source: 'impact-analyzer',
293
+ details: {
294
+ files: categories.api,
295
+ recommendation: 'Review API contracts and update documentation',
296
+ },
297
+ });
298
+ }
299
+
300
+ if (categories.database.length > 0) {
301
+ violations.push({
302
+ ruleId: 'IMPACT-DATABASE',
303
+ severity: categories.database.length > 2 ? 'error' : 'warning',
304
+ message: `Database changes detected: ${categories.database.length} file(s) modified`,
305
+ file: categories.database[0],
306
+ line: 1,
307
+ column: 1,
308
+ category: 'impact',
309
+ source: 'impact-analyzer',
310
+ details: {
311
+ files: categories.database,
312
+ recommendation: 'Verify migrations and backup strategy',
313
+ },
314
+ });
315
+ }
316
+
317
+ if (categories.core.length > 5) {
318
+ violations.push({
319
+ ruleId: 'IMPACT-CORE',
320
+ severity: 'warning',
321
+ message: `Large core logic changes: ${categories.core.length} file(s) modified`,
322
+ file: categories.core[0],
323
+ line: 1,
324
+ column: 1,
325
+ category: 'impact',
326
+ source: 'impact-analyzer',
327
+ details: {
328
+ files: categories.core,
329
+ recommendation: 'Ensure adequate test coverage for core changes',
330
+ },
331
+ });
332
+ }
333
+
334
+ return violations;
335
+ }
336
+
337
+ /**
338
+ * Create empty result when no changes
339
+ */
340
+ createEmptyResult(baseRef) {
341
+ return {
342
+ summary: {
343
+ baseRef,
344
+ totalChanges: 0,
345
+ impactScore: 0,
346
+ severity: 'none',
347
+ categories: {
348
+ api: 0,
349
+ database: 0,
350
+ core: 0,
351
+ ui: 0,
352
+ tests: 0,
353
+ config: 0,
354
+ other: 0,
355
+ }
356
+ },
357
+ changes: {
358
+ api: [],
359
+ database: [],
360
+ core: [],
361
+ ui: [],
362
+ tests: [],
363
+ config: [],
364
+ other: [],
365
+ },
366
+ changedFiles: [],
367
+ violations: [],
368
+ };
369
+ }
370
+
371
+ /**
372
+ * Generate markdown report
373
+ */
374
+ generateMarkdownReport(result) {
375
+ const { summary, changes } = result;
376
+ const severityEmoji = {
377
+ critical: '🔴',
378
+ high: '🟠',
379
+ medium: '🟡',
380
+ low: '🟢',
381
+ none: '⚪',
382
+ };
383
+
384
+ let md = `# Impact Analysis Report\n\n`;
385
+ md += `**Base Reference:** \`${summary.baseRef}\`\n`;
386
+ md += `**Generated:** ${new Date().toISOString()}\n\n`;
387
+
388
+ md += `## Summary\n\n`;
389
+ md += `| Metric | Value |\n`;
390
+ md += `|--------|-------|\n`;
391
+ md += `| Total Changes | ${summary.totalChanges} files |\n`;
392
+ md += `| Impact Score | ${summary.impactScore}/100 |\n`;
393
+ md += `| Severity | ${severityEmoji[summary.severity]} ${summary.severity.toUpperCase()} |\n\n`;
394
+
395
+ md += `## Changes by Category\n\n`;
396
+ md += `| Category | Files |\n`;
397
+ md += `|----------|-------|\n`;
398
+
399
+ for (const [category, count] of Object.entries(summary.categories)) {
400
+ if (count > 0) {
401
+ md += `| ${category.charAt(0).toUpperCase() + category.slice(1)} | ${count} |\n`;
402
+ }
403
+ }
404
+
405
+ md += `\n## Changed Files\n\n`;
406
+
407
+ for (const [category, files] of Object.entries(changes)) {
408
+ if (files.length > 0) {
409
+ md += `### ${category.charAt(0).toUpperCase() + category.slice(1)} (${files.length})\n\n`;
410
+ for (const file of files) {
411
+ md += `- \`${file}\`\n`;
412
+ }
413
+ md += `\n`;
414
+ }
415
+ }
416
+
417
+ md += `---\n`;
418
+ md += `*Generated by SunLint Impact Analyzer*\n`;
419
+
420
+ return md;
421
+ }
422
+
423
+ /**
424
+ * Save markdown report to file
425
+ */
426
+ async saveReport(markdownContent, outputPath) {
427
+ const resolvedPath = path.resolve(outputPath || 'impact-report.md');
428
+ fs.writeFileSync(resolvedPath, markdownContent, 'utf8');
429
+ return resolvedPath;
430
+ }
431
+ }
432
+
433
+ module.exports = { ImpactIntegration };