@sun-asterisk/sunlint 1.3.47 → 1.3.49

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 (185) 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/file-targeting-service.js +148 -15
  8. package/core/init-command.js +118 -70
  9. package/core/output-service.js +12 -3
  10. package/core/project-detector.js +517 -0
  11. package/core/scoring-service.js +12 -6
  12. package/core/summary-report-service.js +9 -4
  13. package/core/tui-select.js +245 -0
  14. package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
  15. package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
  16. package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
  17. package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
  18. package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
  19. package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
  20. package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
  21. package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
  22. package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
  23. package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
  24. package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
  25. package/engines/impact/cli.js +54 -39
  26. package/engines/impact/config/default-config.js +105 -5
  27. package/engines/impact/core/impact-analyzer.js +12 -15
  28. package/engines/impact/core/utils/gitignore-parser.js +123 -0
  29. package/engines/impact/core/utils/method-call-graph.js +272 -87
  30. package/origin-rules/dart-en.md +1 -1
  31. package/origin-rules/go-en.md +231 -0
  32. package/origin-rules/php-en.md +107 -0
  33. package/origin-rules/python-en.md +113 -0
  34. package/origin-rules/ruby-en.md +607 -0
  35. package/package.json +1 -1
  36. package/scripts/copy-arch-detect.js +5 -1
  37. package/scripts/copy-impact-analyzer.js +5 -1
  38. package/scripts/generate-rules-registry.js +30 -14
  39. package/skill-assets/sunlint-code-quality/SKILL.md +3 -2
  40. package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
  41. package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
  42. package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
  43. package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
  44. package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
  45. package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
  46. package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
  47. package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
  48. package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
  49. package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
  50. package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
  51. package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
  52. package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
  53. package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
  54. package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
  55. package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
  56. package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
  57. package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
  58. package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
  59. package/skill-assets/sunlint-code-quality/rules/go/G001-explicit-error-handling.md +53 -0
  60. package/skill-assets/sunlint-code-quality/rules/go/G002-context-first-argument.md +44 -0
  61. package/skill-assets/sunlint-code-quality/rules/go/G003-receiver-consistency.md +38 -0
  62. package/skill-assets/sunlint-code-quality/rules/go/G004-avoid-panic.md +49 -0
  63. package/skill-assets/sunlint-code-quality/rules/go/G005-goroutine-leak-prevention.md +49 -0
  64. package/skill-assets/sunlint-code-quality/rules/go/G006-interface-consumer-definition.md +45 -0
  65. package/skill-assets/sunlint-code-quality/rules/go/GN001-gin-binding-validation.md +57 -0
  66. package/skill-assets/sunlint-code-quality/rules/go/GN002-gin-error-response.md +48 -0
  67. package/skill-assets/sunlint-code-quality/rules/go/GN003-graceful-shutdown.md +57 -0
  68. package/skill-assets/sunlint-code-quality/rules/go/GN004-gin-route-logical-grouping.md +54 -0
  69. package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
  70. package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
  71. package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
  72. package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
  73. package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
  74. package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
  75. package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
  76. package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
  77. package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
  78. package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
  79. package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
  80. package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
  81. package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
  82. package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
  83. package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
  84. package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
  85. package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
  86. package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
  87. package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
  88. package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
  89. package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
  90. package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
  91. package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
  92. package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
  93. package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
  94. package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
  95. package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
  96. package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
  97. package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
  98. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
  99. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
  100. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
  101. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
  102. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
  103. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
  104. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
  105. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
  106. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
  107. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
  108. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
  109. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
  110. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
  111. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
  112. package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
  113. package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
  114. package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
  115. package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
  116. package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
  117. package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
  118. package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
  119. package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
  120. package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
  121. package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
  122. package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
  123. package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
  124. package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
  125. package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
  126. package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
  127. package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
  128. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
  129. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
  130. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
  131. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
  132. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
  133. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
  134. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
  135. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
  136. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
  137. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
  138. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
  139. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
  140. package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
  141. package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
  142. package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
  143. package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
  144. package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
  145. package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
  146. package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
  147. package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
  148. package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
  149. package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
  150. package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
  151. package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
  152. package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
  153. package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
  154. package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
  155. package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
  156. package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
  157. package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
  158. package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
  159. package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
  160. package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
  161. package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
  162. package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
  163. package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
  164. package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
  165. package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
  166. package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
  167. package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
  168. package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
  169. package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
  170. package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
  171. package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
  172. package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
  173. package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
  174. package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
  175. package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
  176. package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
  177. package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
  178. package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
  179. package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
  180. package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
  181. package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
  182. package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
  183. package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
  184. package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
  185. package/skill-assets/sunlint-code-quality/rules/swift/S061-jailbreak-detection.md +132 -0
@@ -0,0 +1,517 @@
1
+ /**
2
+ * SunLint Project Detector
3
+ * Analyzes project directory structure to recommend a language/framework preset.
4
+ * Rule C005: Single responsibility - detect project type only
5
+ * Rule C015: Uses domain language (preset, framework, confidence)
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ /**
14
+ * @typedef {Object} DetectionResult
15
+ * @property {string} language - Matched language key (e.g. 'php', 'typescript')
16
+ * @property {string} framework - Detected framework name (e.g. 'Laravel', 'Next.js')
17
+ * @property {string|null} frameworkKey - Rules sub-folder key (e.g. 'php-laravel', 'ruby-rails', 'go-gin') or null
18
+ * @property {string} confidence - 'high' | 'medium' | 'low'
19
+ * @property {string} reason - Human-readable explanation
20
+ * @property {string[]} signals - File/dir signals that triggered the match
21
+ */
22
+
23
+ /**
24
+ * Ordered detection rules. First match wins.
25
+ * Each rule: { check(dir) → string[] | null, language, framework, confidence }
26
+ */
27
+ const DETECTION_RULES = [
28
+ // ── PHP ──────────────────────────────────────────────────────────────────
29
+ {
30
+ name: 'PHP/Laravel',
31
+ language: 'php',
32
+ framework: 'Laravel',
33
+ frameworkKey: 'php-laravel',
34
+ confidence: 'high',
35
+ check(dir) {
36
+ const signals = [];
37
+ if (fileExists(dir, 'artisan')) signals.push('artisan');
38
+ if (fileExists(dir, 'composer.json')) {
39
+ signals.push('composer.json');
40
+ const composerJson = readJsonSafe(path.join(dir, 'composer.json'));
41
+ if (composerJson && composerJson.require && composerJson.require['laravel/framework']) {
42
+ signals.push('require.laravel/framework');
43
+ }
44
+ }
45
+ if (dirExists(dir, 'vendor', 'laravel')) signals.push('vendor/laravel/');
46
+
47
+ const isLaravel = signals.includes('artisan') ||
48
+ signals.includes('require.laravel/framework') ||
49
+ signals.includes('vendor/laravel/');
50
+ return isLaravel && signals.length >= 2 ? signals : null;
51
+ }
52
+ },
53
+ {
54
+ name: 'PHP/Symfony',
55
+ language: 'php',
56
+ framework: 'Symfony',
57
+ confidence: 'high',
58
+ check(dir) {
59
+ const signals = [];
60
+ if (fileExists(dir, 'composer.json')) signals.push('composer.json');
61
+ if (fileExists(dir, 'symfony.lock')) signals.push('symfony.lock');
62
+ if (dirExists(dir, 'config', 'packages')) signals.push('config/packages/');
63
+ const composerJson = readJsonSafe(path.join(dir, 'composer.json'));
64
+ if (composerJson && composerJson.require && composerJson.require['symfony/framework-bundle']) {
65
+ signals.push('require.symfony/framework-bundle');
66
+ }
67
+ return signals.length >= 2 ? signals : null;
68
+ }
69
+ },
70
+ {
71
+ name: 'PHP',
72
+ language: 'php',
73
+ framework: 'PHP',
74
+ confidence: 'medium',
75
+ check(dir) {
76
+ const signals = [];
77
+ if (fileExists(dir, 'composer.json')) signals.push('composer.json');
78
+ if (fileExists(dir, 'composer.lock')) signals.push('composer.lock');
79
+ if (hasFilesWithExtension(dir, '.php', 2)) signals.push('*.php files');
80
+ return signals.length >= 1 ? signals : null;
81
+ }
82
+ },
83
+
84
+ // ── Dart / Flutter ───────────────────────────────────────────────────────
85
+ {
86
+ name: 'Dart/Flutter',
87
+ language: 'dart',
88
+ framework: 'Flutter',
89
+ confidence: 'high',
90
+ check(dir) {
91
+ const signals = [];
92
+ if (fileExists(dir, 'pubspec.yaml')) signals.push('pubspec.yaml');
93
+ const pubspec = readYamlLineSafe(path.join(dir, 'pubspec.yaml'), 'flutter');
94
+ if (pubspec) signals.push('pubspec.yaml → flutter dependency');
95
+ if (dirExists(dir, 'lib')) signals.push('lib/');
96
+ if (dirExists(dir, 'android') || dirExists(dir, 'ios')) signals.push('android/ or ios/');
97
+ return fileExists(dir, 'pubspec.yaml') ? signals : null;
98
+ }
99
+ },
100
+
101
+ // ── Kotlin ───────────────────────────────────────────────────────────────
102
+ {
103
+ name: 'Kotlin/Android',
104
+ language: 'kotlin',
105
+ framework: 'Android',
106
+ confidence: 'high',
107
+ check(dir) {
108
+ const signals = [];
109
+ if (fileExists(dir, 'build.gradle') || fileExists(dir, 'build.gradle.kts')) {
110
+ signals.push('build.gradle');
111
+ }
112
+ if (fileExists(dir, 'gradlew')) signals.push('gradlew');
113
+ if (dirExists(dir, 'app', 'src', 'main')) signals.push('app/src/main/');
114
+ if (hasFilesWithExtension(dir, '.kt', 2)) signals.push('*.kt files');
115
+ const hasAndroidManifest = fileExists(dir, 'app', 'src', 'main', 'AndroidManifest.xml');
116
+ if (hasAndroidManifest) signals.push('AndroidManifest.xml');
117
+ return (signals.includes('*.kt files') || hasAndroidManifest) ? signals : null;
118
+ }
119
+ },
120
+ {
121
+ name: 'Kotlin',
122
+ language: 'kotlin',
123
+ framework: 'Kotlin',
124
+ confidence: 'medium',
125
+ check(dir) {
126
+ const signals = [];
127
+ if (fileExists(dir, 'build.gradle.kts')) signals.push('build.gradle.kts');
128
+ if (hasFilesWithExtension(dir, '.kt', 1)) signals.push('*.kt files');
129
+ return signals.length >= 1 ? signals : null;
130
+ }
131
+ },
132
+
133
+ // ── Java ─────────────────────────────────────────────────────────────────
134
+ {
135
+ name: 'Java/Spring Boot',
136
+ language: 'java',
137
+ framework: 'Spring Boot',
138
+ confidence: 'high',
139
+ check(dir) {
140
+ const signals = [];
141
+ if (fileExists(dir, 'pom.xml')) signals.push('pom.xml');
142
+ if (fileExists(dir, 'build.gradle') || fileExists(dir, 'gradlew')) signals.push('build.gradle');
143
+ const pom = readFileSafeu(path.join(dir, 'pom.xml'));
144
+ if (pom && pom.includes('spring-boot')) signals.push('pom.xml → spring-boot');
145
+ if (dirExists(dir, 'src', 'main', 'java')) signals.push('src/main/java/');
146
+ return (signals.includes('pom.xml → spring-boot') || signals.includes('src/main/java/')) ? signals : null;
147
+ }
148
+ },
149
+ {
150
+ name: 'Java',
151
+ language: 'java',
152
+ framework: 'Java',
153
+ confidence: 'medium',
154
+ check(dir) {
155
+ const signals = [];
156
+ if (fileExists(dir, 'pom.xml')) signals.push('pom.xml');
157
+ if (fileExists(dir, 'build.gradle')) signals.push('build.gradle');
158
+ if (hasFilesWithExtension(dir, '.java', 2)) signals.push('*.java files');
159
+ return signals.length >= 1 ? signals : null;
160
+ }
161
+ },
162
+
163
+ // ── C# / .NET ────────────────────────────────────────────────────────────
164
+ {
165
+ name: 'C#/.NET',
166
+ language: 'csharp',
167
+ framework: '.NET',
168
+ confidence: 'high',
169
+ check(dir) {
170
+ const signals = [];
171
+ if (hasFilesWithExtension(dir, '.csproj', 1)) signals.push('*.csproj');
172
+ if (hasFilesWithExtension(dir, '.sln', 1)) signals.push('*.sln');
173
+ if (fileExists(dir, 'global.json')) signals.push('global.json');
174
+ if (hasFilesWithExtension(dir, '.cs', 2)) signals.push('*.cs files');
175
+ return signals.length >= 1 ? signals : null;
176
+ }
177
+ },
178
+
179
+ // ── Swift / iOS ──────────────────────────────────────────────────────────
180
+ {
181
+ name: 'Swift/iOS',
182
+ language: 'swift',
183
+ framework: 'iOS/macOS',
184
+ confidence: 'high',
185
+ check(dir) {
186
+ const signals = [];
187
+ if (hasFilesWithExtension(dir, '.xcodeproj', 1)) signals.push('*.xcodeproj');
188
+ if (hasFilesWithExtension(dir, '.xcworkspace', 1)) signals.push('*.xcworkspace');
189
+ if (fileExists(dir, 'Podfile')) signals.push('Podfile');
190
+ if (fileExists(dir, 'Package.swift')) signals.push('Package.swift');
191
+ if (hasFilesWithExtension(dir, '.swift', 2)) signals.push('*.swift files');
192
+ return signals.length >= 1 ? signals : null;
193
+ }
194
+ },
195
+
196
+ // ── Go ───────────────────────────────────────────────────────────────────
197
+ {
198
+ name: 'Go/Gin',
199
+ language: 'go',
200
+ framework: 'Gin',
201
+ frameworkKey: 'go-gin',
202
+ confidence: 'high',
203
+ check(dir) {
204
+ const signals = [];
205
+ if (fileExists(dir, 'go.mod')) {
206
+ signals.push('go.mod');
207
+ const goMod = readFileSafe(path.join(dir, 'go.mod'));
208
+ if (goMod && goMod.includes('github.com/gin-gonic/gin')) {
209
+ signals.push('go.mod → gin-gonic/gin');
210
+ }
211
+ }
212
+ if (fileExists(dir, 'go.sum')) signals.push('go.sum');
213
+ return signals.includes('go.mod → gin-gonic/gin') ? signals : null;
214
+ }
215
+ },
216
+ {
217
+ name: 'Go',
218
+ language: 'go',
219
+ framework: 'Go',
220
+ frameworkKey: null,
221
+ confidence: 'high',
222
+ check(dir) {
223
+ const signals = [];
224
+ if (fileExists(dir, 'go.mod')) signals.push('go.mod');
225
+ if (fileExists(dir, 'go.sum')) signals.push('go.sum');
226
+ if (hasFilesWithExtension(dir, '.go', 2)) signals.push('*.go files');
227
+ return signals.length >= 1 ? signals : null;
228
+ }
229
+ },
230
+
231
+ // ── Ruby ─────────────────────────────────────────────────────────────────
232
+ {
233
+ name: 'Ruby/Rails',
234
+ language: 'ruby',
235
+ framework: 'Rails',
236
+ frameworkKey: 'ruby-rails',
237
+ confidence: 'high',
238
+ check(dir) {
239
+ const signals = [];
240
+ if (fileExists(dir, 'Gemfile')) signals.push('Gemfile');
241
+ if (fileExists(dir, 'config', 'application.rb')) signals.push('config/application.rb');
242
+ if (fileExists(dir, 'config', 'routes.rb')) signals.push('config/routes.rb');
243
+ if (fileExists(dir, 'Rakefile')) signals.push('Rakefile');
244
+ const gemfile = readFileSafe(path.join(dir, 'Gemfile'));
245
+ if (gemfile && gemfile.includes("gem 'rails'")) signals.push('Gemfile → rails gem');
246
+ return (signals.includes('config/routes.rb') || signals.includes('Gemfile → rails gem')) ? signals : null;
247
+ }
248
+ },
249
+ {
250
+ name: 'Ruby',
251
+ language: 'ruby',
252
+ framework: 'Ruby',
253
+ confidence: 'medium',
254
+ check(dir) {
255
+ const signals = [];
256
+ if (fileExists(dir, 'Gemfile')) signals.push('Gemfile');
257
+ if (fileExists(dir, 'Gemfile.lock')) signals.push('Gemfile.lock');
258
+ if (hasFilesWithExtension(dir, '.rb', 2)) signals.push('*.rb files');
259
+ return signals.length >= 1 ? signals : null;
260
+ }
261
+ },
262
+
263
+ // ── Python ───────────────────────────────────────────────────────────────
264
+ {
265
+ name: 'Python/Django',
266
+ language: 'python',
267
+ framework: 'Django',
268
+ confidence: 'high',
269
+ check(dir) {
270
+ const signals = [];
271
+ if (fileExists(dir, 'manage.py')) signals.push('manage.py');
272
+ if (fileExists(dir, 'requirements.txt')) signals.push('requirements.txt');
273
+ const req = readFileSafe(path.join(dir, 'requirements.txt'));
274
+ if (req && req.toLowerCase().includes('django')) signals.push('requirements.txt → django');
275
+ return signals.includes('manage.py') ? signals : null;
276
+ }
277
+ },
278
+ {
279
+ name: 'Python/FastAPI',
280
+ language: 'python',
281
+ framework: 'FastAPI',
282
+ confidence: 'high',
283
+ check(dir) {
284
+ const signals = [];
285
+ if (fileExists(dir, 'requirements.txt')) {
286
+ signals.push('requirements.txt');
287
+ const req = readFileSafe(path.join(dir, 'requirements.txt'));
288
+ if (req && req.toLowerCase().includes('fastapi')) signals.push('requirements.txt → fastapi');
289
+ }
290
+ if (fileExists(dir, 'pyproject.toml')) {
291
+ signals.push('pyproject.toml');
292
+ const pyp = readFileSafe(path.join(dir, 'pyproject.toml'));
293
+ if (pyp && pyp.toLowerCase().includes('fastapi')) signals.push('pyproject.toml → fastapi');
294
+ }
295
+ return signals.some(s => s.includes('fastapi')) ? signals : null;
296
+ }
297
+ },
298
+ {
299
+ name: 'Python',
300
+ language: 'python',
301
+ framework: 'Python',
302
+ confidence: 'medium',
303
+ check(dir) {
304
+ const signals = [];
305
+ if (fileExists(dir, 'requirements.txt')) signals.push('requirements.txt');
306
+ if (fileExists(dir, 'pyproject.toml')) signals.push('pyproject.toml');
307
+ if (fileExists(dir, 'setup.py')) signals.push('setup.py');
308
+ if (fileExists(dir, 'Pipfile')) signals.push('Pipfile');
309
+ if (hasFilesWithExtension(dir, '.py', 2)) signals.push('*.py files');
310
+ return signals.length >= 1 ? signals : null;
311
+ }
312
+ },
313
+
314
+ // ── TypeScript / JavaScript (Next.js, Nuxt, Angular, Vue, React, etc.) ──
315
+ {
316
+ name: 'TypeScript/Next.js',
317
+ language: 'typescript',
318
+ framework: 'Next.js',
319
+ confidence: 'high',
320
+ check(dir) {
321
+ const signals = [];
322
+ if (fileExists(dir, 'package.json')) signals.push('package.json');
323
+ if (fileExists(dir, 'next.config.js') || fileExists(dir, 'next.config.ts') || fileExists(dir, 'next.config.mjs')) {
324
+ signals.push('next.config.*');
325
+ }
326
+ const pkg = readJsonSafe(path.join(dir, 'package.json'));
327
+ if (pkg && pkg.dependencies && pkg.dependencies['next']) signals.push('package.json → next');
328
+ return signals.includes('next.config.*') || signals.includes('package.json → next') ? signals : null;
329
+ }
330
+ },
331
+ {
332
+ name: 'TypeScript/Nuxt',
333
+ language: 'typescript',
334
+ framework: 'Nuxt',
335
+ confidence: 'high',
336
+ check(dir) {
337
+ const signals = [];
338
+ if (fileExists(dir, 'package.json')) signals.push('package.json');
339
+ if (fileExists(dir, 'nuxt.config.js') || fileExists(dir, 'nuxt.config.ts')) signals.push('nuxt.config.*');
340
+ const pkg = readJsonSafe(path.join(dir, 'package.json'));
341
+ if (pkg && pkg.dependencies && (pkg.dependencies['nuxt'] || pkg.dependencies['nuxt3'])) signals.push('package.json → nuxt');
342
+ return signals.includes('nuxt.config.*') || signals.includes('package.json → nuxt') ? signals : null;
343
+ }
344
+ },
345
+ {
346
+ name: 'TypeScript/Angular',
347
+ language: 'typescript',
348
+ framework: 'Angular',
349
+ confidence: 'high',
350
+ check(dir) {
351
+ const signals = [];
352
+ if (fileExists(dir, 'angular.json')) signals.push('angular.json');
353
+ if (fileExists(dir, 'package.json')) {
354
+ const pkg = readJsonSafe(path.join(dir, 'package.json'));
355
+ if (pkg && pkg.dependencies && pkg.dependencies['@angular/core']) signals.push('package.json → @angular/core');
356
+ }
357
+ return signals.length >= 1 ? signals : null;
358
+ }
359
+ },
360
+ {
361
+ name: 'TypeScript/NestJS',
362
+ language: 'typescript',
363
+ framework: 'NestJS',
364
+ confidence: 'high',
365
+ check(dir) {
366
+ const signals = [];
367
+ if (fileExists(dir, 'nest-cli.json')) signals.push('nest-cli.json');
368
+ if (fileExists(dir, 'package.json')) {
369
+ const pkg = readJsonSafe(path.join(dir, 'package.json'));
370
+ if (pkg && pkg.dependencies && pkg.dependencies['@nestjs/core']) signals.push('package.json → @nestjs/core');
371
+ }
372
+ return signals.length >= 1 ? signals : null;
373
+ }
374
+ },
375
+ {
376
+ name: 'TypeScript/React',
377
+ language: 'typescript',
378
+ framework: 'React',
379
+ confidence: 'medium',
380
+ check(dir) {
381
+ const signals = [];
382
+ if (fileExists(dir, 'package.json')) {
383
+ signals.push('package.json');
384
+ const pkg = readJsonSafe(path.join(dir, 'package.json'));
385
+ if (pkg && pkg.dependencies && pkg.dependencies['react']) signals.push('package.json → react');
386
+ if (pkg && pkg.dependencies && pkg.dependencies['typescript']) signals.push('package.json → typescript');
387
+ }
388
+ if (fileExists(dir, 'tsconfig.json')) signals.push('tsconfig.json');
389
+ return signals.includes('package.json → react') ? signals : null;
390
+ }
391
+ },
392
+ {
393
+ name: 'TypeScript',
394
+ language: 'typescript',
395
+ framework: 'TypeScript',
396
+ confidence: 'medium',
397
+ check(dir) {
398
+ const signals = [];
399
+ if (fileExists(dir, 'tsconfig.json')) signals.push('tsconfig.json');
400
+ if (fileExists(dir, 'package.json')) signals.push('package.json');
401
+ if (hasFilesWithExtension(dir, '.ts', 2)) signals.push('*.ts files');
402
+ return signals.length >= 1 ? signals : null;
403
+ }
404
+ },
405
+ ];
406
+
407
+ // ─── Helpers ────────────────────────────────────────────────────────────────
408
+
409
+ function fileExists(dir, ...parts) {
410
+ try {
411
+ return fs.existsSync(path.join(dir, ...parts));
412
+ } catch {
413
+ return false;
414
+ }
415
+ }
416
+
417
+ function dirExists(dir, ...parts) {
418
+ try {
419
+ const p = path.join(dir, ...parts);
420
+ return fs.existsSync(p) && fs.statSync(p).isDirectory();
421
+ } catch {
422
+ return false;
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Check if at least `minCount` files with the given extension exist (shallow scan + 1-level deep)
428
+ */
429
+ function hasFilesWithExtension(dir, ext, minCount = 1) {
430
+ try {
431
+ let count = 0;
432
+ const topItems = fs.readdirSync(dir);
433
+ for (const item of topItems) {
434
+ if (item.endsWith(ext)) count++;
435
+ if (count >= minCount) return true;
436
+ }
437
+ // One level deeper
438
+ for (const item of topItems) {
439
+ const subDir = path.join(dir, item);
440
+ try {
441
+ if (fs.statSync(subDir).isDirectory() && !item.startsWith('.') && item !== 'node_modules' && item !== 'vendor') {
442
+ const subItems = fs.readdirSync(subDir);
443
+ for (const sub of subItems) {
444
+ if (sub.endsWith(ext)) count++;
445
+ if (count >= minCount) return true;
446
+ }
447
+ }
448
+ } catch { /* skip */ }
449
+ }
450
+ return false;
451
+ } catch {
452
+ return false;
453
+ }
454
+ }
455
+
456
+ function readFileSafe(filePath) {
457
+ try {
458
+ return fs.readFileSync(filePath, 'utf8');
459
+ } catch {
460
+ return null;
461
+ }
462
+ }
463
+
464
+ function readJsonSafe(filePath) {
465
+ try {
466
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
467
+ } catch {
468
+ return null;
469
+ }
470
+ }
471
+
472
+ /** Check if a YAML file contains a top-level key (simple line-based check) */
473
+ function readYamlLineSafe(filePath, key) {
474
+ const content = readFileSafe(filePath);
475
+ if (!content) return null;
476
+ return content.split('\n').some(line => line.startsWith(key + ':')) ? key : null;
477
+ }
478
+
479
+ // ─── Public API ─────────────────────────────────────────────────────────────
480
+
481
+ /**
482
+ * Analyze a project directory and return the best-matched language preset.
483
+ * Returns null if no match found (caller should fall back to 'typescript').
484
+ *
485
+ * @param {string} projectDir - Absolute path to project root
486
+ * @returns {DetectionResult | null}
487
+ */
488
+ function detectProjectLanguage(projectDir) {
489
+ if (!fs.existsSync(projectDir)) return null;
490
+
491
+ for (const rule of DETECTION_RULES) {
492
+ try {
493
+ const signals = rule.check(projectDir);
494
+ if (signals && signals.length > 0) {
495
+ return {
496
+ language: rule.language,
497
+ framework: rule.framework,
498
+ frameworkKey: rule.frameworkKey || null,
499
+ confidence: rule.confidence,
500
+ reason: buildReason(rule, signals),
501
+ signals,
502
+ };
503
+ }
504
+ } catch {
505
+ // Skip broken checks silently
506
+ }
507
+ }
508
+
509
+ return null;
510
+ }
511
+
512
+ function buildReason(rule, signals) {
513
+ const sigStr = signals.slice(0, 3).join(', ');
514
+ return `${rule.framework} detected (${sigStr})`;
515
+ }
516
+
517
+ module.exports = { detectProjectLanguage };
@@ -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) {