@sun-asterisk/sunlint 1.3.36 → 1.3.38

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 (113) hide show
  1. package/cli.js +34 -0
  2. package/config/rules/enhanced-rules-registry.json +387 -98
  3. package/config/rules/rules-registry-generated.json +202 -174
  4. package/config/rules-summary.json +1 -1
  5. package/core/architecture-integration.js +115 -17
  6. package/core/cli-action-handler.js +103 -28
  7. package/core/cli-program.js +7 -2
  8. package/core/github-annotate-service.js +62 -0
  9. package/core/impact-integration.js +31 -16
  10. package/core/init-command.js +261 -0
  11. package/core/output-service.js +64 -10
  12. package/core/performance-optimizer.js +1 -1
  13. package/core/summary-report-service.js +46 -0
  14. package/core/unified-rule-registry.js +4 -3
  15. package/docs/DART_RULE_EXECUTION_FLOW.md +1 -1
  16. package/docs/REGISTRY_GENERATION_DIAGRAM.md +289 -0
  17. package/docs/REGISTRY_GENERATION_FLOW.md +486 -0
  18. package/docs/skills/CREATE_NEW_DART_RULE.md +932 -0
  19. package/engines/eslint-engine.js +6 -0
  20. package/engines/heuristic-engine.js +23 -10
  21. package/engines/impact/core/detectors/database-detector.js +1 -1
  22. package/engines/impact/core/detectors/endpoint-detector.js +1 -1
  23. package/engines/impact/core/report-generator.js +235 -73
  24. package/origin-rules/dart-en.md +4 -4
  25. package/origin-rules/security-en.md +470 -282
  26. package/package.json +1 -1
  27. package/rules/dart/D001_recommended_lint_rules/config.json +134 -0
  28. package/rules/index.js +6 -4
  29. package/rules/security/S001_backend_auth_communications/dart/analyzer.js +44 -0
  30. package/rules/security/S001_backend_auth_communications/index.js +87 -0
  31. package/rules/security/S001_backend_auth_communications/typescript/analyzer.js +164 -0
  32. package/rules/security/S002_os_command_injection/dart/analyzer.js +44 -0
  33. package/rules/security/S002_os_command_injection/index.js +87 -0
  34. package/rules/security/S002_os_command_injection/typescript/analyzer.js +194 -0
  35. package/rules/security/S008_svg_content_validation/dart/analyzer.js +44 -0
  36. package/rules/security/S008_svg_content_validation/index.js +87 -0
  37. package/rules/security/S008_svg_content_validation/typescript/analyzer.js +216 -0
  38. package/rules/security/S018_no_sensitive_browser_storage/dart/analyzer.js +44 -0
  39. package/rules/security/S018_no_sensitive_browser_storage/index.js +86 -0
  40. package/rules/security/S018_no_sensitive_browser_storage/typescript/analyzer.js +193 -0
  41. package/rules/security/S021_referrer_policy/dart/analyzer.js +44 -0
  42. package/rules/security/S021_referrer_policy/index.js +86 -0
  43. package/rules/security/S021_referrer_policy/typescript/analyzer.js +183 -0
  44. package/rules/security/S023_no_json_injection/config.json +133 -44
  45. package/rules/security/S023_no_json_injection/dart/analyzer.js +7 -6
  46. package/rules/security/S023_no_json_injection/typescript/analyzer.js +402 -126
  47. package/rules/security/S023_no_json_injection/typescript/ast-analyzer.js +571 -154
  48. package/rules/security/S026_tls_all_connections/config.json +30 -0
  49. package/rules/security/S026_tls_all_connections/typescript/analyzer.js +339 -0
  50. package/rules/security/S027_mtls_certificate_validation/config.json +30 -0
  51. package/rules/security/S027_mtls_certificate_validation/typescript/analyzer.js +225 -0
  52. package/rules/security/S035_separate_app_hostnames/config.json +28 -0
  53. package/rules/security/S035_separate_app_hostnames/typescript/analyzer.js +186 -0
  54. package/rules/security/S036_lfi_rfi_protection/config.json +2 -2
  55. package/rules/security/S039_tls_certificate_validation/config.json +29 -0
  56. package/rules/security/S039_tls_certificate_validation/typescript/analyzer.js +229 -0
  57. package/rules/security/S046_jwt_algorithm_allowlist/config.json +28 -0
  58. package/rules/security/S046_jwt_algorithm_allowlist/dart/analyzer.js +44 -0
  59. package/rules/security/S046_jwt_algorithm_allowlist/index.js +87 -0
  60. package/rules/security/S046_jwt_algorithm_allowlist/typescript/analyzer.js +235 -0
  61. package/rules/security/S047_oauth_pkce_protection/config.json +31 -0
  62. package/rules/security/S047_oauth_pkce_protection/dart/analyzer.js +44 -0
  63. package/rules/security/S047_oauth_pkce_protection/index.js +86 -0
  64. package/rules/security/S047_oauth_pkce_protection/typescript/analyzer.js +78 -0
  65. package/rules/security/S048_oauth_redirect_uri_validation/config.json +30 -0
  66. package/rules/security/S048_oauth_redirect_uri_validation/typescript/analyzer.js +278 -0
  67. package/rules/security/S049_short_validity_tokens/typescript/config.json +10 -3
  68. package/rules/security/S050_reference_tokens_entropy/config.json +28 -0
  69. package/rules/security/S050_reference_tokens_entropy/dart/analyzer.js +45 -0
  70. package/rules/security/S050_reference_tokens_entropy/index.js +86 -0
  71. package/rules/security/S050_reference_tokens_entropy/typescript/analyzer.js +74 -0
  72. package/rules/security/S053_generic_error_messages/config.json +28 -0
  73. package/rules/security/S053_generic_error_messages/dart/analyzer.js +45 -0
  74. package/rules/security/S053_generic_error_messages/index.js +86 -0
  75. package/rules/security/S053_generic_error_messages/typescript/analyzer.js +80 -0
  76. package/rules/security/S055_content_type_validation/typescript/symbol-based-analyzer.js +64 -2
  77. package/rules/security/S059_disable_debug_mode/config.json +28 -0
  78. package/rules/security/S059_disable_debug_mode/dart/analyzer.js +45 -0
  79. package/rules/security/S059_disable_debug_mode/index.js +86 -0
  80. package/rules/security/S059_disable_debug_mode/typescript/analyzer.js +85 -0
  81. package/rules/security/S060_password_minimum_length/config.json +28 -0
  82. package/rules/security/S060_password_minimum_length/dart/analyzer.js +45 -0
  83. package/rules/security/S060_password_minimum_length/index.js +86 -0
  84. package/rules/security/S060_password_minimum_length/typescript/analyzer.js +78 -0
  85. package/rules/security/S026_json_schema_validation/config.json +0 -27
  86. package/rules/security/S026_json_schema_validation/typescript/analyzer.js +0 -251
  87. package/rules/security/S027_no_hardcoded_secrets/config.json +0 -29
  88. package/rules/security/S027_no_hardcoded_secrets/typescript/analyzer.js +0 -309
  89. package/rules/security/S027_no_hardcoded_secrets/typescript/categories.json +0 -153
  90. package/rules/security/S035_path_session_cookies/config.json +0 -99
  91. package/rules/security/S035_path_session_cookies/typescript/analyzer.js +0 -316
  92. package/rules/security/S035_path_session_cookies/typescript/regex-based-analyzer.js +0 -724
  93. package/rules/security/S035_path_session_cookies/typescript/symbol-based-analyzer.js +0 -373
  94. package/rules/security/S039_no_session_tokens_in_url/config.json +0 -92
  95. package/rules/security/S039_no_session_tokens_in_url/typescript/analyzer.js +0 -262
  96. package/rules/security/S039_no_session_tokens_in_url/typescript/regex-based-analyzer.js +0 -337
  97. package/rules/security/S039_no_session_tokens_in_url/typescript/symbol-based-analyzer.js +0 -443
  98. package/rules/security/S048_no_current_password_in_reset/config.json +0 -48
  99. package/rules/security/S048_no_current_password_in_reset/typescript/analyzer.js +0 -366
  100. /package/rules/security/{S026_json_schema_validation → S026_tls_all_connections}/dart/analyzer.js +0 -0
  101. /package/rules/security/{S026_json_schema_validation → S026_tls_all_connections}/index.js +0 -0
  102. /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/dart/analyzer.js +0 -0
  103. /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/index.js +0 -0
  104. /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/typescript/categorized-analyzer.js +0 -0
  105. /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/dart/analyzer.js +0 -0
  106. /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/index.js +0 -0
  107. /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/typescript/README.md +0 -0
  108. /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/dart/analyzer.js +0 -0
  109. /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/index.js +0 -0
  110. /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/typescript/README.md +0 -0
  111. /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/dart/analyzer.js +0 -0
  112. /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/index.js +0 -0
  113. /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/typescript/README.md +0 -0
@@ -0,0 +1,261 @@
1
+ /**
2
+ * SunLint Init Command
3
+ * Initializes a project with SunLint code quality skill and AGENTS.md
4
+ * Following Rule C005: Single responsibility - only handle init logic
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const chalk = require('chalk');
10
+
11
+ // Path to the bundled skill directory (relative to this file in the package)
12
+ const SKILL_SOURCE_DIR = path.join(__dirname, '..', 'skill-assets', 'sunlint-code-quality');
13
+ const AGENTS_MD_SOURCE = path.join(SKILL_SOURCE_DIR, 'AGENTS.md');
14
+
15
+ /**
16
+ * AI Tool configuration mapping
17
+ * Each tool has its own skill folder path convention
18
+ */
19
+ const AI_TOOL_CONFIG = {
20
+ 'antigravity': {
21
+ name: 'Google Antigravity',
22
+ skillPath: '.agent/skills',
23
+ description: 'Google Gemini Agent (.agent/skills/)'
24
+ },
25
+ 'cursor': {
26
+ name: 'Cursor AI',
27
+ skillPath: '.cursor/skills',
28
+ description: 'Cursor AI Agent (.cursor/skills/)'
29
+ },
30
+ 'claude': {
31
+ name: 'Claude',
32
+ skillPath: '.claude/skills',
33
+ description: 'Anthropic Claude Agent (.claude/skills/)'
34
+ },
35
+ 'github-copilot': {
36
+ name: 'GitHub Copilot',
37
+ skillPath: '.github/skills',
38
+ description: 'GitHub Copilot Agent (.github/skills/)'
39
+ }
40
+ };
41
+
42
+ // Default tool if not specified
43
+ const DEFAULT_TOOL = 'antigravity';
44
+
45
+ /**
46
+ * Get available tools list for help text
47
+ */
48
+ function getAvailableToolsHelp() {
49
+ return Object.entries(AI_TOOL_CONFIG)
50
+ .map(([key, config]) => ` ${key.padEnd(16)} - ${config.description}`)
51
+ .join('\n');
52
+ }
53
+
54
+ /**
55
+ * Initialize a project with SunLint skill and AGENTS.md
56
+ * @param {string} targetDir - The target project directory
57
+ * @param {object} options - Command options
58
+ */
59
+ async function initProject(targetDir, options = {}) {
60
+ const resolvedTargetDir = path.resolve(targetDir);
61
+ const tool = options.tool || DEFAULT_TOOL;
62
+
63
+ // Validate tool option
64
+ if (!AI_TOOL_CONFIG[tool]) {
65
+ console.error(chalk.red(`\n❌ Unknown tool: ${tool}`));
66
+ console.log(chalk.yellow('\nAvailable tools:'));
67
+ console.log(chalk.gray(getAvailableToolsHelp()));
68
+ throw new Error(`Unknown tool: ${tool}. Use one of: ${Object.keys(AI_TOOL_CONFIG).join(', ')}`);
69
+ }
70
+
71
+ const toolConfig = AI_TOOL_CONFIG[tool];
72
+
73
+ console.log(chalk.cyan('\n☀️ SunLint Init - Setting up code quality standards...\n'));
74
+ console.log(chalk.blue(` 🤖 Target AI Tool: ${toolConfig.name}`));
75
+
76
+ // Validate target directory exists
77
+ if (!fs.existsSync(resolvedTargetDir)) {
78
+ throw new Error(`Target directory does not exist: ${resolvedTargetDir}`);
79
+ }
80
+
81
+ const skillTargetDir = path.join(resolvedTargetDir, toolConfig.skillPath, 'sunlint-code-quality');
82
+ const agentsTargetPath = path.join(resolvedTargetDir, 'AGENTS.md');
83
+
84
+ // Step 1: Copy skill folder
85
+ await copySkillFolder(skillTargetDir, options, toolConfig);
86
+
87
+ // Step 2: Append/Create AGENTS.md
88
+ await setupAgentsMd(agentsTargetPath, options);
89
+
90
+ console.log(chalk.green('\n✅ SunLint initialization complete!\n'));
91
+ console.log(chalk.white(' Files created/updated:'));
92
+ console.log(chalk.gray(` • ${path.relative(resolvedTargetDir, skillTargetDir)}/`));
93
+ console.log(chalk.gray(` • AGENTS.md`));
94
+ console.log(chalk.cyan(`\n📖 ${toolConfig.name} will now follow SunLint code quality standards.\n`));
95
+ }
96
+
97
+ /**
98
+ * Copy the skill folder to target location
99
+ * @param {string} targetPath - Target path for the skill folder
100
+ * @param {object} options - Command options
101
+ * @param {object} toolConfig - AI tool configuration
102
+ */
103
+ async function copySkillFolder(targetPath, options = {}, toolConfig = {}) {
104
+ const force = options.force || false;
105
+
106
+ // Check if skill folder already exists
107
+ if (fs.existsSync(targetPath)) {
108
+ if (!force) {
109
+ console.log(chalk.yellow(` ⚠️ Skill folder already exists: ${targetPath}`));
110
+ console.log(chalk.yellow(' Use --force to overwrite.'));
111
+ return;
112
+ }
113
+ console.log(chalk.yellow(` 🔄 Overwriting existing skill folder...`));
114
+ fs.rmSync(targetPath, { recursive: true });
115
+ }
116
+
117
+ // Create parent directories if they don't exist
118
+ const parentDir = path.dirname(targetPath);
119
+ if (!fs.existsSync(parentDir)) {
120
+ fs.mkdirSync(parentDir, { recursive: true });
121
+ }
122
+
123
+ const language = options.language || 'typescript';
124
+
125
+ // Copy skill folder usage files (root)
126
+ const relativePath = toolConfig.skillPath || '.agent/skills';
127
+ console.log(chalk.blue(` 📁 Copying skill folder to ${relativePath}/sunlint-code-quality/`));
128
+
129
+ // 1. Copy root files (SKILL.md, AGENTS.md, etc) - Non-recursive
130
+ fs.mkdirSync(targetPath, { recursive: true });
131
+
132
+ // Copy root files
133
+ if (fs.existsSync(SKILL_SOURCE_DIR)) {
134
+ const items = fs.readdirSync(SKILL_SOURCE_DIR);
135
+ for (const item of items) {
136
+ const sourcePath = path.join(SKILL_SOURCE_DIR, item);
137
+ const targetItemPath = path.join(targetPath, item);
138
+ const stat = fs.statSync(sourcePath);
139
+
140
+ if (!stat.isDirectory()) {
141
+ fs.copyFileSync(sourcePath, targetItemPath);
142
+ }
143
+ }
144
+ }
145
+
146
+ // 2. Copy Language Rules to rules/ folder
147
+ const rulesSourceDir = path.join(SKILL_SOURCE_DIR, 'rules', language);
148
+ const rulesTargetDir = path.join(targetPath, 'rules');
149
+
150
+ if (fs.existsSync(rulesSourceDir)) {
151
+ console.log(chalk.blue(` 📝 Installing ${language} rules...`));
152
+ copyDirectoryRecursive(rulesSourceDir, rulesTargetDir);
153
+ } else {
154
+ console.warn(chalk.yellow(` ⚠️ Language '${language}' rules not found. Falling back to typescript rules.`));
155
+ const fallbackDir = path.join(SKILL_SOURCE_DIR, 'rules', 'typescript');
156
+ if (fs.existsSync(fallbackDir)) {
157
+ copyDirectoryRecursive(fallbackDir, rulesTargetDir);
158
+ }
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Recursively copy a directory
164
+ * @param {string} source - Source directory
165
+ * @param {string} target - Target directory
166
+ */
167
+ function copyDirectoryRecursive(source, target) {
168
+ if (!fs.existsSync(source)) {
169
+ // throw new Error(`Source directory not found: ${source}`);
170
+ return;
171
+ }
172
+
173
+ fs.mkdirSync(target, { recursive: true });
174
+
175
+ const items = fs.readdirSync(source);
176
+
177
+ for (const item of items) {
178
+ const sourcePath = path.join(source, item);
179
+ const targetPath = path.join(target, item);
180
+
181
+ const stat = fs.statSync(sourcePath);
182
+
183
+ if (stat.isDirectory()) {
184
+ copyDirectoryRecursive(sourcePath, targetPath);
185
+ } else {
186
+ fs.copyFileSync(sourcePath, targetPath);
187
+ }
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Setup AGENTS.md - append or create
193
+ * @param {string} targetPath - Target path for AGENTS.md
194
+ * @param {object} options - Command options
195
+ */
196
+ async function setupAgentsMd(targetPath, options = {}) {
197
+ const force = options.force || false;
198
+
199
+ // Read the source AGENTS.md content
200
+ if (!fs.existsSync(AGENTS_MD_SOURCE)) {
201
+ throw new Error(`Source AGENTS.md not found: ${AGENTS_MD_SOURCE}`);
202
+ }
203
+
204
+ const sunlintAgentsContent = fs.readFileSync(AGENTS_MD_SOURCE, 'utf-8');
205
+ const separator = '\n\n---\n\n<!-- SunLint Code Quality Standards - Auto-generated by sunlint init -->\n\n';
206
+
207
+ if (fs.existsSync(targetPath)) {
208
+ // Check if already contains SunLint content
209
+ const existingContent = fs.readFileSync(targetPath, 'utf-8');
210
+
211
+ if (existingContent.includes('SunLint Code Quality & Security Standards')) {
212
+ if (!force) {
213
+ console.log(chalk.yellow(` ⚠️ AGENTS.md already contains SunLint standards.`));
214
+ console.log(chalk.yellow(' Use --force to replace.'));
215
+ return;
216
+ }
217
+ // Remove existing SunLint section and append new one
218
+ console.log(chalk.yellow(` 🔄 Updating SunLint section in AGENTS.md...`));
219
+ const cleanedContent = removeSunlintSection(existingContent);
220
+ fs.writeFileSync(targetPath, cleanedContent + separator + sunlintAgentsContent, 'utf-8');
221
+ } else {
222
+ // Append to existing file
223
+ console.log(chalk.blue(` 📝 Appending SunLint standards to existing AGENTS.md`));
224
+ fs.appendFileSync(targetPath, separator + sunlintAgentsContent, 'utf-8');
225
+ }
226
+ } else {
227
+ // Create new AGENTS.md
228
+ console.log(chalk.blue(` 📝 Creating new AGENTS.md with SunLint standards`));
229
+ fs.writeFileSync(targetPath, sunlintAgentsContent, 'utf-8');
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Remove existing SunLint section from content
235
+ * @param {string} content - Existing file content
236
+ * @returns {string} - Content without SunLint section
237
+ */
238
+ function removeSunlintSection(content) {
239
+ // Find and remove the SunLint section (from marker to end or next major section)
240
+ const sunlintMarkerStart = content.indexOf('<!-- SunLint Code Quality Standards');
241
+ if (sunlintMarkerStart === -1) {
242
+ // Try alternative marker
243
+ const altMarkerStart = content.indexOf('# 🦾 SunLint Code Quality');
244
+ if (altMarkerStart === -1) {
245
+ return content;
246
+ }
247
+ // Remove from this point to end
248
+ return content.substring(0, altMarkerStart).trim();
249
+ }
250
+
251
+ return content.substring(0, sunlintMarkerStart).trim();
252
+ }
253
+
254
+ module.exports = {
255
+ initProject,
256
+ copySkillFolder,
257
+ setupAgentsMd,
258
+ AI_TOOL_CONFIG,
259
+ DEFAULT_TOOL,
260
+ getAvailableToolsHelp,
261
+ };
@@ -242,7 +242,8 @@ class OutputService {
242
242
  cwd: process.cwd(),
243
243
  filesAnalyzed: totalFiles,
244
244
  duration: metadata.duration,
245
- version: metadata.version || this.version
245
+ version: metadata.version || this.version,
246
+ architecture: results.architecture || null
246
247
  }
247
248
  );
248
249
 
@@ -380,16 +381,22 @@ class OutputService {
380
381
  if (index > 0) {
381
382
  output += '\n';
382
383
  }
383
- output += `\n${chalk.underline(path.resolve(file))}\n`;
384
384
 
385
- sortedViolations.forEach(violation => {
385
+ sortedViolations.forEach((violation, index) => {
386
+ const isDuplicateCodeRule = violation.ruleId === 'C002';
387
+ if (index === 0 && !isDuplicateCodeRule) {
388
+ output += `\n${chalk.underline(path.resolve(file))}\n`;
389
+ }
386
390
  // Support both location.start.line (new format) and line (legacy format)
387
391
  const line = (violation.location?.start?.line || violation.line || 1).toString();
388
392
  const column = (violation.location?.start?.column || violation.column || 1).toString();
389
- const severityText = violation.severity === 'error' ? 'error' : 'warning';
393
+ const severityText = violation.severity === 'error' ? 'Error' : 'Warning';
390
394
  const severityColor = violation.severity === 'error' ? chalk.red : chalk.yellow;
391
-
392
- output += ` ${chalk.dim(`${line}:${column}`)} ${severityColor(severityText)} ${violation.message} ${chalk.gray(violation.ruleId)}\n`;
395
+ if (isDuplicateCodeRule) {
396
+ output += `${severityColor(severityText)} ${chalk.white(` ${chalk.underline(path.resolve(file))}:${line}:${column} ${violation.message} ${chalk.yellow(`(Confidence: ${violation.confidence}%)`)}`)} ${chalk.gray(violation.ruleId)}\n`;
397
+ } else {
398
+ output += ` ${chalk.blue(`${line}:${column}`)} ${severityColor(severityText)} ${violation.message} ${chalk.gray(violation.ruleId)}\n`;
399
+ }
393
400
  });
394
401
  });
395
402
 
@@ -431,10 +438,39 @@ class OutputService {
431
438
  const jsonResults = [];
432
439
  const fileGroups = {};
433
440
 
441
+ // Combine code quality violations with architecture violations
442
+ const combinedViolations = [...allViolations];
443
+
444
+ // Include architecture violations if available
445
+ if (results.architecture && results.architecture.violations) {
446
+ const cwd = process.cwd();
447
+ for (const archViolation of results.architecture.violations) {
448
+ let file = archViolation.file || 'unknown';
449
+
450
+ // Convert absolute path to relative path
451
+ if (file !== 'unknown' && path.isAbsolute(file)) {
452
+ if (file.startsWith(cwd)) {
453
+ file = path.relative(cwd, file);
454
+ }
455
+ }
456
+
457
+ combinedViolations.push({
458
+ file: file,
459
+ ruleId: archViolation.ruleId,
460
+ severity: archViolation.severity,
461
+ message: archViolation.message,
462
+ line: archViolation.line || 1,
463
+ column: archViolation.column || 1,
464
+ category: 'architecture',
465
+ source: 'architecture-detection'
466
+ });
467
+ }
468
+ }
469
+
434
470
  // Group violations by file
435
- allViolations.forEach(violation => {
471
+ combinedViolations.forEach(violation => {
436
472
  let file = violation.file || violation.filePath || 'unknown';
437
-
473
+
438
474
  // Convert absolute path to relative path for better display
439
475
  if (file !== 'unknown' && path.isAbsolute(file)) {
440
476
  const cwd = process.cwd();
@@ -442,7 +478,7 @@ class OutputService {
442
478
  file = path.relative(cwd, file);
443
479
  }
444
480
  }
445
-
481
+
446
482
  if (!fileGroups[file]) {
447
483
  fileGroups[file] = [];
448
484
  }
@@ -460,7 +496,8 @@ class OutputService {
460
496
  nodeType: violation.nodeType || null,
461
497
  messageId: violation.messageId || null,
462
498
  endLine: violation.endLine || null,
463
- endColumn: violation.endColumn || null
499
+ endColumn: violation.endColumn || null,
500
+ category: violation.category || null // Include category for architecture violations
464
501
  }));
465
502
 
466
503
  jsonResults.push({
@@ -495,6 +532,23 @@ class OutputService {
495
532
  });
496
533
  }
497
534
 
535
+ // Add architecture metadata as a special entry (maintains ESLint compatibility)
536
+ if (results.architecture && results.architecture.summary) {
537
+ jsonResults.push({
538
+ filePath: '__sunlint_architecture__',
539
+ _type: 'architecture-metadata',
540
+ _architectureSummary: results.architecture.summary,
541
+ messages: [],
542
+ suppressedMessages: [],
543
+ errorCount: 0,
544
+ warningCount: 0,
545
+ fatalErrorCount: 0,
546
+ fixableErrorCount: 0,
547
+ fixableWarningCount: 0,
548
+ source: null
549
+ });
550
+ }
551
+
498
552
  return jsonResults;
499
553
  }
500
554
 
@@ -112,7 +112,7 @@ class PerformanceOptimizer {
112
112
  break;
113
113
  }
114
114
 
115
- // Check file count limit (skip if unlimited -1)
115
+ // Check file count limit (skip if unlimited: 0)
116
116
  if (this.fileSizeLimits.maxTotalFiles > 0 && filtered.length >= this.fileSizeLimits.maxTotalFiles) {
117
117
  if (this.config.verbose) {
118
118
  console.log(`⚠️ Reached file count limit: ${this.fileSizeLimits.maxTotalFiles} files`);
@@ -268,6 +268,52 @@ class SummaryReportService {
268
268
  }
269
269
  };
270
270
 
271
+ // Add architecture analysis if available
272
+ if (options.architecture && options.architecture.summary) {
273
+ const archSummary = options.architecture.summary;
274
+ summaryReport.architecture = {
275
+ primary_pattern: archSummary.primaryPattern || 'UNKNOWN',
276
+ primary_confidence: Math.round((archSummary.primaryConfidence || 0) * 100),
277
+ health_score: Math.round(archSummary.healthScore || 0),
278
+ violation_count: archSummary.violationCount || 0,
279
+ rules_checked: archSummary.rulesChecked || 0,
280
+ rules_passed: archSummary.rulesPassed || 0,
281
+ rules_failed: archSummary.rulesFailed || 0,
282
+ is_hybrid: archSummary.isHybrid || false,
283
+ combination: archSummary.combination || null,
284
+ secondary_patterns: (archSummary.secondaryPatterns || []).map(p => ({
285
+ pattern: p.pattern,
286
+ confidence: Math.round((p.confidence || 0) * 100)
287
+ })),
288
+ analysis_time_ms: archSummary.analysisTime || 0
289
+ };
290
+
291
+ // Add all rules with score > 0 (passed + failed) to report
292
+ if (options.architecture.allRules && options.architecture.allRules.length > 0) {
293
+ summaryReport.architecture.rules = options.architecture.allRules
294
+ .filter(r => r.score > 0) // Only include rules with evidence (score > 0)
295
+ .map(r => ({
296
+ rule_code: r.ruleId,
297
+ rule_name: r.ruleName,
298
+ score: r.score,
299
+ status: r.status,
300
+ passed: r.passed
301
+ }));
302
+ }
303
+
304
+ // Add architecture violations to violations summary (only score > 0)
305
+ if (options.architecture.violations && options.architecture.violations.length > 0) {
306
+ summaryReport.architecture.violations = options.architecture.violations.map(v => ({
307
+ rule_code: v.ruleId,
308
+ score: v.score || 0,
309
+ severity: v.severity,
310
+ message: v.message,
311
+ file: v.file,
312
+ line: v.line || 1
313
+ }));
314
+ }
315
+ }
316
+
271
317
  return summaryReport;
272
318
  }
273
319
 
@@ -207,8 +207,8 @@ class UnifiedRuleRegistry {
207
207
  }
208
208
  }
209
209
 
210
- // Also check other category directories (security, typescript, etc.)
211
- const otherDirs = ['security', 'typescript', 'react'];
210
+ // Also check other category directories (security, typescript, dart, etc.)
211
+ const otherDirs = ['security', 'typescript', 'dart', 'react'];
212
212
  for (const categoryDir of otherDirs) {
213
213
  const categoryPath = path.join(rulesBaseDir, categoryDir);
214
214
 
@@ -310,7 +310,8 @@ class UnifiedRuleRegistry {
310
310
  */
311
311
  registerEngineCapabilities() {
312
312
  // Define what each engine can handle
313
- this.engineCapabilities.set('heuristic', ['semantic', 'ast', 'regex']);
313
+ // 'heuristic' strategy type added to support rules that use heuristic-based detection
314
+ this.engineCapabilities.set('heuristic', ['semantic', 'ast', 'regex', 'heuristic']);
314
315
  this.engineCapabilities.set('eslint', ['ast', 'regex']);
315
316
  this.engineCapabilities.set('openai', ['semantic']);
316
317
 
@@ -66,7 +66,7 @@ node cli.js --rule=C002 \
66
66
  // Line 27-51
67
67
  function detectDartSupport(ruleId) {
68
68
  const rulesBasePath = path.join(__dirname, '../rules');
69
- const categories = ['common', 'security', 'typescript'];
69
+ const categories = ['common', 'security', 'typescript', 'dart'];
70
70
 
71
71
  for (const category of categories) {
72
72
  const categoryPath = path.join(rulesBasePath, category);