@sun-asterisk/sunlint 1.3.46 → 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 (102) hide show
  1. package/config/rules/rules-registry-generated.json +1717 -282
  2. package/core/adapters/sunlint-rule-adapter.js +16 -0
  3. package/core/architecture-integration.js +57 -15
  4. package/core/cli-action-handler.js +51 -36
  5. package/core/config-manager.js +6 -0
  6. package/core/config-merger.js +33 -0
  7. package/core/config-validator.js +37 -2
  8. package/core/output-service.js +12 -3
  9. package/core/rule-selection-service.js +24 -3
  10. package/core/scoring-service.js +12 -6
  11. package/core/summary-report-service.js +9 -4
  12. package/engines/heuristic-engine.js +6 -1
  13. package/engines/impact/cli.js +54 -39
  14. package/engines/impact/config/default-config.js +105 -5
  15. package/engines/impact/core/impact-analyzer.js +12 -15
  16. package/engines/impact/core/utils/gitignore-parser.js +123 -0
  17. package/engines/impact/core/utils/method-call-graph.js +272 -87
  18. package/origin-rules/dart-en.md +1 -1
  19. package/origin-rules/go-en.md +231 -0
  20. package/origin-rules/php-en.md +107 -0
  21. package/origin-rules/python-en.md +113 -0
  22. package/origin-rules/ruby-en.md +607 -0
  23. package/package.json +2 -2
  24. package/scripts/copy-arch-detect.js +5 -1
  25. package/scripts/copy-impact-analyzer.js +5 -1
  26. package/scripts/generate-rules-registry.js +30 -14
  27. package/skill-assets/sunlint-code-quality/SKILL.md +3 -2
  28. package/skill-assets/sunlint-code-quality/rules/go/G001-explicit-error-handling.md +53 -0
  29. package/skill-assets/sunlint-code-quality/rules/go/G002-context-first-argument.md +44 -0
  30. package/skill-assets/sunlint-code-quality/rules/go/G003-receiver-consistency.md +38 -0
  31. package/skill-assets/sunlint-code-quality/rules/go/G004-avoid-panic.md +49 -0
  32. package/skill-assets/sunlint-code-quality/rules/go/G005-goroutine-leak-prevention.md +49 -0
  33. package/skill-assets/sunlint-code-quality/rules/go/G006-interface-consumer-definition.md +45 -0
  34. package/skill-assets/sunlint-code-quality/rules/go/GN001-gin-binding-validation.md +57 -0
  35. package/skill-assets/sunlint-code-quality/rules/go/GN002-gin-error-response.md +48 -0
  36. package/skill-assets/sunlint-code-quality/rules/go/GN003-graceful-shutdown.md +57 -0
  37. package/skill-assets/sunlint-code-quality/rules/go/GN004-gin-route-logical-grouping.md +54 -0
  38. package/skill-assets/sunlint-code-quality/rules/ruby/C006-verb-noun-functions.md +63 -0
  39. package/skill-assets/sunlint-code-quality/rules/ruby/C013-no-dead-code.md +48 -0
  40. package/skill-assets/sunlint-code-quality/rules/ruby/C014-dependency-injection.md +42 -0
  41. package/skill-assets/sunlint-code-quality/rules/ruby/C017-no-constructor-logic.md +42 -0
  42. package/skill-assets/sunlint-code-quality/rules/ruby/C018-generic-errors.md +41 -0
  43. package/skill-assets/sunlint-code-quality/rules/ruby/C019-error-log-level.md +41 -0
  44. package/skill-assets/sunlint-code-quality/rules/ruby/C020-no-unused-imports.md +36 -0
  45. package/skill-assets/sunlint-code-quality/rules/ruby/C022-no-unused-variables.md +31 -0
  46. package/skill-assets/sunlint-code-quality/rules/ruby/C023-no-duplicate-names.md +39 -0
  47. package/skill-assets/sunlint-code-quality/rules/ruby/C024-centralize-constants.md +35 -0
  48. package/skill-assets/sunlint-code-quality/rules/ruby/C029-catch-log-root-cause.md +34 -0
  49. package/skill-assets/sunlint-code-quality/rules/ruby/C030-custom-error-classes.md +32 -0
  50. package/skill-assets/sunlint-code-quality/rules/ruby/C033-separate-data-access.md +52 -0
  51. package/skill-assets/sunlint-code-quality/rules/ruby/C035-error-context-logging.md +34 -0
  52. package/skill-assets/sunlint-code-quality/rules/ruby/C041-no-hardcoded-secrets.md +29 -0
  53. package/skill-assets/sunlint-code-quality/rules/ruby/C042-boolean-naming.md +38 -0
  54. package/skill-assets/sunlint-code-quality/rules/ruby/C052-controller-parsing.md +37 -0
  55. package/skill-assets/sunlint-code-quality/rules/ruby/C060-superclass-logic.md +38 -0
  56. package/skill-assets/sunlint-code-quality/rules/ruby/C067-no-hardcoded-config.md +37 -0
  57. package/skill-assets/sunlint-code-quality/rules/ruby/S003-open-redirect.md +58 -0
  58. package/skill-assets/sunlint-code-quality/rules/ruby/S004-no-log-credentials.md +38 -0
  59. package/skill-assets/sunlint-code-quality/rules/ruby/S005-server-authorization.md +37 -0
  60. package/skill-assets/sunlint-code-quality/rules/ruby/S006-default-credentials.md +29 -0
  61. package/skill-assets/sunlint-code-quality/rules/ruby/S007-output-encoding.md +31 -0
  62. package/skill-assets/sunlint-code-quality/rules/ruby/S009-approved-crypto.md +31 -0
  63. package/skill-assets/sunlint-code-quality/rules/ruby/S010-csprng.md +30 -0
  64. package/skill-assets/sunlint-code-quality/rules/ruby/S011-encrypted-client-hello.md +27 -0
  65. package/skill-assets/sunlint-code-quality/rules/ruby/S012-secrets-management.md +28 -0
  66. package/skill-assets/sunlint-code-quality/rules/ruby/S013-tls-connections.md +30 -0
  67. package/skill-assets/sunlint-code-quality/rules/ruby/S016-no-sensitive-query-string.md +37 -0
  68. package/skill-assets/sunlint-code-quality/rules/ruby/S017-parameterized-queries.md +33 -0
  69. package/skill-assets/sunlint-code-quality/rules/ruby/S019-email-input-sanitization.md +31 -0
  70. package/skill-assets/sunlint-code-quality/rules/ruby/S020-eval-code-execution.md +36 -0
  71. package/skill-assets/sunlint-code-quality/rules/ruby/S022-context-escaping.md +36 -0
  72. package/skill-assets/sunlint-code-quality/rules/ruby/S023-dynamic-js-encoding.md +33 -0
  73. package/skill-assets/sunlint-code-quality/rules/ruby/S025-server-validation.md +30 -0
  74. package/skill-assets/sunlint-code-quality/rules/ruby/S026-tls-encryption.md +30 -0
  75. package/skill-assets/sunlint-code-quality/rules/ruby/S027-mtls-validation.md +26 -0
  76. package/skill-assets/sunlint-code-quality/rules/ruby/S028-upload-limits.md +33 -0
  77. package/skill-assets/sunlint-code-quality/rules/ruby/S029-csrf-protection.md +32 -0
  78. package/skill-assets/sunlint-code-quality/rules/ruby/S030-directory-browsing.md +30 -0
  79. package/skill-assets/sunlint-code-quality/rules/ruby/S031-secure-cookie-flag.md +27 -0
  80. package/skill-assets/sunlint-code-quality/rules/ruby/S032-httponly-cookie.md +26 -0
  81. package/skill-assets/sunlint-code-quality/rules/ruby/S033-samesite-cookie.md +29 -0
  82. package/skill-assets/sunlint-code-quality/rules/ruby/S034-host-prefix-cookie.md +30 -0
  83. package/skill-assets/sunlint-code-quality/rules/ruby/S035-app-hostnames.md +28 -0
  84. package/skill-assets/sunlint-code-quality/rules/ruby/S036-internal-file-paths.md +37 -0
  85. package/skill-assets/sunlint-code-quality/rules/ruby/S037-anti-cache-headers.md +31 -0
  86. package/skill-assets/sunlint-code-quality/rules/ruby/S039-tls-certificate-validation.md +29 -0
  87. package/skill-assets/sunlint-code-quality/rules/ruby/S041-logout-invalidation.md +31 -0
  88. package/skill-assets/sunlint-code-quality/rules/ruby/S042-long-lived-sessions.md +27 -0
  89. package/skill-assets/sunlint-code-quality/rules/ruby/S044-critical-changes-reauth.md +34 -0
  90. package/skill-assets/sunlint-code-quality/rules/ruby/S045-brute-force-protection.md +33 -0
  91. package/skill-assets/sunlint-code-quality/rules/ruby/S047-oauth-csrf-protection.md +33 -0
  92. package/skill-assets/sunlint-code-quality/rules/ruby/S048-oauth-redirect-validation.md +29 -0
  93. package/skill-assets/sunlint-code-quality/rules/ruby/S049-auth-code-expiry.md +31 -0
  94. package/skill-assets/sunlint-code-quality/rules/ruby/S050-token-entropy.md +26 -0
  95. package/skill-assets/sunlint-code-quality/rules/ruby/S051-password-length.md +38 -0
  96. package/skill-assets/sunlint-code-quality/rules/ruby/S052-otp-entropy.md +25 -0
  97. package/skill-assets/sunlint-code-quality/rules/ruby/S053-generic-error-messages.md +33 -0
  98. package/skill-assets/sunlint-code-quality/rules/ruby/S054-no-default-admin.md +29 -0
  99. package/skill-assets/sunlint-code-quality/rules/ruby/S055-content-type-validation.md +24 -0
  100. package/skill-assets/sunlint-code-quality/rules/ruby/S056-log-injection.md +28 -0
  101. package/skill-assets/sunlint-code-quality/rules/ruby/S057-synchronized-time.md +18 -0
  102. package/skill-assets/sunlint-code-quality/rules/ruby/S058-ssrf-protection.md +39 -0
@@ -203,6 +203,22 @@ class SunlintRuleAdapter {
203
203
  return Array.from(this.rulesCache.keys());
204
204
  }
205
205
 
206
+ /**
207
+ * Get rules registry for severity resolution
208
+ * Returns object with rules property compatible with ConfigValidator
209
+ */
210
+ getRulesRegistry() {
211
+ if (this.registryCache) {
212
+ return { rules: this.registryCache };
213
+ }
214
+ // Convert cache map to registry format
215
+ const rules = {};
216
+ this.rulesCache.forEach((rule, ruleId) => {
217
+ rules[ruleId] = rule;
218
+ });
219
+ return { rules };
220
+ }
221
+
206
222
  /**
207
223
  * Validate rule ID
208
224
  */
@@ -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
 
@@ -10,11 +10,13 @@ const fs = require('fs');
10
10
  const path = require('path');
11
11
  const RuleMappingService = require('./rule-mapping-service');
12
12
  const SunlintRuleAdapter = require('./adapters/sunlint-rule-adapter');
13
+ const ConfigValidator = require('./config-validator');
13
14
 
14
15
  class RuleSelectionService {
15
16
  constructor() {
16
17
  this.ruleAdapter = SunlintRuleAdapter.getInstance();
17
18
  this.ruleMappingService = new RuleMappingService();
19
+ this.configValidator = new ConfigValidator();
18
20
  this.initialized = false;
19
21
  // Path works both in dev (from pages/) and npm package (from config/)
20
22
  this.releasedRulesPath = path.join(__dirname, '../config/released-rules.json');
@@ -209,14 +211,33 @@ class RuleSelectionService {
209
211
  selectedRules = selectedRules.filter(ruleId => !disabledRules.has(ruleId));
210
212
  }
211
213
 
212
- // Convert to rule objects
214
+ // Get rules registry for severity resolution
215
+ const rulesRegistry = this.ruleAdapter.getRulesRegistry();
216
+
217
+ // Convert to rule objects with proper severity from config
213
218
  return selectedRules.map(ruleId => {
214
219
  const adapterRule = this.ruleAdapter.getRuleById(ruleId);
220
+
221
+ // Resolve severity from config (config.rules > config.categories > rule default)
222
+ const effectiveConfig = this.configValidator.getEffectiveRuleConfiguration(
223
+ ruleId,
224
+ config,
225
+ rulesRegistry
226
+ );
227
+
228
+ // Map config values to severity (error/warn)
229
+ let severity = 'warning';
230
+ if (effectiveConfig === 'error' || effectiveConfig === 2) {
231
+ severity = 'error';
232
+ } else if (effectiveConfig === 'warn' || effectiveConfig === 'warning' || effectiveConfig === 1) {
233
+ severity = 'warning';
234
+ }
235
+
215
236
  return {
237
+ ...(adapterRule || {}),
216
238
  id: ruleId,
217
239
  name: this.getRuleName(ruleId),
218
- severity: 'warning',
219
- ...(adapterRule || {})
240
+ severity // Must come AFTER spread to override adapter's severity
220
241
  };
221
242
  }).filter(rule => rule.id);
222
243
  }