@sun-asterisk/sunlint 1.3.34 → 1.3.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/core/architecture-integration.js +16 -7
  2. package/core/auto-performance-manager.js +1 -1
  3. package/core/cli-action-handler.js +92 -2
  4. package/core/cli-program.js +96 -138
  5. package/core/file-targeting-service.js +62 -4
  6. package/core/git-utils.js +19 -12
  7. package/core/github-annotate-service.js +326 -11
  8. package/core/html-report-generator.js +326 -731
  9. package/core/impact-integration.js +433 -0
  10. package/core/output-service.js +293 -21
  11. package/core/scoring-service.js +3 -2
  12. package/engines/arch-detect/core/analyzer.js +413 -0
  13. package/engines/arch-detect/core/index.js +22 -0
  14. package/engines/arch-detect/engine/hybrid-detector.js +176 -0
  15. package/engines/arch-detect/engine/index.js +24 -0
  16. package/engines/arch-detect/engine/rule-executor.js +228 -0
  17. package/engines/arch-detect/engine/score-calculator.js +214 -0
  18. package/engines/arch-detect/engine/violation-detector.js +616 -0
  19. package/engines/arch-detect/index.js +50 -0
  20. package/engines/arch-detect/rules/base-rule.js +187 -0
  21. package/engines/arch-detect/rules/index.js +35 -0
  22. package/engines/arch-detect/rules/layered/index.js +28 -0
  23. package/engines/arch-detect/rules/layered/l001-presentation-layer.js +237 -0
  24. package/engines/arch-detect/rules/layered/l002-business-layer.js +215 -0
  25. package/engines/arch-detect/rules/layered/l003-data-layer.js +229 -0
  26. package/engines/arch-detect/rules/layered/l004-model-layer.js +204 -0
  27. package/engines/arch-detect/rules/layered/l005-layer-separation.js +215 -0
  28. package/engines/arch-detect/rules/layered/l006-dependency-direction.js +221 -0
  29. package/engines/arch-detect/rules/layered/layered-rules-collection.js +445 -0
  30. package/engines/arch-detect/rules/modular/index.js +27 -0
  31. package/engines/arch-detect/rules/modular/m001-feature-modules.js +238 -0
  32. package/engines/arch-detect/rules/modular/m002-core-module.js +169 -0
  33. package/engines/arch-detect/rules/modular/m003-module-declaration.js +186 -0
  34. package/engines/arch-detect/rules/modular/m004-public-api.js +171 -0
  35. package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +220 -0
  36. package/engines/arch-detect/rules/modular/modular-rules-collection.js +357 -0
  37. package/engines/arch-detect/rules/presentation/index.js +27 -0
  38. package/engines/arch-detect/rules/presentation/pr001-view-layer.js +221 -0
  39. package/engines/arch-detect/rules/presentation/pr002-presentation-logic.js +192 -0
  40. package/engines/arch-detect/rules/presentation/pr004-data-binding.js +187 -0
  41. package/engines/arch-detect/rules/presentation/pr006-router-layer.js +185 -0
  42. package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +181 -0
  43. package/engines/arch-detect/rules/presentation/presentation-rules-collection.js +507 -0
  44. package/engines/arch-detect/rules/project-scanner/index.js +31 -0
  45. package/engines/arch-detect/rules/project-scanner/ps001-project-root.js +213 -0
  46. package/engines/arch-detect/rules/project-scanner/ps002-language-detection.js +192 -0
  47. package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +339 -0
  48. package/engines/arch-detect/rules/project-scanner/ps004-build-system.js +171 -0
  49. package/engines/arch-detect/rules/project-scanner/ps005-source-directory.js +163 -0
  50. package/engines/arch-detect/rules/project-scanner/ps006-test-directory.js +184 -0
  51. package/engines/arch-detect/rules/project-scanner/ps007-documentation.js +149 -0
  52. package/engines/arch-detect/rules/project-scanner/ps008-cicd-detection.js +163 -0
  53. package/engines/arch-detect/rules/project-scanner/ps009-code-quality.js +152 -0
  54. package/engines/arch-detect/rules/project-scanner/ps010-statistics.js +180 -0
  55. package/engines/arch-detect/rules/rule-registry.js +111 -0
  56. package/engines/arch-detect/types/context.types.js +60 -0
  57. package/engines/arch-detect/types/enums.js +161 -0
  58. package/engines/arch-detect/types/index.js +25 -0
  59. package/engines/arch-detect/types/result.types.js +7 -0
  60. package/engines/arch-detect/types/rule.types.js +7 -0
  61. package/engines/arch-detect/utils/file-scanner.js +411 -0
  62. package/engines/arch-detect/utils/index.js +23 -0
  63. package/engines/arch-detect/utils/pattern-matcher.js +328 -0
  64. package/engines/impact/cli.js +106 -0
  65. package/engines/impact/config/default-config.js +54 -0
  66. package/engines/impact/core/change-detector.js +258 -0
  67. package/engines/impact/core/detectors/database-detector.js +1317 -0
  68. package/engines/impact/core/detectors/endpoint-detector.js +55 -0
  69. package/engines/impact/core/impact-analyzer.js +124 -0
  70. package/engines/impact/core/report-generator.js +462 -0
  71. package/engines/impact/core/utils/ast-parser.js +241 -0
  72. package/engines/impact/core/utils/dependency-graph.js +159 -0
  73. package/engines/impact/core/utils/file-utils.js +116 -0
  74. package/engines/impact/core/utils/git-utils.js +203 -0
  75. package/engines/impact/core/utils/logger.js +13 -0
  76. package/engines/impact/core/utils/method-call-graph.js +1192 -0
  77. package/engines/impact/index.js +135 -0
  78. package/engines/impact/package.json +29 -0
  79. package/package.json +18 -43
  80. package/scripts/build-release.sh +0 -0
  81. package/scripts/copy-impact-analyzer.js +135 -0
  82. package/scripts/install.sh +0 -0
  83. package/scripts/manual-release.sh +0 -0
  84. package/scripts/pre-release-test.sh +0 -0
  85. package/scripts/prepare-release.sh +0 -0
  86. package/scripts/quick-performance-test.js +0 -0
  87. package/scripts/setup-github-registry.sh +0 -0
  88. package/scripts/trigger-release.sh +0 -0
  89. package/scripts/verify-install.sh +0 -0
  90. package/templates/combined-report.html +1418 -0
@@ -71,9 +71,10 @@ function sleep(ms) {
71
71
  * @returns {Promise<string|null>} AI-generated summary or null
72
72
  */
73
73
  async function generateAISummary(violations, stats) {
74
- const token = process.env.GITHUB_TOKEN;
74
+ // Prefer GH_MODELS_TOKEN (PAT with Models access) over GITHUB_TOKEN
75
+ const token = process.env.GH_MODELS_TOKEN || process.env.GITHUB_TOKEN;
75
76
  if (!token) {
76
- logger.debug('No GITHUB_TOKEN, skipping AI summary');
77
+ logger.debug('No GitHub token available, skipping AI summary');
77
78
  return null;
78
79
  }
79
80
 
@@ -96,7 +97,7 @@ async function generateAISummary(violations, stats) {
96
97
  ruleGroups[v.rule] = (ruleGroups[v.rule] || 0) + 1;
97
98
  }
98
99
 
99
- const prompt = `You are a code review assistant. Analyze these code quality violations and provide a brief, actionable summary in 2-3 sentences.
100
+ const prompt = `You are a code review assistant. Analyze these code quality violations and provide a verdict and brief summary.
100
101
 
101
102
  Violations by rule:
102
103
  ${Object.entries(ruleGroups).map(([rule, count]) => `- ${rule}: ${count} issues`).join('\n')}
@@ -106,10 +107,19 @@ ${topViolations.slice(0, 5).map(v => `- [${v.rule}] ${v.file}: ${v.message}`).jo
106
107
 
107
108
  Stats: ${stats.errorCount} errors, ${stats.warningCount} warnings in ${stats.filesWithIssues} files.
108
109
 
109
- Provide a concise summary focusing on:
110
- 1. Main patterns/issues found
111
- 2. Priority areas to fix
112
- Keep it under 100 words, no markdown headers.`;
110
+ Your response MUST start with one of these verdicts on its own line:
111
+ - "🚫 REQUIRES FIXES" - if there are errors or critical issues that must be fixed before merging
112
+ - "⚠️ NEEDS ATTENTION" - if there are warnings that should be reviewed but not blocking
113
+ - "✅ READY TO MERGE" - if issues are minor or acceptable
114
+
115
+ Then provide 2-3 sentences about:
116
+ 1. Main issues found
117
+ 2. Priority areas to address
118
+
119
+ Keep it under 120 words total.`;
120
+
121
+ const tokenSource = process.env.GH_MODELS_TOKEN ? 'GH_MODELS_TOKEN' : 'GITHUB_TOKEN';
122
+ logger.info(`Generating AI summary via GitHub Models API (using ${tokenSource})...`);
113
123
 
114
124
  const response = await fetch('https://models.inference.ai.azure.com/chat/completions', {
115
125
  method: 'POST',
@@ -126,7 +136,8 @@ Keep it under 100 words, no markdown headers.`;
126
136
  });
127
137
 
128
138
  if (!response.ok) {
129
- logger.debug(`GitHub Models API error: ${response.status}`);
139
+ const errorText = await response.text().catch(() => 'Unknown error');
140
+ logger.warn(`GitHub Models API error: ${response.status} - ${errorText}`);
130
141
  return null;
131
142
  }
132
143
 
@@ -138,13 +149,106 @@ Keep it under 100 words, no markdown headers.`;
138
149
  return aiSummary;
139
150
  }
140
151
 
152
+ logger.warn('AI summary response empty');
141
153
  return null;
142
154
  } catch (error) {
143
- logger.debug(`AI summary generation failed: ${error.message}`);
155
+ logger.warn(`AI summary generation failed: ${error.message}`);
144
156
  return null;
145
157
  }
146
158
  }
147
159
 
160
+ /**
161
+ * Format AI summary for better readability
162
+ * @param {string} summary - Raw AI summary text
163
+ * @returns {string} Formatted summary with highlights
164
+ */
165
+ function formatAISummary(summary) {
166
+ if (!summary) return '';
167
+
168
+ let formatted = summary;
169
+ let verdictLine = '';
170
+
171
+ // Extract verdict from first line
172
+ const lines = formatted.split('\n');
173
+ const firstLine = lines[0].trim();
174
+
175
+ if (firstLine.includes('REQUIRES FIXES') || firstLine.includes('🚫')) {
176
+ verdictLine = '> 🚫 **REQUIRES FIXES** - Critical issues must be resolved before merging\n\n';
177
+ formatted = lines.slice(1).join('\n').trim();
178
+ } else if (firstLine.includes('NEEDS ATTENTION') || firstLine.includes('⚠️')) {
179
+ verdictLine = '> ⚠️ **NEEDS ATTENTION** - Review recommended but not blocking\n\n';
180
+ formatted = lines.slice(1).join('\n').trim();
181
+ } else if (firstLine.includes('READY TO MERGE') || firstLine.includes('✅')) {
182
+ verdictLine = '> ✅ **READY TO MERGE** - Code quality meets standards\n\n';
183
+ formatted = lines.slice(1).join('\n').trim();
184
+ }
185
+
186
+ // Highlight rule IDs (C001, C019, etc.)
187
+ formatted = formatted.replace(/\b(C\d{3})\b/g, '`$1`');
188
+
189
+ // Highlight severity words
190
+ formatted = formatted.replace(/\b(critical|high|error|errors)\b/gi, '**$1**');
191
+ formatted = formatted.replace(/\b(warning|warnings|medium)\b/gi, '*$1*');
192
+
193
+ // Split into sentences and format as list if multiple sentences
194
+ const sentences = formatted.split(/(?<=[.!?])\s+/).filter(s => s.trim());
195
+
196
+ if (sentences.length > 1) {
197
+ // Format as bullet points
198
+ const mainIssues = [];
199
+ const recommendations = [];
200
+
201
+ for (const sentence of sentences) {
202
+ const lower = sentence.toLowerCase();
203
+ if (lower.includes('prioritize') || lower.includes('focus') || lower.includes('recommend') || lower.includes('should')) {
204
+ recommendations.push(sentence.trim());
205
+ } else {
206
+ mainIssues.push(sentence.trim());
207
+ }
208
+ }
209
+
210
+ let result = verdictLine;
211
+
212
+ if (mainIssues.length > 0) {
213
+ result += '**🔍 Issues Found:**\n';
214
+ for (const issue of mainIssues) {
215
+ result += `- ${issue}\n`;
216
+ }
217
+ }
218
+
219
+ if (recommendations.length > 0) {
220
+ result += '\n**💡 Recommendations:**\n';
221
+ for (const rec of recommendations) {
222
+ result += `- ${rec}\n`;
223
+ }
224
+ }
225
+
226
+ return result || verdictLine + formatted;
227
+ }
228
+
229
+ return verdictLine + (formatted ? `> ${formatted}` : '');
230
+ }
231
+
232
+ /**
233
+ * Generate a visual score bar using Unicode blocks
234
+ * @param {number} score - Score from 0-100
235
+ * @returns {string} Visual progress bar
236
+ */
237
+ function generateScoreBar(score) {
238
+ const total = 20;
239
+ const filled = Math.round((score / 100) * total);
240
+ const empty = total - filled;
241
+
242
+ // Use different colors based on score
243
+ let color;
244
+ if (score >= 80) color = '🟩';
245
+ else if (score >= 60) color = '🟨';
246
+ else if (score >= 40) color = '🟧';
247
+ else color = '🟥';
248
+
249
+ return color.repeat(filled) + '⬜'.repeat(empty);
250
+ }
251
+
148
252
  /**
149
253
  * Calculate quality score from violations
150
254
  * @param {number} errorCount - Number of errors
@@ -1030,8 +1134,8 @@ async function postSummaryComment({
1030
1134
 
1031
1135
  // AI Summary (if available)
1032
1136
  if (aiSummary) {
1033
- summary += `#### 🤖 AI Analysis\n`;
1034
- summary += `${aiSummary}\n\n`;
1137
+ summary += `#### 🧠 AI Analysis\n\n`;
1138
+ summary += `${formatAISummary(aiSummary)}\n\n`;
1035
1139
  }
1036
1140
 
1037
1141
  // Compact summary table
@@ -1142,9 +1246,220 @@ async function postSummaryComment({
1142
1246
  }
1143
1247
  }
1144
1248
 
1249
+ /**
1250
+ * Post combined summary comment on GitHub PR (Code Quality + Architecture + Impact)
1251
+ * @param {Object} options
1252
+ * @param {string} [options.githubToken] - GitHub token, falls back to GITHUB_TOKEN env
1253
+ * @param {string} options.repo - GitHub repo in format owner/repo
1254
+ * @param {number} options.prNumber - Pull request number
1255
+ * @param {Object} [options.codeQuality] - Code quality results
1256
+ * @param {Object} [options.architecture] - Architecture detection results
1257
+ * @param {Object} [options.impact] - Impact analysis results
1258
+ * @returns {Promise<Object>} Result object
1259
+ */
1260
+ async function postCombinedSummaryComment({
1261
+ githubToken,
1262
+ repo,
1263
+ prNumber,
1264
+ codeQuality = null,
1265
+ architecture = null,
1266
+ impact = null
1267
+ }) {
1268
+ const startTime = Date.now();
1269
+
1270
+ try {
1271
+ logger.info('Starting combined summary comment', { repo, prNumber });
1272
+
1273
+ // Validate basic params
1274
+ const token = githubToken || process.env.GITHUB_TOKEN;
1275
+ if (!token) {
1276
+ throw new ValidationError('githubToken is required or GITHUB_TOKEN env var must be set');
1277
+ }
1278
+ if (!repo || !repo.includes('/')) {
1279
+ throw new ValidationError('repo must be in format "owner/repo"');
1280
+ }
1281
+ if (!prNumber || prNumber <= 0) {
1282
+ throw new ValidationError('prNumber must be a positive integer');
1283
+ }
1284
+
1285
+ const [owner, repoName] = repo.split('/');
1286
+
1287
+ if (!Octokit) {
1288
+ Octokit = (await import('@octokit/rest')).Octokit;
1289
+ }
1290
+ const octokit = new Octokit({ auth: token });
1291
+
1292
+ // Build combined summary - Clean minimal style with logo
1293
+ let summary = `## <a href="https://coding-standards.sun-asterisk.vn/"><img src="https://coding-standards.sun-asterisk.vn/logo-light.svg" alt="SunLint" height="28"></a> Code Quality Analysis\n\n`;
1294
+
1295
+ // === Quality Score Section ===
1296
+ if (codeQuality) {
1297
+ const { errorCount = 0, warningCount = 0, filesWithIssues = 0, totalViolations = 0, score = {} } = codeQuality;
1298
+ const qualityScore = typeof score.value === 'number' && !isNaN(score.value) ? score.value : 0;
1299
+ const grade = score.grade || 'F';
1300
+
1301
+ // Grade emoji and description based on score
1302
+ let gradeEmoji, gradeDesc;
1303
+ if (qualityScore >= 90) {
1304
+ gradeEmoji = '🏆'; gradeDesc = 'Excellent';
1305
+ } else if (qualityScore >= 80) {
1306
+ gradeEmoji = '✨'; gradeDesc = 'Good';
1307
+ } else if (qualityScore >= 70) {
1308
+ gradeEmoji = '👍'; gradeDesc = 'Fair';
1309
+ } else if (qualityScore >= 60) {
1310
+ gradeEmoji = '⚡'; gradeDesc = 'Needs Work';
1311
+ } else {
1312
+ gradeEmoji = '🔧'; gradeDesc = 'Needs Improvement';
1313
+ }
1314
+
1315
+ // Clean score display
1316
+ summary += `### Quality Score: **${qualityScore}/100** · Grade: ${gradeEmoji} ${grade} (${gradeDesc})\n\n`;
1317
+
1318
+ // Issue summary
1319
+ if (errorCount === 0 && warningCount === 0) {
1320
+ summary += `✅ **No issues found!** Great job!\n\n`;
1321
+ } else {
1322
+ summary += `| | Count |\n`;
1323
+ summary += `|:--|--:|\n`;
1324
+ if (errorCount > 0) {
1325
+ summary += `| ❌ Errors | **${errorCount}** |\n`;
1326
+ }
1327
+ if (warningCount > 0) {
1328
+ summary += `| ⚠️ Warnings | ${warningCount} |\n`;
1329
+ }
1330
+ summary += `| 📁 Files with issues | ${filesWithIssues} |\n\n`;
1331
+ }
1332
+
1333
+ // AI Summary (if available)
1334
+ if (codeQuality.aiSummary) {
1335
+ summary += `#### 🧠 AI Analysis\n\n`;
1336
+ summary += `${formatAISummary(codeQuality.aiSummary)}\n\n`;
1337
+ }
1338
+ }
1339
+
1340
+ // === Additional Metrics (Direct display) ===
1341
+ if (architecture || impact) {
1342
+ // Architecture section
1343
+ if (architecture) {
1344
+ const { pattern, confidence, healthScore, violations = [] } = architecture;
1345
+ const patternDisplay = pattern ? pattern.toUpperCase() : 'UNKNOWN';
1346
+ const confidenceDisplay = confidence !== undefined ? confidence : 0;
1347
+ const healthDisplay = healthScore !== undefined ? healthScore : 0;
1348
+ const healthIcon = healthDisplay >= 80 ? '🟢' : healthDisplay >= 60 ? '🟡' : '🔴';
1349
+
1350
+ summary += `#### 🏛️ Architecture\n\n`;
1351
+ summary += `| Pattern | Confidence | Health |\n`;
1352
+ summary += `|:--|:--:|:--:|\n`;
1353
+ summary += `| **${patternDisplay}** | ${confidenceDisplay}% | ${healthIcon} ${healthDisplay}/100 |\n\n`;
1354
+
1355
+ if (violations.length > 0) {
1356
+ summary += `⚠️ ${violations.length} architecture violations detected\n\n`;
1357
+ }
1358
+ }
1359
+
1360
+ // Impact section
1361
+ if (impact) {
1362
+ const { score: impactScore = 0, severity = 'LOW', endpoints = [], tables = [] } = impact;
1363
+ const severityIcon = severity === 'HIGH' ? '🔴' : severity === 'MEDIUM' ? '🟡' : '🟢';
1364
+
1365
+ summary += `#### 🎯 Impact Analysis\n\n`;
1366
+ summary += `| Severity | Score | Affected |\n`;
1367
+ summary += `|:--|:--:|:--|\n`;
1368
+
1369
+ let affectedItems = [];
1370
+ if (endpoints.length > 0) affectedItems.push(`${endpoints.length} APIs`);
1371
+ if (tables.length > 0) affectedItems.push(`${tables.length} tables`);
1372
+ const affectedText = affectedItems.length > 0 ? affectedItems.join(', ') : 'None';
1373
+
1374
+ summary += `| ${severityIcon} **${severity}** | ${impactScore}/100 | ${affectedText} |\n\n`;
1375
+ }
1376
+ }
1377
+
1378
+ // === Footer ===
1379
+ summary += `---\n`;
1380
+ summary += `<sub>`;
1381
+ summary += `Powered by [SunLint](https://coding-standards.sun-asterisk.vn)`;
1382
+
1383
+ if (process.env.GITHUB_RUN_ID) {
1384
+ const runUrl = `https://github.com/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`;
1385
+ summary += ` · [View Details](${runUrl})`;
1386
+ }
1387
+ summary += `</sub>\n`;
1388
+
1389
+ // Find existing comment
1390
+ let existingComment = null;
1391
+ try {
1392
+ const { data: comments } = await octokit.issues.listComments({
1393
+ owner,
1394
+ repo: repoName,
1395
+ issue_number: prNumber,
1396
+ per_page: 100
1397
+ });
1398
+
1399
+ existingComment = comments.find(comment =>
1400
+ comment.body.includes('Code Quality Analysis') ||
1401
+ comment.body.includes('☀️ SunLint') ||
1402
+ comment.body.includes('SunLint Analysis Report') ||
1403
+ comment.body.includes('SunLint Report')
1404
+ );
1405
+ } catch (error) {
1406
+ logger.warn('Failed to fetch existing comments', { error: error.message });
1407
+ }
1408
+
1409
+ // Post or update comment
1410
+ let commentResult;
1411
+ try {
1412
+ if (existingComment) {
1413
+ commentResult = await withRetry(async () => {
1414
+ return await octokit.issues.updateComment({
1415
+ owner,
1416
+ repo: repoName,
1417
+ comment_id: existingComment.id,
1418
+ body: summary
1419
+ });
1420
+ });
1421
+ logger.info('Combined summary updated');
1422
+ } else {
1423
+ commentResult = await withRetry(async () => {
1424
+ return await octokit.issues.createComment({
1425
+ owner,
1426
+ repo: repoName,
1427
+ issue_number: prNumber,
1428
+ body: summary
1429
+ });
1430
+ });
1431
+ logger.info('Combined summary created');
1432
+ }
1433
+ } catch (error) {
1434
+ throw new GitHubAPIError(
1435
+ `Failed to post combined summary: ${error.message}`,
1436
+ error.status,
1437
+ error
1438
+ );
1439
+ }
1440
+
1441
+ return {
1442
+ success: true,
1443
+ action: existingComment ? 'updated' : 'created',
1444
+ commentId: commentResult.data.id,
1445
+ commentUrl: commentResult.data.html_url,
1446
+ duration: Date.now() - startTime
1447
+ };
1448
+
1449
+ } catch (error) {
1450
+ logger.error('Combined summary failed', error);
1451
+ if (error instanceof ValidationError || error instanceof GitHubAPIError) {
1452
+ throw error;
1453
+ }
1454
+ throw new Error(`Combined summary failed: ${error.message}`);
1455
+ }
1456
+ }
1457
+
1145
1458
  module.exports = {
1146
1459
  annotate,
1147
1460
  postSummaryComment,
1461
+ postCombinedSummaryComment,
1462
+ generateAISummary,
1148
1463
  ValidationError,
1149
1464
  GitHubAPIError
1150
1465
  };