@sun-asterisk/sunlint 1.3.34 → 1.3.36

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 +102 -2
  4. package/core/cli-program.js +102 -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 +551 -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
package/core/git-utils.js CHANGED
@@ -186,13 +186,19 @@ class GitUtils {
186
186
  try {
187
187
  const { baseBranch, provider } = prContext;
188
188
 
189
- console.log(`🔍 Detecting changed files for PR (provider: ${provider}, base: ${baseBranch})`);
189
+ // Professional output header
190
+ console.log('');
191
+ console.log('┌─ Git Integration ────────────────────────────────────');
192
+ console.log(`│ Provider: ${provider.toUpperCase()}`);
193
+ console.log(`│ Base: ${baseBranch}`);
194
+ console.log('├──────────────────────────────────────────────────────');
190
195
 
191
196
  // Try to find the base branch reference
192
197
  let baseRef = this.findBaseRef(baseBranch, gitRoot);
193
198
 
194
199
  if (!baseRef) {
195
- console.log(`⚠️ Base ref not found locally, attempting to fetch origin/${baseBranch}...`);
200
+ console.log(`│ Base ref not found locally`);
201
+ console.log(`│ → Fetching origin/${baseBranch}...`);
196
202
 
197
203
  // Try to fetch and create the ref
198
204
  const fetchSuccess = this.ensureBaseRefExists(`origin/${baseBranch}`, gitRoot);
@@ -200,11 +206,12 @@ class GitUtils {
200
206
  if (fetchSuccess) {
201
207
  baseRef = `origin/${baseBranch}`;
202
208
  } else {
209
+ console.log('└──────────────────────────────────────────────────────');
203
210
  throw new Error(`Cannot find or fetch base branch: ${baseBranch}`);
204
211
  }
205
212
  } else {
206
213
  // Ensure we have the latest
207
- console.log(`✅ Found base ref: ${baseRef}`);
214
+ console.log(`│ ✓ Base ref: ${baseRef}`);
208
215
  this.ensureBaseRefExists(baseRef, gitRoot);
209
216
  }
210
217
 
@@ -215,10 +222,10 @@ class GitUtils {
215
222
  cwd: gitRoot,
216
223
  encoding: 'utf8'
217
224
  }).trim();
218
- console.log(`✅ Found merge-base: ${mergeBase.substring(0, 8)}`);
225
+ console.log(`│ ✓ Merge-base: ${mergeBase.substring(0, 8)}`);
219
226
  } catch (error) {
220
227
  // If merge-base fails, fall back to direct comparison
221
- console.warn(`⚠️ Could not find merge-base, using direct diff with ${baseRef}`);
228
+ console.log(`│ Merge-base not found, using direct diff`);
222
229
  mergeBase = baseRef;
223
230
  }
224
231
 
@@ -232,7 +239,10 @@ class GitUtils {
232
239
  .map(file => path.resolve(gitRoot, file))
233
240
  .filter(file => fs.existsSync(file));
234
241
 
235
- console.log(`✅ Found ${changedFiles.length} changed files in PR`);
242
+ console.log('├──────────────────────────────────────────────────────');
243
+ console.log(`│ Changed files: ${changedFiles.length}`);
244
+ console.log('└──────────────────────────────────────────────────────');
245
+ console.log('');
236
246
 
237
247
  return changedFiles;
238
248
  } catch (error) {
@@ -290,13 +300,11 @@ class GitUtils {
290
300
 
291
301
  if (remote === 'origin' || remote === 'upstream') {
292
302
  try {
293
- console.log(`⬇️ Fetching ${remote}/${branch}...`);
294
-
295
303
  // Check if this is a shallow repository (common in GitHub Actions)
296
304
  const isShallow = this.isShallowRepository(gitRoot);
297
305
 
298
306
  if (isShallow) {
299
- console.log(` ℹ️ Detected shallow clone, fetching with additional history...`);
307
+ console.log(`│ Shallow clone detected, deepening...`);
300
308
 
301
309
  // For shallow clones, we need to:
302
310
  // 1. Fetch the base branch
@@ -317,7 +325,6 @@ class GitUtils {
317
325
  });
318
326
  } catch (shallowError) {
319
327
  // If deepen fails, try direct fetch
320
- console.log(` ℹ️ Trying direct fetch...`);
321
328
  execSync(`git fetch ${remote} ${branch}`, {
322
329
  cwd: gitRoot,
323
330
  stdio: 'pipe',
@@ -339,10 +346,10 @@ class GitUtils {
339
346
  stdio: 'ignore'
340
347
  });
341
348
 
342
- console.log(`✅ Successfully fetched ${baseRef}`);
349
+ console.log(`│ ✓ Fetched ${baseRef}`);
343
350
  return true;
344
351
  } catch (fetchError) {
345
- console.warn(`⚠️ Failed to fetch ${baseRef}: ${fetchError.message}`);
352
+ console.log(`│ Failed to fetch: ${fetchError.message}`);
346
353
  return false;
347
354
  }
348
355
  }
@@ -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
  };