@vasanth-mv/pqs-cli 1.1.0

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.
@@ -0,0 +1,187 @@
1
+ /**
2
+ * reportBuilder.js
3
+ * Generates a self-contained HTML quality report from analysis results.
4
+ * No external dependencies — everything is inline.
5
+ */
6
+
7
+ const SEV_COLOR = { critical: "#dc2626", warning: "#d97706", info: "#2563eb" };
8
+ const SEV_BG = { critical: "#fef2f2", warning: "#fff7ed", info: "#eff6ff" };
9
+ const SEV_BORDER= { critical: "#fecaca", warning: "#fed7aa", info: "#bfdbfe" };
10
+
11
+ function gradeLabel(score) {
12
+ if (score >= 90) return { label: "Excellent", color: "#059669" };
13
+ if (score >= 75) return { label: "Good", color: "#0d9488" };
14
+ if (score >= 60) return { label: "Fair", color: "#d97706" };
15
+ return { label: "Needs work", color: "#dc2626" };
16
+ }
17
+
18
+ function escHtml(str) {
19
+ return String(str ?? "")
20
+ .replace(/&/g, "&amp;").replace(/</g, "&lt;")
21
+ .replace(/>/g, "&gt;").replace(/"/g, "&quot;");
22
+ }
23
+
24
+ /**
25
+ * Build a self-contained HTML report.
26
+ *
27
+ * @param {{
28
+ * projectName: string,
29
+ * stackId: string,
30
+ * runAt: string,
31
+ * files: Array<{ name: string, result: object }>,
32
+ * threshold: number,
33
+ * passed: boolean,
34
+ * }} opts
35
+ * @returns {string} complete HTML string
36
+ */
37
+ export function buildHtmlReport({ projectName, stackId, runAt, files, threshold, passed }) {
38
+ const allFindings = files.flatMap((f) =>
39
+ (f.result?.findings ?? []).map((fi) => ({ ...fi, _file: f.name }))
40
+ );
41
+ const scores = files.map((f) => f.result?.overallScore ?? 0);
42
+ const avgScore = scores.length
43
+ ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length)
44
+ : 0;
45
+ const grade = gradeLabel(avgScore);
46
+ const critical = allFindings.filter((f) => f.severity === "critical").length;
47
+ const warnings = allFindings.filter((f) => f.severity === "warning").length;
48
+
49
+ const findingRows = allFindings.map((f) => `
50
+ <tr>
51
+ <td style="padding:8px 10px;font-size:11.5px;color:#374151;font-family:monospace;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escHtml(f._file)}">${escHtml(f._file.split("/").pop())}</td>
52
+ <td style="padding:8px 10px;text-align:center">
53
+ <span style="padding:2px 8px;border-radius:10px;font-size:10px;font-weight:700;background:${SEV_BG[f.severity]||"#f1f5f9"};color:${SEV_COLOR[f.severity]||"#374151"};border:1px solid ${SEV_BORDER[f.severity]||"#e2e8f0"}">${escHtml(f.severity?.toUpperCase())}</span>
54
+ </td>
55
+ <td style="padding:8px 10px;font-size:12px;font-weight:600;color:#0f172a">${escHtml(f.title)}</td>
56
+ <td style="padding:8px 10px;font-size:11px;color:#64748b;font-family:monospace">${escHtml(f.ruleId)}</td>
57
+ <td style="padding:8px 10px;font-size:11.5px;color:#374151">${f.line != null ? `Line ${f.line}` : "—"}</td>
58
+ <td style="padding:8px 10px;font-size:11px;color:#64748b;max-width:260px">${escHtml(f.description ?? f.fix ?? "")}</td>
59
+ </tr>`).join("");
60
+
61
+ const fileRows = files.map((f) => {
62
+ const s = f.result?.overallScore ?? 0;
63
+ const g = gradeLabel(s);
64
+ const crit = (f.result?.findings ?? []).filter((x) => x.severity === "critical").length;
65
+ return `
66
+ <tr>
67
+ <td style="padding:9px 12px;font-size:12px;color:#0f172a;font-family:monospace">${escHtml(f.name)}</td>
68
+ <td style="padding:9px 12px;text-align:center;font-size:20px;font-weight:800;color:${g.color}">${s}</td>
69
+ <td style="padding:9px 12px;font-size:12px;color:${g.color};font-weight:600">${g.label}</td>
70
+ <td style="padding:9px 12px;font-size:12px;color:${crit > 0 ? "#dc2626" : "#16a34a"};font-weight:600">${crit > 0 ? `${crit} critical` : "✓ none"}</td>
71
+ <td style="padding:9px 12px;font-size:12px;color:#64748b">${(f.result?.findings ?? []).length} findings</td>
72
+ </tr>`;
73
+ }).join("");
74
+
75
+ return `<!DOCTYPE html>
76
+ <html lang="en">
77
+ <head>
78
+ <meta charset="UTF-8"/>
79
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
80
+ <title>${escHtml(projectName)} — Quality Report</title>
81
+ <style>
82
+ *{box-sizing:border-box;margin:0;padding:0}
83
+ body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;color:#0f172a;-webkit-font-smoothing:antialiased}
84
+ .header{background:linear-gradient(135deg,#0f172a 0%,#1e3a5f 100%);color:#fff;padding:28px 40px}
85
+ .header h1{font-size:22px;font-weight:800;margin-bottom:4px}
86
+ .header .meta{font-size:12px;color:#94a3b8;margin-top:2px}
87
+ .container{max-width:1100px;margin:0 auto;padding:28px 24px}
88
+ .stat-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:14px;margin-bottom:24px}
89
+ .stat{background:#fff;border-radius:12px;padding:16px 18px;border:1px solid #e2e8f0;box-shadow:0 1px 4px rgba(0,0,0,.05)}
90
+ .stat-val{font-size:28px;font-weight:800;margin-bottom:2px}
91
+ .stat-label{font-size:11.5px;font-weight:600;color:#64748b}
92
+ .stat-sub{font-size:11px;color:#94a3b8;margin-top:2px}
93
+ .card{background:#fff;border-radius:12px;border:1px solid #e2e8f0;overflow:hidden;margin-bottom:20px;box-shadow:0 1px 4px rgba(0,0,0,.05)}
94
+ .card-header{padding:12px 18px;border-bottom:1px solid #f1f5f9;font-size:13px;font-weight:700;color:#0f172a;background:#fafafa;display:flex;align-items:center;justify-content:space-between}
95
+ .badge{padding:3px 10px;border-radius:12px;font-size:10.5px;font-weight:700}
96
+ table{width:100%;border-collapse:collapse}
97
+ tr:hover{background:#f8fafc}
98
+ th{padding:8px 10px;font-size:10.5px;font-weight:700;color:#64748b;text-align:left;text-transform:uppercase;letter-spacing:.04em;border-bottom:2px solid #f1f5f9;background:#fafafa}
99
+ .status-pass{color:#059669;font-weight:700;font-size:13px}
100
+ .status-fail{color:#dc2626;font-weight:700;font-size:13px}
101
+ .footer{text-align:center;padding:20px;font-size:11px;color:#94a3b8}
102
+ </style>
103
+ </head>
104
+ <body>
105
+ <div class="header">
106
+ <div style="display:flex;align-items:center;gap:16px;flex-wrap:wrap">
107
+ <div style="width:48px;height:48px;background:#0d9488;border-radius:14px;display:flex;align-items:center;justify-content:center;font-size:24px">⚡</div>
108
+ <div>
109
+ <h1>${escHtml(projectName)} — Code Quality Report</h1>
110
+ <div class="meta">Stack: ${escHtml(stackId)} · ${escHtml(files.length)} files · Generated ${escHtml(runAt)}</div>
111
+ </div>
112
+ <div style="margin-left:auto;text-align:right">
113
+ <div style="font-size:36px;font-weight:800;color:${grade.color}">${avgScore}</div>
114
+ <div style="font-size:12px;color:#94a3b8">Overall score · ${grade.label}</div>
115
+ <div class="${passed ? "status-pass" : "status-fail"}" style="margin-top:4px">${passed ? "✓ PASSED" : "✗ FAILED"} (threshold ${threshold})</div>
116
+ </div>
117
+ </div>
118
+ </div>
119
+
120
+ <div class="container">
121
+
122
+ <!-- Stats -->
123
+ <div class="stat-grid">
124
+ <div class="stat">
125
+ <div class="stat-val" style="color:${grade.color}">${avgScore}</div>
126
+ <div class="stat-label">Avg quality score</div>
127
+ <div class="stat-sub">${grade.label}</div>
128
+ </div>
129
+ <div class="stat">
130
+ <div class="stat-val" style="color:#374151">${files.length}</div>
131
+ <div class="stat-label">Files analysed</div>
132
+ <div class="stat-sub">${stackId}</div>
133
+ </div>
134
+ <div class="stat">
135
+ <div class="stat-val" style="color:${critical > 0 ? "#dc2626" : "#059669"}">${critical}</div>
136
+ <div class="stat-label">Critical issues</div>
137
+ <div class="stat-sub">must fix</div>
138
+ </div>
139
+ <div class="stat">
140
+ <div class="stat-val" style="color:${warnings > 0 ? "#d97706" : "#059669"}">${warnings}</div>
141
+ <div class="stat-label">Warnings</div>
142
+ <div class="stat-sub">should fix</div>
143
+ </div>
144
+ <div class="stat">
145
+ <div class="stat-val" style="color:#374151">${allFindings.length}</div>
146
+ <div class="stat-label">Total findings</div>
147
+ <div class="stat-sub">across all files</div>
148
+ </div>
149
+ </div>
150
+
151
+ <!-- File scores -->
152
+ <div class="card">
153
+ <div class="card-header">
154
+ File Scores
155
+ <span class="badge" style="background:${passed?"#f0fdf4":"#fef2f2"};color:${passed?"#059669":"#dc2626"};border:1px solid ${passed?"#86efac":"#fecaca"}">${passed ? "PASSED" : "BELOW THRESHOLD"}</span>
156
+ </div>
157
+ <table>
158
+ <thead><tr>
159
+ <th>File</th><th style="text-align:center">Score</th><th>Grade</th><th>Critical</th><th>Findings</th>
160
+ </tr></thead>
161
+ <tbody>${fileRows}</tbody>
162
+ </table>
163
+ </div>
164
+
165
+ <!-- All findings -->
166
+ ${allFindings.length > 0 ? `
167
+ <div class="card">
168
+ <div class="card-header">
169
+ All Findings
170
+ <span class="badge" style="background:#f1f5f9;color:#374151;border:1px solid #e2e8f0">${allFindings.length} total</span>
171
+ </div>
172
+ <table>
173
+ <thead><tr>
174
+ <th>File</th><th style="text-align:center">Severity</th><th>Title</th><th>Rule</th><th>Line</th><th>Description</th>
175
+ </tr></thead>
176
+ <tbody>${findingRows}</tbody>
177
+ </table>
178
+ </div>` : `
179
+ <div class="card">
180
+ <div style="padding:32px;text-align:center;color:#059669;font-size:16px;font-weight:700">🎉 No findings — all files pass!</div>
181
+ </div>`}
182
+
183
+ </div>
184
+ <div class="footer">Generated by @cqs/qcbot · Code Quality Studio · ${escHtml(runAt)}</div>
185
+ </body>
186
+ </html>`;
187
+ }
Binary file