@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.
- package/core/architecture-integration.js +16 -7
- package/core/auto-performance-manager.js +1 -1
- package/core/cli-action-handler.js +92 -2
- package/core/cli-program.js +96 -138
- package/core/file-targeting-service.js +62 -4
- package/core/git-utils.js +19 -12
- package/core/github-annotate-service.js +326 -11
- package/core/html-report-generator.js +326 -731
- package/core/impact-integration.js +433 -0
- package/core/output-service.js +293 -21
- package/core/scoring-service.js +3 -2
- package/engines/arch-detect/core/analyzer.js +413 -0
- package/engines/arch-detect/core/index.js +22 -0
- package/engines/arch-detect/engine/hybrid-detector.js +176 -0
- package/engines/arch-detect/engine/index.js +24 -0
- package/engines/arch-detect/engine/rule-executor.js +228 -0
- package/engines/arch-detect/engine/score-calculator.js +214 -0
- package/engines/arch-detect/engine/violation-detector.js +616 -0
- package/engines/arch-detect/index.js +50 -0
- package/engines/arch-detect/rules/base-rule.js +187 -0
- package/engines/arch-detect/rules/index.js +35 -0
- package/engines/arch-detect/rules/layered/index.js +28 -0
- package/engines/arch-detect/rules/layered/l001-presentation-layer.js +237 -0
- package/engines/arch-detect/rules/layered/l002-business-layer.js +215 -0
- package/engines/arch-detect/rules/layered/l003-data-layer.js +229 -0
- package/engines/arch-detect/rules/layered/l004-model-layer.js +204 -0
- package/engines/arch-detect/rules/layered/l005-layer-separation.js +215 -0
- package/engines/arch-detect/rules/layered/l006-dependency-direction.js +221 -0
- package/engines/arch-detect/rules/layered/layered-rules-collection.js +445 -0
- package/engines/arch-detect/rules/modular/index.js +27 -0
- package/engines/arch-detect/rules/modular/m001-feature-modules.js +238 -0
- package/engines/arch-detect/rules/modular/m002-core-module.js +169 -0
- package/engines/arch-detect/rules/modular/m003-module-declaration.js +186 -0
- package/engines/arch-detect/rules/modular/m004-public-api.js +171 -0
- package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +220 -0
- package/engines/arch-detect/rules/modular/modular-rules-collection.js +357 -0
- package/engines/arch-detect/rules/presentation/index.js +27 -0
- package/engines/arch-detect/rules/presentation/pr001-view-layer.js +221 -0
- package/engines/arch-detect/rules/presentation/pr002-presentation-logic.js +192 -0
- package/engines/arch-detect/rules/presentation/pr004-data-binding.js +187 -0
- package/engines/arch-detect/rules/presentation/pr006-router-layer.js +185 -0
- package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +181 -0
- package/engines/arch-detect/rules/presentation/presentation-rules-collection.js +507 -0
- package/engines/arch-detect/rules/project-scanner/index.js +31 -0
- package/engines/arch-detect/rules/project-scanner/ps001-project-root.js +213 -0
- package/engines/arch-detect/rules/project-scanner/ps002-language-detection.js +192 -0
- package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +339 -0
- package/engines/arch-detect/rules/project-scanner/ps004-build-system.js +171 -0
- package/engines/arch-detect/rules/project-scanner/ps005-source-directory.js +163 -0
- package/engines/arch-detect/rules/project-scanner/ps006-test-directory.js +184 -0
- package/engines/arch-detect/rules/project-scanner/ps007-documentation.js +149 -0
- package/engines/arch-detect/rules/project-scanner/ps008-cicd-detection.js +163 -0
- package/engines/arch-detect/rules/project-scanner/ps009-code-quality.js +152 -0
- package/engines/arch-detect/rules/project-scanner/ps010-statistics.js +180 -0
- package/engines/arch-detect/rules/rule-registry.js +111 -0
- package/engines/arch-detect/types/context.types.js +60 -0
- package/engines/arch-detect/types/enums.js +161 -0
- package/engines/arch-detect/types/index.js +25 -0
- package/engines/arch-detect/types/result.types.js +7 -0
- package/engines/arch-detect/types/rule.types.js +7 -0
- package/engines/arch-detect/utils/file-scanner.js +411 -0
- package/engines/arch-detect/utils/index.js +23 -0
- package/engines/arch-detect/utils/pattern-matcher.js +328 -0
- package/engines/impact/cli.js +106 -0
- package/engines/impact/config/default-config.js +54 -0
- package/engines/impact/core/change-detector.js +258 -0
- package/engines/impact/core/detectors/database-detector.js +1317 -0
- package/engines/impact/core/detectors/endpoint-detector.js +55 -0
- package/engines/impact/core/impact-analyzer.js +124 -0
- package/engines/impact/core/report-generator.js +462 -0
- package/engines/impact/core/utils/ast-parser.js +241 -0
- package/engines/impact/core/utils/dependency-graph.js +159 -0
- package/engines/impact/core/utils/file-utils.js +116 -0
- package/engines/impact/core/utils/git-utils.js +203 -0
- package/engines/impact/core/utils/logger.js +13 -0
- package/engines/impact/core/utils/method-call-graph.js +1192 -0
- package/engines/impact/index.js +135 -0
- package/engines/impact/package.json +29 -0
- package/package.json +18 -43
- package/scripts/build-release.sh +0 -0
- package/scripts/copy-impact-analyzer.js +135 -0
- package/scripts/install.sh +0 -0
- package/scripts/manual-release.sh +0 -0
- package/scripts/pre-release-test.sh +0 -0
- package/scripts/prepare-release.sh +0 -0
- package/scripts/quick-performance-test.js +0 -0
- package/scripts/setup-github-registry.sh +0 -0
- package/scripts/trigger-release.sh +0 -0
- package/scripts/verify-install.sh +0 -0
- package/templates/combined-report.html +1418 -0
|
@@ -1,49 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTML Report Generator
|
|
3
|
-
* Generate
|
|
3
|
+
* Generate HTML report from template for SunLint results
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
6
9
|
/**
|
|
7
|
-
* Generate HTML report from violations data
|
|
10
|
+
* Generate HTML report from violations data using template
|
|
8
11
|
* @param {Array} violations - Array of violation objects
|
|
9
12
|
* @param {Object} metadata - Report metadata
|
|
10
13
|
* @param {Object} metadata.score - Scoring summary
|
|
11
14
|
* @param {Object} metadata.gitInfo - Git information
|
|
12
15
|
* @param {string} metadata.timestamp - Report timestamp
|
|
16
|
+
* @param {Object} metadata.architecture - Architecture analysis data
|
|
17
|
+
* @param {Object} metadata.impact - Impact analysis data
|
|
13
18
|
* @returns {string} Complete HTML report
|
|
14
19
|
*/
|
|
15
20
|
function generateHTMLReport(violations, metadata = {}) {
|
|
16
21
|
const {
|
|
17
22
|
score = {},
|
|
18
23
|
gitInfo = {},
|
|
19
|
-
timestamp = new Date().toISOString()
|
|
24
|
+
timestamp = new Date().toISOString(),
|
|
25
|
+
architecture = null,
|
|
26
|
+
impact = null,
|
|
27
|
+
aiSummary = null
|
|
20
28
|
} = metadata;
|
|
21
29
|
|
|
30
|
+
// Load template
|
|
31
|
+
const templatePath = path.join(__dirname, '..', 'templates', 'combined-report.html');
|
|
32
|
+
let template;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
template = fs.readFileSync(templatePath, 'utf8');
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error(`Failed to load template: ${error.message}`);
|
|
38
|
+
// Fallback to basic HTML
|
|
39
|
+
return generateFallbackHTML(violations, metadata);
|
|
40
|
+
}
|
|
41
|
+
|
|
22
42
|
// Calculate statistics
|
|
23
43
|
const stats = calculateStatistics(violations);
|
|
24
44
|
|
|
25
|
-
//
|
|
26
|
-
const
|
|
27
|
-
<html lang="en">
|
|
28
|
-
<head>
|
|
29
|
-
<meta charset="UTF-8">
|
|
30
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
31
|
-
<title>SunLint Report - ${gitInfo.repository_name || 'Project'}</title>
|
|
32
|
-
${embedCSS()}
|
|
33
|
-
</head>
|
|
34
|
-
<body>
|
|
35
|
-
<div class="container">
|
|
36
|
-
${generateHeader(gitInfo, timestamp)}
|
|
37
|
-
${generateSummary(stats, score)}
|
|
38
|
-
${generateByFileSection(violations, stats.fileGroups)}
|
|
39
|
-
${generateByRuleSection(violations, stats.ruleGroups)}
|
|
40
|
-
${generateFooter(timestamp)}
|
|
41
|
-
</div>
|
|
42
|
-
${embedJavaScript()}
|
|
43
|
-
</body>
|
|
44
|
-
</html>`;
|
|
45
|
+
// Prepare template data
|
|
46
|
+
const data = prepareTemplateData(stats, score, architecture, impact, gitInfo, timestamp, violations, aiSummary);
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
// Render template
|
|
49
|
+
return renderTemplate(template, data);
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
/**
|
|
@@ -65,15 +68,6 @@ function calculateStatistics(violations) {
|
|
|
65
68
|
fileGroups[v.file].push(v);
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
// Group by rule
|
|
69
|
-
const ruleGroups = {};
|
|
70
|
-
for (const v of violations) {
|
|
71
|
-
if (!ruleGroups[v.rule]) {
|
|
72
|
-
ruleGroups[v.rule] = [];
|
|
73
|
-
}
|
|
74
|
-
ruleGroups[v.rule].push(v);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
71
|
const filesWithIssues = Object.keys(fileGroups).length;
|
|
78
72
|
|
|
79
73
|
return {
|
|
@@ -81,748 +75,302 @@ function calculateStatistics(violations) {
|
|
|
81
75
|
errorCount,
|
|
82
76
|
warningCount,
|
|
83
77
|
filesWithIssues,
|
|
84
|
-
fileGroups
|
|
85
|
-
ruleGroups
|
|
78
|
+
fileGroups
|
|
86
79
|
};
|
|
87
80
|
}
|
|
88
81
|
|
|
89
82
|
/**
|
|
90
|
-
*
|
|
91
|
-
* @param {Object} gitInfo - Git information
|
|
92
|
-
* @param {string} timestamp - Report timestamp
|
|
93
|
-
* @returns {string} Header HTML
|
|
83
|
+
* Prepare data for template rendering
|
|
94
84
|
*/
|
|
95
|
-
function
|
|
96
|
-
const repoName = gitInfo.repository_name || 'Project';
|
|
97
|
-
const branch = gitInfo.branch || 'Unknown';
|
|
98
|
-
const commit = gitInfo.commit_hash ? gitInfo.commit_hash.substring(0, 7) : 'N/A';
|
|
99
|
-
|
|
100
|
-
return `
|
|
101
|
-
<header class="header">
|
|
102
|
-
<div class="header-content">
|
|
103
|
-
<h1>
|
|
104
|
-
<span class="logo">🌟</span>
|
|
105
|
-
SunLint Code Quality Report
|
|
106
|
-
</h1>
|
|
107
|
-
<div class="header-meta">
|
|
108
|
-
<div class="meta-item">
|
|
109
|
-
<span class="meta-label">Repository:</span>
|
|
110
|
-
<span class="meta-value">${escapeHTML(repoName)}</span>
|
|
111
|
-
</div>
|
|
112
|
-
<div class="meta-item">
|
|
113
|
-
<span class="meta-label">Branch:</span>
|
|
114
|
-
<span class="meta-value">${escapeHTML(branch)}</span>
|
|
115
|
-
</div>
|
|
116
|
-
<div class="meta-item">
|
|
117
|
-
<span class="meta-label">Commit:</span>
|
|
118
|
-
<span class="meta-value">${escapeHTML(commit)}</span>
|
|
119
|
-
</div>
|
|
120
|
-
<div class="meta-item">
|
|
121
|
-
<span class="meta-label">Generated:</span>
|
|
122
|
-
<span class="meta-value">${new Date(timestamp).toLocaleString()}</span>
|
|
123
|
-
</div>
|
|
124
|
-
</div>
|
|
125
|
-
</div>
|
|
126
|
-
</header>`;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Generate summary section
|
|
131
|
-
* @param {Object} stats - Statistics object
|
|
132
|
-
* @param {Object} score - Score information
|
|
133
|
-
* @returns {string} Summary HTML
|
|
134
|
-
*/
|
|
135
|
-
function generateSummary(stats, score) {
|
|
85
|
+
function prepareTemplateData(stats, score, architecture, impact, gitInfo, timestamp, violations, aiSummary) {
|
|
136
86
|
const { totalViolations, errorCount, warningCount, filesWithIssues } = stats;
|
|
137
|
-
const status = errorCount > 0 ? 'failed' : warningCount > 0 ? 'warning' : 'passed';
|
|
138
|
-
const statusIcon = errorCount > 0 ? '❌' : warningCount > 0 ? '⚠️' : '✅';
|
|
139
|
-
const statusText = errorCount > 0 ? 'Failed' : warningCount > 0 ? 'Passed with Warnings' : 'Passed';
|
|
140
|
-
|
|
141
|
-
const scoreValue = score.score !== undefined ? score.score : 'N/A';
|
|
142
|
-
const grade = score.grade || 'N/A';
|
|
143
|
-
|
|
144
|
-
return `
|
|
145
|
-
<section class="summary">
|
|
146
|
-
<div class="summary-header">
|
|
147
|
-
<div class="status-badge status-${status}">
|
|
148
|
-
<span class="status-icon">${statusIcon}</span>
|
|
149
|
-
<span class="status-text">${statusText}</span>
|
|
150
|
-
</div>
|
|
151
|
-
${scoreValue !== 'N/A' ? `
|
|
152
|
-
<div class="score-display">
|
|
153
|
-
<div class="score-value">${scoreValue}</div>
|
|
154
|
-
<div class="score-grade">${grade}</div>
|
|
155
|
-
</div>
|
|
156
|
-
` : ''}
|
|
157
|
-
</div>
|
|
158
|
-
|
|
159
|
-
<div class="summary-stats">
|
|
160
|
-
<div class="stat-card">
|
|
161
|
-
<div class="stat-value">${totalViolations}</div>
|
|
162
|
-
<div class="stat-label">Total Violations</div>
|
|
163
|
-
</div>
|
|
164
|
-
<div class="stat-card stat-error">
|
|
165
|
-
<div class="stat-value">${errorCount}</div>
|
|
166
|
-
<div class="stat-label">Errors</div>
|
|
167
|
-
</div>
|
|
168
|
-
<div class="stat-card stat-warning">
|
|
169
|
-
<div class="stat-value">${warningCount}</div>
|
|
170
|
-
<div class="stat-label">Warnings</div>
|
|
171
|
-
</div>
|
|
172
|
-
<div class="stat-card">
|
|
173
|
-
<div class="stat-value">${filesWithIssues}</div>
|
|
174
|
-
<div class="stat-label">Files with Issues</div>
|
|
175
|
-
</div>
|
|
176
|
-
</div>
|
|
177
|
-
</section>`;
|
|
178
|
-
}
|
|
179
87
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
let html = `
|
|
199
|
-
<section class="section">
|
|
200
|
-
<h2>📁 Violations by File</h2>
|
|
201
|
-
<div class="controls">
|
|
202
|
-
<input type="text" id="fileSearch" class="search-box" placeholder="Search files...">
|
|
203
|
-
<select id="fileFilter" class="filter-select">
|
|
204
|
-
<option value="all">All Severities</option>
|
|
205
|
-
<option value="error">Errors Only</option>
|
|
206
|
-
<option value="warning">Warnings Only</option>
|
|
207
|
-
</select>
|
|
208
|
-
</div>
|
|
209
|
-
<table class="violations-table" id="fileTable">
|
|
210
|
-
<thead>
|
|
211
|
-
<tr>
|
|
212
|
-
<th onclick="sortTable('fileTable', 0)">File <span class="sort-icon">↕</span></th>
|
|
213
|
-
<th onclick="sortTable('fileTable', 1)">Errors <span class="sort-icon">↕</span></th>
|
|
214
|
-
<th onclick="sortTable('fileTable', 2)">Warnings <span class="sort-icon">↕</span></th>
|
|
215
|
-
<th onclick="sortTable('fileTable', 3)">Total <span class="sort-icon">↕</span></th>
|
|
216
|
-
<th>Details</th>
|
|
217
|
-
</tr>
|
|
218
|
-
</thead>
|
|
219
|
-
<tbody>`;
|
|
220
|
-
|
|
221
|
-
for (const [file, fileViolations] of sortedFiles) {
|
|
222
|
-
const fileErrors = fileViolations.filter(v => v.severity === 'error').length;
|
|
223
|
-
const fileWarnings = fileViolations.filter(v => v.severity === 'warning').length;
|
|
224
|
-
const total = fileViolations.length;
|
|
225
|
-
const fileId = `file-${Buffer.from(file).toString('base64').replace(/=/g, '')}`;
|
|
226
|
-
|
|
227
|
-
html += `
|
|
228
|
-
<tr class="file-row" data-errors="${fileErrors}" data-warnings="${fileWarnings}">
|
|
229
|
-
<td class="file-path"><code>${escapeHTML(file)}</code></td>
|
|
230
|
-
<td class="count-cell ${fileErrors > 0 ? 'has-errors' : ''}">${fileErrors}</td>
|
|
231
|
-
<td class="count-cell ${fileWarnings > 0 ? 'has-warnings' : ''}">${fileWarnings}</td>
|
|
232
|
-
<td class="count-cell">${total}</td>
|
|
233
|
-
<td>
|
|
234
|
-
<button class="details-btn" onclick="toggleDetails('${fileId}')">
|
|
235
|
-
Show Details <span class="arrow">▼</span>
|
|
236
|
-
</button>
|
|
237
|
-
</td>
|
|
238
|
-
</tr>
|
|
239
|
-
<tr class="details-row" id="${fileId}" style="display: none;">
|
|
240
|
-
<td colspan="5">
|
|
241
|
-
<div class="details-content">
|
|
242
|
-
${generateFileDetails(fileViolations)}
|
|
243
|
-
</div>
|
|
244
|
-
</td>
|
|
245
|
-
</tr>`;
|
|
88
|
+
// Quality data
|
|
89
|
+
const qualityScore = score.score ?? 0;
|
|
90
|
+
const qualityGrade = score.grade ?? 'F';
|
|
91
|
+
const qualityColor = qualityScore >= 80 ? 'text-success' : qualityScore >= 60 ? 'text-warning' : 'text-danger';
|
|
92
|
+
const qualityBarClass = qualityScore >= 80 ? 'excellent' : qualityScore >= 60 ? 'good' : 'poor';
|
|
93
|
+
const qualityBadgeClass = qualityScore >= 80 ? 'bg-success' : qualityScore >= 60 ? 'bg-warning' : 'bg-danger';
|
|
94
|
+
// Grade emoji and description based on score
|
|
95
|
+
let gradeEmoji, gradeDesc;
|
|
96
|
+
if (qualityScore >= 90) {
|
|
97
|
+
gradeEmoji = '🏆'; gradeDesc = 'Excellent';
|
|
98
|
+
} else if (qualityScore >= 80) {
|
|
99
|
+
gradeEmoji = '✨'; gradeDesc = 'Good';
|
|
100
|
+
} else if (qualityScore >= 70) {
|
|
101
|
+
gradeEmoji = '👍'; gradeDesc = 'Fair';
|
|
102
|
+
} else if (qualityScore >= 60) {
|
|
103
|
+
gradeEmoji = '⚡'; gradeDesc = 'Needs Work';
|
|
104
|
+
} else {
|
|
105
|
+
gradeEmoji = '🔧'; gradeDesc = 'Needs Improvement';
|
|
246
106
|
}
|
|
247
107
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
108
|
+
// Architecture data
|
|
109
|
+
const archPattern = architecture?.pattern?.toUpperCase() || 'UNKNOWN';
|
|
110
|
+
const archHealth = architecture?.healthScore ?? 0;
|
|
111
|
+
const archConfidence = architecture?.confidence ?? 0;
|
|
112
|
+
const archViolations = architecture?.violations?.length ?? 0;
|
|
113
|
+
const archColor = archHealth >= 80 ? 'text-success' : archHealth >= 60 ? 'text-warning' : 'text-danger';
|
|
114
|
+
const archBarClass = archHealth >= 80 ? 'bg-success' : archHealth >= 60 ? 'bg-warning' : 'bg-danger';
|
|
115
|
+
|
|
116
|
+
// Impact data
|
|
117
|
+
const impactScore = impact?.score ?? 0;
|
|
118
|
+
const impactSeverity = impact?.severity || 'LOW';
|
|
119
|
+
const affectedApis = impact?.endpoints?.length ?? 0;
|
|
120
|
+
const affectedTables = impact?.tables?.length ?? 0;
|
|
121
|
+
const impactIconClass = impactSeverity === 'HIGH' ? 'bg-danger' : impactSeverity === 'MEDIUM' ? 'bg-warning' : 'bg-success';
|
|
122
|
+
const impactBadgeClass = impactSeverity === 'HIGH' ? 'bg-danger' : impactSeverity === 'MEDIUM' ? 'bg-warning' : 'bg-success';
|
|
123
|
+
const impactBarClass = impactSeverity === 'HIGH' ? 'bg-danger' : impactSeverity === 'MEDIUM' ? 'bg-warning' : 'bg-success';
|
|
124
|
+
|
|
125
|
+
// Endpoints
|
|
126
|
+
const endpoints = (impact?.endpoints || []).slice(0, 20).map(ep => ({
|
|
127
|
+
method: ep.method || 'GET',
|
|
128
|
+
method_lower: (ep.method || 'GET').toLowerCase(),
|
|
129
|
+
path: ep.path || ep.endpoint || ''
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
// Violations for table
|
|
133
|
+
const violationList = violations.slice(0, 100).map(v => ({
|
|
134
|
+
severity: v.severity,
|
|
135
|
+
severity_label: v.severity === 'error' ? 'Error' : 'Warning',
|
|
136
|
+
severity_icon: v.severity === 'error' ? '✕' : '⚠',
|
|
137
|
+
rule: v.rule || 'unknown',
|
|
138
|
+
file: shortenPath(v.file),
|
|
139
|
+
line: v.line || 1,
|
|
140
|
+
message: v.message || ''
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
// Architecture violations
|
|
144
|
+
const archViolationList = (architecture?.violations || []).slice(0, 20).map(v => ({
|
|
145
|
+
type: v.type || 'violation',
|
|
146
|
+
source: shortenPath(v.source || v.file || ''),
|
|
147
|
+
target: shortenPath(v.target || ''),
|
|
148
|
+
description: v.message || v.description || ''
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
// Extract owner/repo from repository_url (e.g., https://github.com/owner/repo -> owner/repo)
|
|
152
|
+
let repoFullName = process.env.GITHUB_REPOSITORY || 'Unknown';
|
|
153
|
+
if (repoFullName === 'Unknown' && gitInfo.repository_url) {
|
|
154
|
+
const match = gitInfo.repository_url.match(/github\.com\/([^\/]+\/[^\/]+)/);
|
|
155
|
+
if (match) {
|
|
156
|
+
repoFullName = match[1];
|
|
157
|
+
}
|
|
290
158
|
}
|
|
291
159
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
<section class="section">
|
|
297
|
-
<h2>📋 Violations by Rule</h2>
|
|
298
|
-
<table class="violations-table" id="ruleTable">
|
|
299
|
-
<thead>
|
|
300
|
-
<tr>
|
|
301
|
-
<th onclick="sortTable('ruleTable', 0)">Rule <span class="sort-icon">↕</span></th>
|
|
302
|
-
<th onclick="sortTable('ruleTable', 1)">Errors <span class="sort-icon">↕</span></th>
|
|
303
|
-
<th onclick="sortTable('ruleTable', 2)">Warnings <span class="sort-icon">↕</span></th>
|
|
304
|
-
<th onclick="sortTable('ruleTable', 3)">Total <span class="sort-icon">↕</span></th>
|
|
305
|
-
<th>Locations</th>
|
|
306
|
-
</tr>
|
|
307
|
-
</thead>
|
|
308
|
-
<tbody>`;
|
|
309
|
-
|
|
310
|
-
for (const [ruleId, ruleViolations] of sortedRules) {
|
|
311
|
-
const ruleErrors = ruleViolations.filter(v => v.severity === 'error').length;
|
|
312
|
-
const ruleWarnings = ruleViolations.filter(v => v.severity === 'warning').length;
|
|
313
|
-
const total = ruleViolations.length;
|
|
314
|
-
const ruleIdSafe = `rule-${Buffer.from(ruleId).toString('base64').replace(/=/g, '')}`;
|
|
315
|
-
|
|
316
|
-
html += `
|
|
317
|
-
<tr class="rule-row">
|
|
318
|
-
<td class="rule-id"><code>${escapeHTML(ruleId)}</code></td>
|
|
319
|
-
<td class="count-cell ${ruleErrors > 0 ? 'has-errors' : ''}">${ruleErrors}</td>
|
|
320
|
-
<td class="count-cell ${ruleWarnings > 0 ? 'has-warnings' : ''}">${ruleWarnings}</td>
|
|
321
|
-
<td class="count-cell">${total}</td>
|
|
322
|
-
<td>
|
|
323
|
-
<button class="details-btn" onclick="toggleDetails('${ruleIdSafe}')">
|
|
324
|
-
Show Locations <span class="arrow">▼</span>
|
|
325
|
-
</button>
|
|
326
|
-
</td>
|
|
327
|
-
</tr>
|
|
328
|
-
<tr class="details-row" id="${ruleIdSafe}" style="display: none;">
|
|
329
|
-
<td colspan="5">
|
|
330
|
-
<div class="details-content">
|
|
331
|
-
${generateRuleDetails(ruleViolations)}
|
|
332
|
-
</div>
|
|
333
|
-
</td>
|
|
334
|
-
</tr>`;
|
|
160
|
+
// Get branch name, fallback if HEAD (detached state)
|
|
161
|
+
let branchName = gitInfo.branch || 'Unknown';
|
|
162
|
+
if (branchName === 'HEAD') {
|
|
163
|
+
branchName = process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || 'main';
|
|
335
164
|
}
|
|
336
165
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
166
|
+
return {
|
|
167
|
+
// Git Info
|
|
168
|
+
repo_name: repoFullName,
|
|
169
|
+
branch: branchName,
|
|
170
|
+
commit_short: gitInfo.commit_hash ? gitInfo.commit_hash.substring(0, 7) : (process.env.GITHUB_SHA ? process.env.GITHUB_SHA.substring(0, 7) : 'N/A'),
|
|
171
|
+
commit_author: gitInfo.author_name || gitInfo.author || process.env.GITHUB_ACTOR || 'Unknown',
|
|
172
|
+
|
|
173
|
+
// Header
|
|
174
|
+
generated_date: new Date(timestamp).toLocaleString(),
|
|
175
|
+
total_files: filesWithIssues,
|
|
176
|
+
duration: 'N/A',
|
|
177
|
+
|
|
178
|
+
// Quality
|
|
179
|
+
quality_score: qualityScore,
|
|
180
|
+
quality_grade: qualityGrade,
|
|
181
|
+
quality_color: qualityColor,
|
|
182
|
+
quality_bar_class: qualityBarClass,
|
|
183
|
+
quality_badge_class: qualityBadgeClass,
|
|
184
|
+
grade_emoji: gradeEmoji,
|
|
185
|
+
grade_desc: gradeDesc,
|
|
186
|
+
error_count: errorCount,
|
|
187
|
+
warning_count: warningCount,
|
|
188
|
+
|
|
189
|
+
// Architecture
|
|
190
|
+
has_architecture: architecture !== null,
|
|
191
|
+
arch_pattern: archPattern,
|
|
192
|
+
arch_health: archHealth,
|
|
193
|
+
arch_confidence: archConfidence,
|
|
194
|
+
arch_violations: archViolations,
|
|
195
|
+
arch_color: archColor,
|
|
196
|
+
arch_bar_class: archBarClass,
|
|
197
|
+
has_arch_violations: archViolations > 0,
|
|
198
|
+
arch_violation_list: archViolationList,
|
|
199
|
+
|
|
200
|
+
// Impact
|
|
201
|
+
has_impact: impact !== null,
|
|
202
|
+
impact_score: impactScore,
|
|
203
|
+
impact_severity: impactSeverity,
|
|
204
|
+
affected_apis: affectedApis,
|
|
205
|
+
affected_tables: affectedTables,
|
|
206
|
+
impact_icon_class: impactIconClass,
|
|
207
|
+
impact_badge_class: impactBadgeClass,
|
|
208
|
+
impact_bar_class: impactBarClass,
|
|
209
|
+
has_endpoints: endpoints.length > 0,
|
|
210
|
+
endpoints: endpoints,
|
|
211
|
+
|
|
212
|
+
// Violations
|
|
213
|
+
has_violations: totalViolations > 0,
|
|
214
|
+
no_violations: totalViolations === 0,
|
|
215
|
+
total_violations: totalViolations,
|
|
216
|
+
violations: violationList,
|
|
217
|
+
|
|
218
|
+
// AI Summary
|
|
219
|
+
has_ai_summary: !!aiSummary,
|
|
220
|
+
ai_summary_html: aiSummary ? formatAISummaryToHTML(aiSummary) : '',
|
|
221
|
+
|
|
222
|
+
// Footer
|
|
223
|
+
version: getVersion(),
|
|
224
|
+
year: new Date().getFullYear()
|
|
225
|
+
};
|
|
343
226
|
}
|
|
344
227
|
|
|
345
228
|
/**
|
|
346
|
-
*
|
|
347
|
-
* @param {
|
|
348
|
-
* @returns {string}
|
|
229
|
+
* Format AI summary text to HTML
|
|
230
|
+
* @param {string} summary - Raw AI summary text
|
|
231
|
+
* @returns {string} Formatted HTML
|
|
349
232
|
*/
|
|
350
|
-
function
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
233
|
+
function formatAISummaryToHTML(summary) {
|
|
234
|
+
if (!summary) return '';
|
|
235
|
+
|
|
236
|
+
let formatted = summary;
|
|
237
|
+
let verdictHtml = '';
|
|
238
|
+
|
|
239
|
+
// Extract verdict from first line
|
|
240
|
+
const lines = formatted.split('\n');
|
|
241
|
+
const firstLine = lines[0].trim();
|
|
242
|
+
|
|
243
|
+
if (firstLine.includes('REQUIRES FIXES') || firstLine.includes('🚫')) {
|
|
244
|
+
verdictHtml = '<div class="ai-verdict ai-verdict--danger"><span class="ai-verdict-icon">🚫</span><span class="ai-verdict-text">Requires Fixes</span><span class="ai-verdict-desc">Critical issues must be resolved before merging</span></div>';
|
|
245
|
+
formatted = lines.slice(1).join('\n').trim();
|
|
246
|
+
} else if (firstLine.includes('NEEDS ATTENTION') || firstLine.includes('⚠️')) {
|
|
247
|
+
verdictHtml = '<div class="ai-verdict ai-verdict--warning"><span class="ai-verdict-icon">⚠️</span><span class="ai-verdict-text">Needs Attention</span><span class="ai-verdict-desc">Review recommended but not blocking</span></div>';
|
|
248
|
+
formatted = lines.slice(1).join('\n').trim();
|
|
249
|
+
} else if (firstLine.includes('READY TO MERGE') || firstLine.includes('✅')) {
|
|
250
|
+
verdictHtml = '<div class="ai-verdict ai-verdict--success"><span class="ai-verdict-icon">✅</span><span class="ai-verdict-text">Ready to Merge</span><span class="ai-verdict-desc">Code quality meets standards</span></div>';
|
|
251
|
+
formatted = lines.slice(1).join('\n').trim();
|
|
363
252
|
}
|
|
364
253
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Generate footer
|
|
371
|
-
* @param {string} timestamp - Report timestamp
|
|
372
|
-
* @returns {string} Footer HTML
|
|
373
|
-
*/
|
|
374
|
-
function generateFooter(timestamp) {
|
|
375
|
-
return `
|
|
376
|
-
<footer class="footer">
|
|
377
|
-
<p>
|
|
378
|
-
Generated by <a href="https://github.com/sun-asterisk/engineer-excellence" target="_blank">SunLint</a>
|
|
379
|
-
on ${new Date(timestamp).toLocaleString()}
|
|
380
|
-
</p>
|
|
381
|
-
</footer>`;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Embed CSS styles
|
|
386
|
-
* @returns {string} Style tag
|
|
387
|
-
*/
|
|
388
|
-
function embedCSS() {
|
|
389
|
-
return `<style>
|
|
390
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
391
|
-
|
|
392
|
-
body {
|
|
393
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
394
|
-
line-height: 1.6;
|
|
395
|
-
color: #333;
|
|
396
|
-
background: #f5f5f5;
|
|
397
|
-
padding: 20px;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
.container {
|
|
401
|
-
max-width: 1400px;
|
|
402
|
-
margin: 0 auto;
|
|
403
|
-
background: white;
|
|
404
|
-
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
405
|
-
border-radius: 8px;
|
|
406
|
-
overflow: hidden;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
.header {
|
|
410
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
411
|
-
color: white;
|
|
412
|
-
padding: 30px;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
.header-content h1 {
|
|
416
|
-
font-size: 28px;
|
|
417
|
-
margin-bottom: 20px;
|
|
418
|
-
display: flex;
|
|
419
|
-
align-items: center;
|
|
420
|
-
gap: 10px;
|
|
421
|
-
}
|
|
254
|
+
// Highlight rule IDs (C001, C019, etc.)
|
|
255
|
+
formatted = formatted.replace(/\b(C\d{3})\b/g, '<span class="rule-tag">$1</span>');
|
|
422
256
|
|
|
423
|
-
|
|
257
|
+
// Highlight severity words
|
|
258
|
+
formatted = formatted.replace(/\b(critical|high|error|errors)\b/gi, '<span class="highlight-error">$1</span>');
|
|
259
|
+
formatted = formatted.replace(/\b(warning|warnings|medium)\b/gi, '<span class="highlight-warning">$1</span>');
|
|
424
260
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
428
|
-
gap: 15px;
|
|
429
|
-
margin-top: 20px;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
.meta-item {
|
|
433
|
-
background: rgba(255,255,255,0.1);
|
|
434
|
-
padding: 10px 15px;
|
|
435
|
-
border-radius: 5px;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
.meta-label {
|
|
439
|
-
display: block;
|
|
440
|
-
font-size: 12px;
|
|
441
|
-
opacity: 0.8;
|
|
442
|
-
margin-bottom: 5px;
|
|
443
|
-
}
|
|
261
|
+
// Split into sentences
|
|
262
|
+
const sentences = formatted.split(/(?<=[.!?])\s+/).filter(s => s.trim());
|
|
444
263
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
.summary {
|
|
452
|
-
padding: 30px;
|
|
453
|
-
background: #f9fafb;
|
|
454
|
-
border-bottom: 1px solid #e5e7eb;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
.summary-header {
|
|
458
|
-
display: flex;
|
|
459
|
-
justify-content: space-between;
|
|
460
|
-
align-items: center;
|
|
461
|
-
margin-bottom: 30px;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
.status-badge {
|
|
465
|
-
display: inline-flex;
|
|
466
|
-
align-items: center;
|
|
467
|
-
gap: 10px;
|
|
468
|
-
padding: 12px 24px;
|
|
469
|
-
border-radius: 8px;
|
|
470
|
-
font-size: 20px;
|
|
471
|
-
font-weight: 600;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
.status-passed { background: #d1fae5; color: #065f46; }
|
|
475
|
-
.status-warning { background: #fef3c7; color: #92400e; }
|
|
476
|
-
.status-failed { background: #fee2e2; color: #991b1b; }
|
|
477
|
-
|
|
478
|
-
.score-display {
|
|
479
|
-
text-align: center;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
.score-value {
|
|
483
|
-
font-size: 48px;
|
|
484
|
-
font-weight: bold;
|
|
485
|
-
color: #667eea;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
.score-grade {
|
|
489
|
-
font-size: 24px;
|
|
490
|
-
color: #666;
|
|
491
|
-
margin-top: -5px;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
.summary-stats {
|
|
495
|
-
display: grid;
|
|
496
|
-
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
497
|
-
gap: 20px;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
.stat-card {
|
|
501
|
-
background: white;
|
|
502
|
-
padding: 20px;
|
|
503
|
-
border-radius: 8px;
|
|
504
|
-
text-align: center;
|
|
505
|
-
border: 2px solid #e5e7eb;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
.stat-card.stat-error { border-color: #fca5a5; }
|
|
509
|
-
.stat-card.stat-warning { border-color: #fcd34d; }
|
|
510
|
-
|
|
511
|
-
.stat-value {
|
|
512
|
-
font-size: 36px;
|
|
513
|
-
font-weight: bold;
|
|
514
|
-
color: #667eea;
|
|
515
|
-
margin-bottom: 5px;
|
|
516
|
-
}
|
|
264
|
+
let contentHtml = '';
|
|
265
|
+
if (sentences.length > 1) {
|
|
266
|
+
const mainIssues = [];
|
|
267
|
+
const recommendations = [];
|
|
517
268
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
.section {
|
|
527
|
-
padding: 30px;
|
|
528
|
-
border-bottom: 1px solid #e5e7eb;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
.section h2 {
|
|
532
|
-
font-size: 22px;
|
|
533
|
-
margin-bottom: 20px;
|
|
534
|
-
color: #1f2937;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
.controls {
|
|
538
|
-
display: flex;
|
|
539
|
-
gap: 15px;
|
|
540
|
-
margin-bottom: 20px;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
.search-box, .filter-select {
|
|
544
|
-
padding: 10px 15px;
|
|
545
|
-
border: 2px solid #e5e7eb;
|
|
546
|
-
border-radius: 6px;
|
|
547
|
-
font-size: 14px;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
.search-box {
|
|
551
|
-
flex: 1;
|
|
552
|
-
max-width: 400px;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
.filter-select {
|
|
556
|
-
min-width: 150px;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
.violations-table {
|
|
560
|
-
width: 100%;
|
|
561
|
-
border-collapse: collapse;
|
|
562
|
-
background: white;
|
|
563
|
-
border: 1px solid #e5e7eb;
|
|
564
|
-
border-radius: 8px;
|
|
565
|
-
overflow: hidden;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
.violations-table th {
|
|
569
|
-
background: #f9fafb;
|
|
570
|
-
padding: 12px;
|
|
571
|
-
text-align: left;
|
|
572
|
-
font-weight: 600;
|
|
573
|
-
border-bottom: 2px solid #e5e7eb;
|
|
574
|
-
cursor: pointer;
|
|
575
|
-
user-select: none;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
.violations-table th:hover { background: #f3f4f6; }
|
|
579
|
-
|
|
580
|
-
.sort-icon {
|
|
581
|
-
font-size: 10px;
|
|
582
|
-
opacity: 0.5;
|
|
583
|
-
margin-left: 5px;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
.violations-table td {
|
|
587
|
-
padding: 12px;
|
|
588
|
-
border-bottom: 1px solid #e5e7eb;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
.file-path, .rule-id {
|
|
592
|
-
font-family: 'Courier New', monospace;
|
|
593
|
-
font-size: 13px;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
.count-cell {
|
|
597
|
-
text-align: center;
|
|
598
|
-
font-weight: 600;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
.count-cell.has-errors { color: #dc2626; }
|
|
602
|
-
.count-cell.has-warnings { color: #f59e0b; }
|
|
603
|
-
|
|
604
|
-
.details-btn {
|
|
605
|
-
background: #667eea;
|
|
606
|
-
color: white;
|
|
607
|
-
border: none;
|
|
608
|
-
padding: 6px 12px;
|
|
609
|
-
border-radius: 4px;
|
|
610
|
-
cursor: pointer;
|
|
611
|
-
font-size: 13px;
|
|
612
|
-
display: inline-flex;
|
|
613
|
-
align-items: center;
|
|
614
|
-
gap: 5px;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
.details-btn:hover { background: #5568d3; }
|
|
618
|
-
|
|
619
|
-
.arrow {
|
|
620
|
-
font-size: 10px;
|
|
621
|
-
transition: transform 0.2s;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
.details-row td {
|
|
625
|
-
background: #f9fafb;
|
|
626
|
-
padding: 0;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
.details-content {
|
|
630
|
-
padding: 20px;
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
.violation-list, .location-list {
|
|
634
|
-
list-style: none;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
.violation-item, .location-item {
|
|
638
|
-
padding: 10px;
|
|
639
|
-
margin-bottom: 8px;
|
|
640
|
-
background: white;
|
|
641
|
-
border-left: 3px solid #e5e7eb;
|
|
642
|
-
border-radius: 4px;
|
|
643
|
-
display: flex;
|
|
644
|
-
gap: 10px;
|
|
645
|
-
align-items: center;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
.severity-error { color: #dc2626; font-weight: 600; }
|
|
649
|
-
.severity-warning { color: #f59e0b; font-weight: 600; }
|
|
650
|
-
|
|
651
|
-
.violation-line {
|
|
652
|
-
background: #e5e7eb;
|
|
653
|
-
padding: 2px 8px;
|
|
654
|
-
border-radius: 3px;
|
|
655
|
-
font-size: 12px;
|
|
656
|
-
font-family: monospace;
|
|
269
|
+
for (const sentence of sentences) {
|
|
270
|
+
const lower = sentence.toLowerCase();
|
|
271
|
+
if (lower.includes('prioritize') || lower.includes('focus') || lower.includes('recommend') || lower.includes('should')) {
|
|
272
|
+
recommendations.push(sentence.trim());
|
|
273
|
+
} else {
|
|
274
|
+
mainIssues.push(sentence.trim());
|
|
275
|
+
}
|
|
657
276
|
}
|
|
658
277
|
|
|
659
|
-
.
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
278
|
+
if (mainIssues.length > 0) {
|
|
279
|
+
contentHtml += '<div class="issue-section">';
|
|
280
|
+
contentHtml += '<div class="section-title">🔍 Issues Found</div>';
|
|
281
|
+
contentHtml += '<ul>';
|
|
282
|
+
for (const issue of mainIssues) {
|
|
283
|
+
contentHtml += `<li>${issue}</li>`;
|
|
284
|
+
}
|
|
285
|
+
contentHtml += '</ul></div>';
|
|
663
286
|
}
|
|
664
287
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
288
|
+
if (recommendations.length > 0) {
|
|
289
|
+
contentHtml += '<div class="recommendation-section">';
|
|
290
|
+
contentHtml += '<div class="section-title">💡 Recommendations</div>';
|
|
291
|
+
contentHtml += '<ul>';
|
|
292
|
+
for (const rec of recommendations) {
|
|
293
|
+
contentHtml += `<li>${rec}</li>`;
|
|
294
|
+
}
|
|
295
|
+
contentHtml += '</ul></div>';
|
|
669
296
|
}
|
|
297
|
+
} else if (formatted) {
|
|
298
|
+
contentHtml = `<p>${formatted}</p>`;
|
|
299
|
+
}
|
|
670
300
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
background: #f3f4f6;
|
|
674
|
-
padding: 2px 8px;
|
|
675
|
-
border-radius: 3px;
|
|
676
|
-
font-size: 12px;
|
|
677
|
-
}
|
|
301
|
+
return verdictHtml + contentHtml;
|
|
302
|
+
}
|
|
678
303
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
304
|
+
/**
|
|
305
|
+
* Render template with data
|
|
306
|
+
* Simple template engine supporting {{var}}, {{#if}}, {{#each}}
|
|
307
|
+
*/
|
|
308
|
+
function renderTemplate(template, data) {
|
|
309
|
+
let result = template;
|
|
310
|
+
|
|
311
|
+
// Handle {{#each items}}...{{/each}}
|
|
312
|
+
result = result.replace(/\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (match, key, content) => {
|
|
313
|
+
const items = data[key];
|
|
314
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
315
|
+
return '';
|
|
316
|
+
}
|
|
317
|
+
return items.map(item => {
|
|
318
|
+
let itemContent = content;
|
|
319
|
+
// Replace item properties
|
|
320
|
+
for (const [propKey, propValue] of Object.entries(item)) {
|
|
321
|
+
const regex = new RegExp(`\\{\\{${propKey}\\}\\}`, 'g');
|
|
322
|
+
itemContent = itemContent.replace(regex, escapeHTML(String(propValue ?? '')));
|
|
323
|
+
}
|
|
324
|
+
return itemContent;
|
|
325
|
+
}).join('');
|
|
326
|
+
});
|
|
685
327
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
font-size: 14px;
|
|
691
|
-
background: #f9fafb;
|
|
692
|
-
}
|
|
328
|
+
// Handle {{#if condition}}...{{/if}}
|
|
329
|
+
result = result.replace(/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (match, key, content) => {
|
|
330
|
+
return data[key] ? content : '';
|
|
331
|
+
});
|
|
693
332
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
333
|
+
// Handle simple {{variable}} replacements
|
|
334
|
+
result = result.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
335
|
+
const value = data[key];
|
|
336
|
+
if (value === undefined || value === null) {
|
|
337
|
+
return '';
|
|
697
338
|
}
|
|
698
|
-
|
|
699
|
-
.
|
|
700
|
-
|
|
701
|
-
@media print {
|
|
702
|
-
body { background: white; padding: 0; }
|
|
703
|
-
.container { box-shadow: none; }
|
|
704
|
-
.details-btn { display: none; }
|
|
705
|
-
.details-row { display: table-row !important; }
|
|
339
|
+
// Don't escape class names, style values, or pre-formatted HTML
|
|
340
|
+
if (key.includes('class') || key.includes('color') || key.includes('bar') || key.endsWith('_html')) {
|
|
341
|
+
return String(value);
|
|
706
342
|
}
|
|
343
|
+
return escapeHTML(String(value));
|
|
344
|
+
});
|
|
707
345
|
|
|
708
|
-
|
|
709
|
-
.header-meta { grid-template-columns: 1fr; }
|
|
710
|
-
.summary-stats { grid-template-columns: 1fr; }
|
|
711
|
-
.controls { flex-direction: column; }
|
|
712
|
-
.search-box { max-width: 100%; }
|
|
713
|
-
}
|
|
714
|
-
</style>`;
|
|
346
|
+
return result;
|
|
715
347
|
}
|
|
716
348
|
|
|
717
349
|
/**
|
|
718
|
-
*
|
|
719
|
-
* @returns {string} Script tag
|
|
350
|
+
* Shorten file path for display
|
|
720
351
|
*/
|
|
721
|
-
function
|
|
722
|
-
return
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
const arrow = btn.querySelector('.arrow');
|
|
728
|
-
|
|
729
|
-
if (row.style.display === 'none') {
|
|
730
|
-
row.style.display = 'table-row';
|
|
731
|
-
arrow.style.transform = 'rotate(180deg)';
|
|
732
|
-
btn.innerHTML = 'Hide Details <span class="arrow" style="transform: rotate(180deg);">▼</span>';
|
|
733
|
-
} else {
|
|
734
|
-
row.style.display = 'none';
|
|
735
|
-
arrow.style.transform = 'rotate(0deg)';
|
|
736
|
-
btn.innerHTML = 'Show Details <span class="arrow">▼</span>';
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
// Sort table
|
|
741
|
-
function sortTable(tableId, columnIndex) {
|
|
742
|
-
const table = document.getElementById(tableId);
|
|
743
|
-
const tbody = table.querySelector('tbody');
|
|
744
|
-
const rows = Array.from(tbody.querySelectorAll('tr')).filter(r => !r.classList.contains('details-row'));
|
|
745
|
-
|
|
746
|
-
const isNumeric = columnIndex > 0;
|
|
747
|
-
rows.sort((a, b) => {
|
|
748
|
-
const aVal = a.cells[columnIndex].textContent.trim();
|
|
749
|
-
const bVal = b.cells[columnIndex].textContent.trim();
|
|
750
|
-
|
|
751
|
-
if (isNumeric) {
|
|
752
|
-
return parseInt(bVal) - parseInt(aVal);
|
|
753
|
-
}
|
|
754
|
-
return aVal.localeCompare(bVal);
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
rows.forEach(row => {
|
|
758
|
-
const detailsRow = row.nextElementSibling;
|
|
759
|
-
tbody.appendChild(row);
|
|
760
|
-
if (detailsRow && detailsRow.classList.contains('details-row')) {
|
|
761
|
-
tbody.appendChild(detailsRow);
|
|
762
|
-
}
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// File search
|
|
767
|
-
const fileSearch = document.getElementById('fileSearch');
|
|
768
|
-
if (fileSearch) {
|
|
769
|
-
fileSearch.addEventListener('input', function() {
|
|
770
|
-
const searchTerm = this.value.toLowerCase();
|
|
771
|
-
const rows = document.querySelectorAll('#fileTable tbody tr.file-row');
|
|
772
|
-
|
|
773
|
-
rows.forEach(row => {
|
|
774
|
-
const file = row.querySelector('.file-path').textContent.toLowerCase();
|
|
775
|
-
const detailsRow = row.nextElementSibling;
|
|
776
|
-
|
|
777
|
-
if (file.includes(searchTerm)) {
|
|
778
|
-
row.style.display = '';
|
|
779
|
-
if (detailsRow && detailsRow.style.display !== 'none') {
|
|
780
|
-
detailsRow.style.display = '';
|
|
781
|
-
}
|
|
782
|
-
} else {
|
|
783
|
-
row.style.display = 'none';
|
|
784
|
-
if (detailsRow) {
|
|
785
|
-
detailsRow.style.display = 'none';
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
});
|
|
789
|
-
});
|
|
790
|
-
}
|
|
352
|
+
function shortenPath(filePath) {
|
|
353
|
+
if (!filePath) return '';
|
|
354
|
+
const parts = filePath.split('/');
|
|
355
|
+
if (parts.length <= 3) return filePath;
|
|
356
|
+
return '.../' + parts.slice(-3).join('/');
|
|
357
|
+
}
|
|
791
358
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
let show = true;
|
|
805
|
-
if (filter === 'error' && errors === 0) show = false;
|
|
806
|
-
if (filter === 'warning' && warnings === 0) show = false;
|
|
807
|
-
|
|
808
|
-
if (show) {
|
|
809
|
-
row.style.display = '';
|
|
810
|
-
} else {
|
|
811
|
-
row.style.display = 'none';
|
|
812
|
-
if (detailsRow) {
|
|
813
|
-
detailsRow.style.display = 'none';
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
});
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
</script>`;
|
|
359
|
+
/**
|
|
360
|
+
* Get package version
|
|
361
|
+
*/
|
|
362
|
+
function getVersion() {
|
|
363
|
+
try {
|
|
364
|
+
const packagePath = path.join(__dirname, '..', 'package.json');
|
|
365
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
366
|
+
return pkg.version || '1.0.0';
|
|
367
|
+
} catch {
|
|
368
|
+
return '1.0.0';
|
|
369
|
+
}
|
|
820
370
|
}
|
|
821
371
|
|
|
822
372
|
/**
|
|
823
373
|
* Escape HTML special characters
|
|
824
|
-
* @param {string} text - Text to escape
|
|
825
|
-
* @returns {string} Escaped text
|
|
826
374
|
*/
|
|
827
375
|
function escapeHTML(text) {
|
|
828
376
|
if (!text) return '';
|
|
@@ -834,6 +382,53 @@ function escapeHTML(text) {
|
|
|
834
382
|
.replace(/'/g, ''');
|
|
835
383
|
}
|
|
836
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Fallback HTML generator if template fails
|
|
387
|
+
*/
|
|
388
|
+
function generateFallbackHTML(violations, metadata) {
|
|
389
|
+
const { score = {}, architecture, impact } = metadata;
|
|
390
|
+
const errorCount = violations.filter(v => v.severity === 'error').length;
|
|
391
|
+
const warningCount = violations.filter(v => v.severity === 'warning').length;
|
|
392
|
+
|
|
393
|
+
return `<!DOCTYPE html>
|
|
394
|
+
<html>
|
|
395
|
+
<head>
|
|
396
|
+
<meta charset="UTF-8">
|
|
397
|
+
<title>SunLint Report</title>
|
|
398
|
+
<style>
|
|
399
|
+
body { font-family: system-ui; padding: 2rem; background: #1e293b; color: #f1f5f9; }
|
|
400
|
+
.card { background: #334155; padding: 1.5rem; border-radius: 0.5rem; margin: 1rem 0; }
|
|
401
|
+
.error { color: #fca5a5; }
|
|
402
|
+
.warning { color: #fcd34d; }
|
|
403
|
+
h1 { color: #10b981; }
|
|
404
|
+
</style>
|
|
405
|
+
</head>
|
|
406
|
+
<body>
|
|
407
|
+
<h1>SunLint Report</h1>
|
|
408
|
+
<div class="card">
|
|
409
|
+
<h2>Code Quality</h2>
|
|
410
|
+
<p>Score: <strong>${score.score ?? 'N/A'}</strong>/100 (${score.grade ?? 'N/A'})</p>
|
|
411
|
+
<p><span class="error">${errorCount} errors</span> · <span class="warning">${warningCount} warnings</span></p>
|
|
412
|
+
</div>
|
|
413
|
+
${architecture ? `
|
|
414
|
+
<div class="card">
|
|
415
|
+
<h2>Architecture</h2>
|
|
416
|
+
<p>Pattern: <strong>${architecture.pattern?.toUpperCase() || 'UNKNOWN'}</strong></p>
|
|
417
|
+
<p>Health: ${architecture.healthScore ?? 0}/100</p>
|
|
418
|
+
</div>
|
|
419
|
+
` : ''}
|
|
420
|
+
${impact ? `
|
|
421
|
+
<div class="card">
|
|
422
|
+
<h2>Impact</h2>
|
|
423
|
+
<p>Severity: <strong>${impact.severity || 'LOW'}</strong></p>
|
|
424
|
+
<p>Score: ${impact.score ?? 0}/100</p>
|
|
425
|
+
</div>
|
|
426
|
+
` : ''}
|
|
427
|
+
<p style="margin-top: 2rem; color: #94a3b8;">Generated by SunLint</p>
|
|
428
|
+
</body>
|
|
429
|
+
</html>`;
|
|
430
|
+
}
|
|
431
|
+
|
|
837
432
|
module.exports = {
|
|
838
433
|
generateHTMLReport
|
|
839
434
|
};
|