@sun-asterisk/sunlint 1.3.47 → 1.3.48

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 (34) hide show
  1. package/config/rules/rules-registry-generated.json +1717 -282
  2. package/core/architecture-integration.js +57 -15
  3. package/core/cli-action-handler.js +51 -36
  4. package/core/config-manager.js +6 -0
  5. package/core/config-merger.js +33 -0
  6. package/core/config-validator.js +37 -2
  7. package/core/output-service.js +12 -3
  8. package/core/scoring-service.js +12 -6
  9. package/core/summary-report-service.js +9 -4
  10. package/engines/impact/cli.js +54 -39
  11. package/engines/impact/config/default-config.js +105 -5
  12. package/engines/impact/core/impact-analyzer.js +12 -15
  13. package/engines/impact/core/utils/gitignore-parser.js +123 -0
  14. package/engines/impact/core/utils/method-call-graph.js +272 -87
  15. package/origin-rules/dart-en.md +1 -1
  16. package/origin-rules/go-en.md +231 -0
  17. package/origin-rules/php-en.md +107 -0
  18. package/origin-rules/python-en.md +113 -0
  19. package/origin-rules/ruby-en.md +607 -0
  20. package/package.json +1 -1
  21. package/scripts/copy-arch-detect.js +5 -1
  22. package/scripts/copy-impact-analyzer.js +5 -1
  23. package/scripts/generate-rules-registry.js +30 -14
  24. package/skill-assets/sunlint-code-quality/SKILL.md +3 -2
  25. package/skill-assets/sunlint-code-quality/rules/go/G001-explicit-error-handling.md +53 -0
  26. package/skill-assets/sunlint-code-quality/rules/go/G002-context-first-argument.md +44 -0
  27. package/skill-assets/sunlint-code-quality/rules/go/G003-receiver-consistency.md +38 -0
  28. package/skill-assets/sunlint-code-quality/rules/go/G004-avoid-panic.md +49 -0
  29. package/skill-assets/sunlint-code-quality/rules/go/G005-goroutine-leak-prevention.md +49 -0
  30. package/skill-assets/sunlint-code-quality/rules/go/G006-interface-consumer-definition.md +45 -0
  31. package/skill-assets/sunlint-code-quality/rules/go/GN001-gin-binding-validation.md +57 -0
  32. package/skill-assets/sunlint-code-quality/rules/go/GN002-gin-error-response.md +48 -0
  33. package/skill-assets/sunlint-code-quality/rules/go/GN003-graceful-shutdown.md +57 -0
  34. package/skill-assets/sunlint-code-quality/rules/go/GN004-gin-route-logical-grouping.md +54 -0
@@ -9,8 +9,9 @@ const fs = require('fs');
9
9
  const chalk = require('chalk');
10
10
 
11
11
  class ArchitectureIntegration {
12
- constructor(options = {}) {
12
+ constructor(options = {}, config = {}) {
13
13
  this.options = options;
14
+ this.config = config; // Store merged config
14
15
  this.archModule = null;
15
16
  }
16
17
 
@@ -78,13 +79,25 @@ class ArchitectureIntegration {
78
79
  }
79
80
 
80
81
  /**
81
- * Parse architecture patterns from CLI option
82
+ * Parse architecture patterns from CLI option or config
83
+ * Priority: CLI --arch-patterns > Config file patterns > Default (undefined)
82
84
  */
83
85
  parsePatterns() {
84
- if (!this.options.archPatterns) {
85
- return undefined; // Use default patterns
86
+ // Priority 1: CLI --arch-patterns flag
87
+ if (this.options.archPatterns) {
88
+ return this.parsePatternString(this.options.archPatterns);
86
89
  }
87
90
 
91
+ // Priority 2: Config file patterns array
92
+ if (this.config?.architecture?.patterns?.length > 0) {
93
+ return this.normalizePatterns(this.config.architecture.patterns);
94
+ }
95
+
96
+ // Priority 3: Default (undefined = all patterns)
97
+ return undefined;
98
+ }
99
+
100
+ normalizePatterns(patterns) {
88
101
  const patternMap = {
89
102
  'layered': 'LAYERED',
90
103
  'modular': 'MODULAR',
@@ -95,13 +108,46 @@ class ArchitectureIntegration {
95
108
  'tdd': 'TDD_CLEAN_ARCHITECTURE',
96
109
  };
97
110
 
98
- const patterns = this.options.archPatterns
99
- .split(',')
100
- .map(p => p.trim().toLowerCase())
111
+ return patterns
112
+ .map(p => typeof p === 'string' ? p.trim().toLowerCase() : p)
101
113
  .map(p => patternMap[p] || p.toUpperCase())
102
114
  .filter(Boolean);
115
+ }
116
+
117
+ parsePatternString(patternString) {
118
+ const patterns = patternString.split(',').map(p => p.trim());
119
+ return this.normalizePatterns(patterns);
120
+ }
121
+
122
+ /**
123
+ * Determine if markdown report should be generated
124
+ * Priority: CLI flag > Config file > Default (false)
125
+ */
126
+ shouldGenerateReport() {
127
+ if (this.options.archReport !== undefined) {
128
+ return this.options.archReport;
129
+ }
103
130
 
104
- return patterns.length > 0 ? patterns : undefined;
131
+ if (this.config?.architecture?.generateReport !== undefined) {
132
+ return this.config.architecture.generateReport;
133
+ }
134
+
135
+ return false;
136
+ }
137
+
138
+ /**
139
+ * Get report filename from config or auto-generate
140
+ */
141
+ getReportFilename(projectPath) {
142
+ // Use config reportOutput if provided
143
+ if (this.config?.architecture?.reportOutput) {
144
+ return this.config.architecture.reportOutput;
145
+ }
146
+
147
+ // Auto-generate filename
148
+ const projectName = path.basename(projectPath);
149
+ const date = new Date().toISOString().split('T')[0];
150
+ return `sun_arch_report_${projectName}_${date}.md`;
105
151
  }
106
152
 
107
153
  /**
@@ -259,7 +305,7 @@ class ArchitectureIntegration {
259
305
 
260
306
  // Generate markdown report if requested
261
307
  let markdownReport = null;
262
- if (this.options.archReport) {
308
+ if (this.shouldGenerateReport()) {
263
309
  try {
264
310
  markdownReport = analyzer.formatAsMarkdown(result);
265
311
  } catch (error) {
@@ -313,12 +359,8 @@ class ArchitectureIntegration {
313
359
  /**
314
360
  * Save markdown report to file
315
361
  */
316
- async saveReport(markdownContent, projectPath) {
317
- const projectName = path.basename(projectPath);
318
- const date = new Date().toISOString().split('T')[0].replace(/-/g, '_');
319
- const fileName = `sun_arch_report_${projectName}_${date}.md`;
320
- const outputPath = path.join(process.cwd(), fileName);
321
-
362
+ async saveReport(markdownContent, reportFilename) {
363
+ const outputPath = path.join(process.cwd(), reportFilename);
322
364
  fs.writeFileSync(outputPath, markdownContent, 'utf8');
323
365
  return outputPath;
324
366
  }
@@ -25,13 +25,13 @@ class CliActionHandler {
25
25
  this.options = options;
26
26
  this.configManager = null;
27
27
  this.ruleSelectionService = new RuleSelectionService();
28
-
28
+
29
29
  // Use new orchestrator by default, fallback to legacy if needed
30
30
  this.orchestrator = new AnalysisOrchestrator();
31
-
31
+
32
32
  this.outputService = new OutputService(options);
33
33
  this.fileTargetingService = new FileTargetingService();
34
-
34
+
35
35
  this.isModernMode = !options.useLegacy;
36
36
  }
37
37
 
@@ -43,10 +43,11 @@ class CliActionHandler {
43
43
  try {
44
44
  this.displayModernBanner();
45
45
  this.handleShortcuts();
46
-
46
+
47
47
  // Load configuration
48
48
  const config = await this.loadConfiguration();
49
-
49
+ this.loadedConfig = config; // Store for architecture integration
50
+
50
51
  // Validate input with priority system
51
52
  this.validateInput(config);
52
53
 
@@ -60,7 +61,7 @@ class CliActionHandler {
60
61
 
61
62
  // Select rules to run
62
63
  const rulesToRun = await this.ruleSelectionService.selectRules(config, this.options);
63
-
64
+
64
65
  if (rulesToRun.length === 0) {
65
66
  console.log(chalk.yellow('āš ļø No rules to run'));
66
67
  return;
@@ -68,13 +69,20 @@ class CliActionHandler {
68
69
 
69
70
  // Apply enhanced file targeting
70
71
  const targetingResult = await this.applyFileTargeting(config);
71
- if (targetingResult.files.length === 0) {
72
+
73
+ // Determine if we should proceed based on requested analyses
74
+ const hasSourceFiles = targetingResult.files.length > 0;
75
+ const willRunCodeQuality = rulesToRun.length > 0 && !this.isArchitectureOnly() && !this.isImpactOnly();
76
+ const willRunArchitecture = !!config.architecture?.enabled;
77
+ const willRunImpact = !!(this.options.impact || config.impact?.enabled);
78
+
79
+ if (!hasSourceFiles && !willRunArchitecture && !willRunImpact) {
72
80
  console.log(chalk.yellow('āš ļø No files to analyze after applying filters'));
73
81
  this.displayTargetingStats(targetingResult.stats);
74
82
  return;
75
83
  }
76
84
 
77
- // Update options with filtered files
85
+ // Update options with filtered files (even if empty, for engines that handle it)
78
86
  this.options.targetFiles = targetingResult.files;
79
87
 
80
88
  // Display analysis info
@@ -91,8 +99,8 @@ class CliActionHandler {
91
99
  results = { results: [], summary: { total: 0, errors: 0, warnings: 0 } };
92
100
  }
93
101
 
94
- // Run architecture analysis if requested
95
- if (this.options.architecture) {
102
+ // Run architecture analysis if requested (via CLI or Config)
103
+ if (config.architecture?.enabled) {
96
104
  const architectureResults = await this.runArchitectureAnalysis();
97
105
  results.architecture = architectureResults;
98
106
  }
@@ -114,10 +122,10 @@ class CliActionHandler {
114
122
 
115
123
  // Exit with appropriate code
116
124
  this.handleExit(results);
117
-
125
+
118
126
  } catch (error) {
119
127
  console.error(chalk.red('āŒ Sun Lint Error:'), error.message);
120
-
128
+
121
129
  // Following Rule C035: Log complete error context
122
130
  if (this.options.debug) {
123
131
  console.error('Full error context:', {
@@ -127,7 +135,7 @@ class CliActionHandler {
127
135
  mode: this.isModernMode ? 'modern' : 'legacy'
128
136
  });
129
137
  }
130
-
138
+
131
139
  process.exit(1);
132
140
  }
133
141
  }
@@ -150,7 +158,7 @@ class CliActionHandler {
150
158
  verbose: this.options.verbose // Pass verbose for debugging
151
159
  }
152
160
  });
153
-
161
+
154
162
  if (this.options.verbose) {
155
163
  console.log(`šŸ”§ Debug: maxSemanticFiles option = ${this.options.maxSemanticFiles}`);
156
164
  console.log(`šŸ”§ Debug: parsed maxSemanticFiles = ${this.options.maxSemanticFiles !== undefined ? parseInt(this.options.maxSemanticFiles) : Infinity}`);
@@ -192,15 +200,15 @@ class CliActionHandler {
192
200
  if (this.options.engine === 'auto') {
193
201
  // Auto-select best engines: default to heuristic for compatibility
194
202
  const autoEngines = ['heuristic'];
195
-
203
+
196
204
  // Add ESLint for JS/TS files if available
197
205
  if (this.hasJavaScriptTypeScriptFiles() || config.eslint?.enabled !== false) {
198
206
  autoEngines.push('eslint');
199
207
  }
200
-
208
+
201
209
  return autoEngines;
202
210
  }
203
-
211
+
204
212
  // Return specific engine as requested
205
213
  return [this.options.engine];
206
214
  }
@@ -234,7 +242,7 @@ class CliActionHandler {
234
242
  */
235
243
  validateAIConfiguration(config) {
236
244
  const aiConfig = config.ai || {};
237
-
245
+
238
246
  // Check for API key
239
247
  if (!aiConfig.apiKey && !process.env.OPENAI_API_KEY) {
240
248
  console.warn(chalk.yellow('āš ļø No OpenAI API key found in config or environment'));
@@ -256,7 +264,7 @@ class CliActionHandler {
256
264
  */
257
265
  hasJavaScriptTypeScriptFiles() {
258
266
  if (!this.options.targetFiles) return false;
259
-
267
+
260
268
  return this.options.targetFiles.some(file => {
261
269
  const ext = require('path').extname(file).toLowerCase();
262
270
  return ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'].includes(ext);
@@ -272,7 +280,7 @@ class CliActionHandler {
272
280
  if (this.options.quiet || this.options.format === 'json') {
273
281
  return;
274
282
  }
275
-
283
+
276
284
  const { version } = require('../package.json');
277
285
 
278
286
  console.log();
@@ -284,7 +292,7 @@ class CliActionHandler {
284
292
  }
285
293
 
286
294
  // Delegate methods to base functionality (same as original CliActionHandler)
287
-
295
+
288
296
  /**
289
297
  * Load configuration using existing config manager
290
298
  * Following Rule C006: Verb-noun naming
@@ -325,7 +333,7 @@ class CliActionHandler {
325
333
  chalk.gray('Example: sunlint --all --output-summary=report.json --upload-report --input=src')
326
334
  );
327
335
  }
328
-
336
+
329
337
  // Set default URL if no URL provided (when uploadReport is true)
330
338
  if (typeof this.options.uploadReport === 'boolean' || !this.options.uploadReport) {
331
339
  this.options.uploadReport = 'https://coding-standards-report.sun-asterisk.vn/api/reports';
@@ -333,7 +341,7 @@ class CliActionHandler {
333
341
  console.log(chalk.gray(`ā„¹ļø Using default upload URL: ${this.options.uploadReport}`));
334
342
  }
335
343
  }
336
-
344
+
337
345
  // Basic URL validation
338
346
  try {
339
347
  new URL(this.options.uploadReport);
@@ -361,8 +369,8 @@ class CliActionHandler {
361
369
  if (config && config.include && Array.isArray(config.include) && config.include.length > 0) {
362
370
  // Config provides include patterns, use current directory as base
363
371
  // Let FileTargetingService handle the include patterns from config
364
- this.options.input = '.';
365
-
372
+ this.options.input = '.';
373
+
366
374
  if (this.options.verbose) {
367
375
  console.log(chalk.gray(`ā„¹ļø Using config include patterns: ${config.include.join(', ')}`));
368
376
  }
@@ -373,7 +381,7 @@ class CliActionHandler {
373
381
  if (!this.options.input && (!config || !config.include)) {
374
382
  // Set default input directory instead of glob patterns
375
383
  this.options.input = '.'; // Current directory, let FileTargetingService handle patterns
376
-
384
+
377
385
  if (this.options.verbose) {
378
386
  console.log(chalk.gray('ā„¹ļø Using default input: current directory with JS/TS file patterns'));
379
387
  }
@@ -422,14 +430,14 @@ class CliActionHandler {
422
430
  async showDryRunPreview(config, rulesToRun = null) {
423
431
  console.log(chalk.blue('šŸ” Dry Run Preview'));
424
432
  console.log(chalk.gray('This would analyze the following configuration:'));
425
-
433
+
426
434
  let rulesInfo;
427
435
  if (rulesToRun) {
428
436
  rulesInfo = `${rulesToRun.length} rules (${this.getPresetName()})`;
429
437
  } else {
430
438
  rulesInfo = this.options.rules || 'config-based';
431
439
  }
432
-
440
+
433
441
  console.log(JSON.stringify({
434
442
  rules: rulesInfo,
435
443
  files: this.options.targetFiles?.length || 'auto-detected',
@@ -492,7 +500,7 @@ class CliActionHandler {
492
500
  */
493
501
  displayTargetingStats(stats) {
494
502
  if (this.options.quiet) return;
495
-
503
+
496
504
  console.log(chalk.gray('Targeting Stats:'));
497
505
  Object.entries(stats).forEach(([key, value]) => {
498
506
  console.log(`• ${key}: ${value}`);
@@ -523,8 +531,11 @@ class CliActionHandler {
523
531
  * Following Rule C006: Verb-noun naming
524
532
  */
525
533
  isArchitectureOnly() {
526
- return this.options.architecture &&
527
- !this.options.impact &&
534
+ const isArchEnabled = this.options.architecture || this.loadedConfig?.architecture?.enabled;
535
+ const isImpactEnabled = this.options.impact || this.loadedConfig?.impact?.enabled;
536
+
537
+ return isArchEnabled &&
538
+ !isImpactEnabled &&
528
539
  !this.options.all &&
529
540
  !this.options.specific &&
530
541
  !this.options.rule &&
@@ -539,8 +550,11 @@ class CliActionHandler {
539
550
  * Following Rule C006: Verb-noun naming
540
551
  */
541
552
  isImpactOnly() {
542
- return this.options.impact &&
543
- !this.options.architecture &&
553
+ const isArchEnabled = this.options.architecture || this.loadedConfig?.architecture?.enabled;
554
+ const isImpactEnabled = this.options.impact || this.loadedConfig?.impact?.enabled;
555
+
556
+ return isImpactEnabled &&
557
+ !isArchEnabled &&
544
558
  !this.options.all &&
545
559
  !this.options.specific &&
546
560
  !this.options.rule &&
@@ -560,13 +574,14 @@ class CliActionHandler {
560
574
  }
561
575
 
562
576
  try {
563
- const integration = new ArchitectureIntegration(this.options);
577
+ const integration = new ArchitectureIntegration(this.options, this.loadedConfig);
564
578
  const projectPath = this.getProjectPath();
565
579
  const results = await integration.analyze(projectPath);
566
580
 
567
581
  // Save markdown report if requested
568
- if (this.options.archReport && results.markdownReport) {
569
- const reportPath = await integration.saveReport(results.markdownReport, projectPath);
582
+ if (integration.shouldGenerateReport() && results.markdownReport) {
583
+ const reportFilename = integration.getReportFilename(projectPath);
584
+ const reportPath = await integration.saveReport(results.markdownReport, reportFilename);
570
585
  if (!this.options.quiet) {
571
586
  console.log(chalk.green(`šŸ“„ Architecture report saved: ${reportPath}`));
572
587
  }
@@ -147,6 +147,12 @@ class ConfigManager {
147
147
  sortBy: 'severity',
148
148
  showProgress: true,
149
149
  exitOnError: false
150
+ },
151
+ architecture: {
152
+ enabled: false,
153
+ patterns: [],
154
+ generateReport: false,
155
+ reportOutput: undefined
150
156
  }
151
157
  };
152
158
  }
@@ -92,6 +92,12 @@ class ConfigMerger {
92
92
  // Performance overrides
93
93
  overrides.performance = this.applyPerformanceOverrides(overrides.performance, options);
94
94
 
95
+ // Architecture overrides
96
+ overrides.architecture = this.applyArchitectureOverrides(
97
+ overrides.architecture,
98
+ options
99
+ );
100
+
95
101
  // Auto-expand include patterns if input doesn't match any existing patterns
96
102
  const expandedOverrides = this.autoExpandIncludePatterns(overrides, options);
97
103
  // Copy expanded properties back to overrides
@@ -139,6 +145,33 @@ class ConfigMerger {
139
145
  return performance;
140
146
  }
141
147
 
148
+ /**
149
+ * Apply CLI architecture overrides to config
150
+ * Rule C006: verb-noun naming convention
151
+ * Priority: CLI flags > Config file > Defaults
152
+ */
153
+ applyArchitectureOverrides(architectureConfig, options) {
154
+ const architecture = { ...architectureConfig };
155
+
156
+ // CLI --architecture overrides config enabled
157
+ if (options.architecture !== undefined) {
158
+ architecture.enabled = options.architecture;
159
+ }
160
+
161
+ // CLI --arch-patterns overrides config patterns
162
+ if (options.archPatterns) {
163
+ const patterns = options.archPatterns.split(',').map(p => p.trim());
164
+ architecture.patterns = patterns;
165
+ }
166
+
167
+ // CLI --arch-report overrides config generateReport
168
+ if (options.archReport !== undefined) {
169
+ architecture.generateReport = options.archReport;
170
+ }
171
+
172
+ return architecture;
173
+ }
174
+
142
175
  /**
143
176
  * Rule C006: applyEnvironmentVariables - verb-noun naming
144
177
  */
@@ -11,12 +11,13 @@ class ConfigValidator {
11
11
  this.validRuleValues = ['error', 'warning', 'info', 'warn', 'off', true, false, 0, 1, 2];
12
12
  this.ruleValueMapping = {
13
13
  0: 'off',
14
- 1: 'warning',
14
+ 1: 'warning',
15
15
  2: 'error',
16
16
  'warn': 'warning',
17
17
  true: 'warning',
18
18
  false: 'off'
19
19
  };
20
+ this.validArchitecturePatterns = ['layered', 'modular', 'mvvm', 'viper', 'presentation', 'clean', 'tdd'];
20
21
  }
21
22
 
22
23
  /**
@@ -29,6 +30,7 @@ class ConfigValidator {
29
30
  this.validateIncludeExcludePatterns(config.include, config.exclude);
30
31
  this.validateOutputFormat(config.output);
31
32
  this.validateRuleValues(config.rules);
33
+ this.validateArchitectureConfig(config);
32
34
  }
33
35
 
34
36
  /**
@@ -109,7 +111,7 @@ class ConfigValidator {
109
111
 
110
112
  // Check category configuration
111
113
  const rule = rulesRegistry.rules[ruleId];
112
-
114
+
113
115
  if (rule && config.categories && config.categories[rule.category] !== undefined) {
114
116
  return this.normalizeRuleValue(config.categories[rule.category]);
115
117
  }
@@ -121,6 +123,39 @@ class ConfigValidator {
121
123
 
122
124
  return 'off';
123
125
  }
126
+
127
+ /**
128
+ * Rule C006: validateArchitectureConfig - verb-noun naming
129
+ * Rule C031: Specific validation logic for architecture configuration
130
+ */
131
+ validateArchitectureConfig(config) {
132
+ if (!config.architecture) return;
133
+
134
+ const chalk = require('chalk');
135
+ const arch = config.architecture;
136
+
137
+ if (arch.enabled !== undefined && typeof arch.enabled !== 'boolean') {
138
+ console.warn(chalk.yellow('āš ļø architecture.enabled must be boolean'));
139
+ }
140
+
141
+ if (arch.patterns !== undefined && !Array.isArray(arch.patterns)) {
142
+ console.warn(chalk.yellow('āš ļø architecture.patterns must be an array'));
143
+ }
144
+
145
+ if (arch.patterns && Array.isArray(arch.patterns)) {
146
+ const invalid = arch.patterns.filter(p =>
147
+ !this.validArchitecturePatterns.includes(String(p).toLowerCase())
148
+ );
149
+ if (invalid.length > 0) {
150
+ console.warn(chalk.yellow(`āš ļø Unknown patterns: ${invalid.join(', ')}`));
151
+ console.warn(chalk.gray(` Valid: ${this.validArchitecturePatterns.join(', ')}`));
152
+ }
153
+ }
154
+
155
+ if (arch.generateReport !== undefined && typeof arch.generateReport !== 'boolean') {
156
+ console.warn(chalk.yellow('āš ļø architecture.generateReport must be boolean'));
157
+ }
158
+ }
124
159
  }
125
160
 
126
161
  module.exports = ConfigValidator;
@@ -195,8 +195,11 @@ class OutputService {
195
195
  const totalFiles = results.filesAnalyzed || results.summary?.totalFiles || results.totalFiles || results.fileCount || 0;
196
196
 
197
197
  // Calculate LOC
198
+ // In changed-files mode (PR), use only the analyzed files for accurate scoring
198
199
  let loc = 0;
199
- if (options.input) {
200
+ if (options.changedFiles && options.targetFiles && options.targetFiles.length > 0) {
201
+ loc = this.scoringService.calculateLOC(options.targetFiles);
202
+ } else if (options.input) {
200
203
  const inputPaths = Array.isArray(options.input) ? options.input : [options.input];
201
204
  for (const inputPath of inputPaths) {
202
205
  if (fs.existsSync(inputPath)) {
@@ -210,9 +213,10 @@ class OutputService {
210
213
  }
211
214
  }
212
215
 
213
- // Count violations
216
+ // Count violations by severity
214
217
  const errorCount = violations.filter(v => v.severity === 'error').length;
215
218
  const warningCount = violations.filter(v => v.severity === 'warning').length;
219
+ const infoCount = violations.filter(v => v.severity === 'info').length;
216
220
 
217
221
  // Get number of rules checked - use metadata first, then parse from options
218
222
  let rulesChecked = metadata.rulesChecked;
@@ -225,10 +229,14 @@ class OutputService {
225
229
  }
226
230
  rulesChecked = rulesChecked || 1;
227
231
 
232
+ // Determine scoring mode
233
+ const scoringMode = options.changedFiles ? 'pr' : 'project';
234
+
228
235
  // Calculate score
229
236
  const scoringSummary = this.scoringService.generateScoringSummary({
230
237
  errorCount,
231
238
  warningCount,
239
+ infoCount,
232
240
  rulesChecked,
233
241
  loc
234
242
  });
@@ -243,7 +251,8 @@ class OutputService {
243
251
  filesAnalyzed: totalFiles,
244
252
  duration: metadata.duration,
245
253
  version: metadata.version || this.version,
246
- architecture: results.architecture || null
254
+ architecture: results.architecture || null,
255
+ scoringMode
247
256
  }
248
257
  );
249
258
 
@@ -22,9 +22,10 @@ class ScoringService {
22
22
  //
23
23
  this.weights = {
24
24
  // Penalty per violation type (per KLOC)
25
- // Errors are 3x more severe than warnings
25
+ // Severity ratio: error(6) : warning(2) : info(0.5) = 12:4:1
26
26
  errorPenaltyPerKLOC: 6, // Each error per KLOC reduces score by 6 points
27
27
  warningPenaltyPerKLOC: 2, // Each warning per KLOC reduces score by 2 points
28
+ infoPenaltyPerKLOC: 0.5, // Each info per KLOC reduces score by 0.5 points
28
29
 
29
30
  // Absolute penalty thresholds (regardless of LOC)
30
31
  // Large projects should still be penalized for raw violation counts
@@ -57,11 +58,12 @@ class ScoringService {
57
58
  * @param {Object} params
58
59
  * @param {number} params.errorCount - Number of errors found
59
60
  * @param {number} params.warningCount - Number of warnings found
61
+ * @param {number} params.infoCount - Number of info violations found
60
62
  * @param {number} params.rulesChecked - Number of rules checked
61
63
  * @param {number} params.loc - Total lines of code
62
64
  * @returns {number} Score between 0-100
63
65
  */
64
- calculateScore({ errorCount = 0, warningCount = 0, rulesChecked = 0, loc = 0 }) {
66
+ calculateScore({ errorCount = 0, warningCount = 0, infoCount = 0, rulesChecked = 0, loc = 0 }) {
65
67
  // Base score starts at 100
66
68
  let score = 100;
67
69
 
@@ -72,12 +74,13 @@ class ScoringService {
72
74
  // Calculate violations per KLOC
73
75
  const errorsPerKLOC = errorCount / kloc;
74
76
  const warningsPerKLOC = warningCount / kloc;
75
- const totalViolationsPerKLOC = errorsPerKLOC + warningsPerKLOC;
77
+ const infosPerKLOC = infoCount / kloc;
76
78
 
77
79
  // 1. Density-based penalty (main scoring factor)
78
80
  // This penalizes based on how "dense" the violations are
79
81
  const densityPenalty = (errorsPerKLOC * this.weights.errorPenaltyPerKLOC) +
80
- (warningsPerKLOC * this.weights.warningPenaltyPerKLOC);
82
+ (warningsPerKLOC * this.weights.warningPenaltyPerKLOC) +
83
+ (infosPerKLOC * this.weights.infoPenaltyPerKLOC);
81
84
  score -= densityPenalty;
82
85
 
83
86
  // 2. Absolute penalty for projects with too many errors
@@ -195,6 +198,8 @@ class ScoringService {
195
198
  generateScoringSummary(params) {
196
199
  const score = this.calculateScore(params);
197
200
  const grade = this.getGrade(score);
201
+ const infoCount = params.infoCount || 0;
202
+ const totalViolations = params.errorCount + params.warningCount + infoCount;
198
203
 
199
204
  return {
200
205
  score,
@@ -202,10 +207,11 @@ class ScoringService {
202
207
  metrics: {
203
208
  errors: params.errorCount,
204
209
  warnings: params.warningCount,
210
+ infos: infoCount,
205
211
  rulesChecked: params.rulesChecked,
206
212
  linesOfCode: params.loc,
207
- violationsPerKLOC: params.loc > 0
208
- ? Math.round(((params.errorCount + params.warningCount) / params.loc * 1000) * 10) / 10
213
+ violationsPerKLOC: params.loc > 0
214
+ ? Math.round((totalViolations / params.loc * 1000) * 10) / 10
209
215
  : 0
210
216
  }
211
217
  };
@@ -247,11 +247,12 @@ class SummaryReportService {
247
247
  total_violations: violations.length,
248
248
  error_count: scoringSummary.metrics.errors,
249
249
  warning_count: scoringSummary.metrics.warnings,
250
- info_count: 0, // Reserved for future use
250
+ info_count: scoringSummary.metrics.infos || 0,
251
251
  lines_of_code: scoringSummary.metrics.linesOfCode,
252
252
  files_analyzed: options.filesAnalyzed || 0,
253
253
  sunlint_version: options.version || this.version,
254
254
  analysis_duration_ms: options.duration || 0,
255
+ scoring_mode: options.scoringMode || 'project',
255
256
  violations: violationsSummary,
256
257
 
257
258
  // Additional metadata for backwards compatibility
@@ -259,7 +260,8 @@ class SummaryReportService {
259
260
  generated_at: new Date().toISOString(),
260
261
  tool: 'SunLint',
261
262
  version: options.version || this.version,
262
- analysis_duration_ms: options.duration || 0
263
+ analysis_duration_ms: options.duration || 0,
264
+ scoring_mode: options.scoringMode || 'project'
263
265
  },
264
266
  quality: {
265
267
  score: scoringSummary.score,
@@ -384,15 +386,18 @@ class SummaryReportService {
384
386
  const totalViolations = summaryReport.total_violations || 0;
385
387
  const errorCount = summaryReport.error_count || 0;
386
388
  const warningCount = summaryReport.warning_count || 0;
389
+ const infoCount = summaryReport.info_count || 0;
387
390
  const violationsByRule = summaryReport.violations || [];
388
391
  const violationsPerKLOC = summaryReport.quality?.metrics?.violationsPerKLOC || 0;
392
+ const scoringMode = summaryReport.scoring_mode || 'project';
389
393
 
390
394
  let output = '\nšŸ“Š Quality Summary Report\n';
391
395
  output += '━'.repeat(50) + '\n';
392
- output += `šŸ“ˆ Quality Score: ${score} (Grade: ${grade})\n`;
396
+ output += `šŸ“ˆ Quality Score: ${score} (Grade: ${grade})`;
397
+ output += scoringMode === 'pr' ? ' [PR mode]\n' : '\n';
393
398
  output += `šŸ“ Files Analyzed: ${filesAnalyzed}\n`;
394
399
  output += `šŸ“ Lines of Code: ${linesOfCode.toLocaleString()}\n`;
395
- output += `āš ļø Total Violations: ${totalViolations} (${errorCount} errors, ${warningCount} warnings)\n`;
400
+ output += `āš ļø Total Violations: ${totalViolations} (${errorCount} errors, ${warningCount} warnings, ${infoCount} info)\n`;
396
401
  output += `šŸ“Š Violations per KLOC: ${violationsPerKLOC}\n`;
397
402
 
398
403
  if (violationsByRule.length > 0) {