@vibecodeqa/cli 0.16.0 → 0.18.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.
- package/README.md +73 -63
- package/dist/check-meta.d.ts +1 -0
- package/dist/check-meta.js +58 -6
- package/dist/cli.js +48 -10
- package/dist/detect.js +24 -2
- package/dist/fs-utils.d.ts +4 -0
- package/dist/fs-utils.js +12 -6
- package/dist/report/html.d.ts +18 -9
- package/dist/report/html.js +108 -68
- package/dist/report/pages.d.ts +4 -4
- package/dist/report/pages.js +165 -115
- package/dist/report/sarif.d.ts +3 -0
- package/dist/report/sarif.js +67 -0
- package/dist/report/styles.d.ts +1 -1
- package/dist/report/styles.js +105 -33
- package/dist/report/svg.d.ts +17 -0
- package/dist/report/svg.js +99 -0
- package/dist/runners/accessibility.d.ts +3 -0
- package/dist/runners/accessibility.js +85 -0
- package/dist/runners/architecture.d.ts +2 -0
- package/dist/runners/architecture.js +232 -20
- package/dist/runners/code-coherence.d.ts +17 -0
- package/dist/runners/code-coherence.js +39 -0
- package/dist/runners/complexity.js +7 -37
- package/dist/runners/confusion.js +3 -31
- package/dist/runners/context.js +9 -40
- package/dist/runners/dependencies.js +28 -0
- package/dist/runners/doc-coherence.d.ts +14 -0
- package/dist/runners/doc-coherence.js +48 -0
- package/dist/runners/docs.js +7 -32
- package/dist/runners/duplication.js +9 -37
- package/dist/runners/lint.js +17 -0
- package/dist/runners/performance.d.ts +10 -0
- package/dist/runners/performance.js +174 -0
- package/dist/runners/react.d.ts +3 -0
- package/dist/runners/react.js +86 -0
- package/dist/runners/secrets.js +8 -29
- package/dist/runners/security.js +15 -38
- package/dist/runners/standards.js +3 -36
- package/dist/runners/structure.js +35 -55
- package/dist/runners/testing.js +2 -36
- package/dist/runners/type-safety.d.ts +1 -1
- package/dist/runners/type-safety.js +19 -37
- package/dist/runners/types-check.d.ts +1 -1
- package/dist/runners/types-check.js +38 -20
- package/dist/types.d.ts +5 -5
- package/package.json +11 -10
package/dist/report/html.js
CHANGED
|
@@ -1,41 +1,44 @@
|
|
|
1
|
-
/** Generate a multi-page
|
|
1
|
+
/** Generate a multi-page HTML report as separate files.
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
* Top nav: Overview | Foundations | Quality |
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
3
|
+
* Layout:
|
|
4
|
+
* Top nav: Logo | Overview | Foundations | Quality | ... | Issues | Files
|
|
5
|
+
* Page-level navigation. Scrollable on mobile.
|
|
6
|
+
* Sidebar: CONTEXTUAL to current page — NOT a duplicate of top nav.
|
|
7
|
+
* Overview: score + category scores
|
|
8
|
+
* Category: individual checks with grades (click to jump)
|
|
9
|
+
* Issues: severity breakdown
|
|
10
|
+
* Files: summary stats
|
|
11
|
+
* Mobile: Hamburger toggles both top nav dropdown and sidebar panel.
|
|
10
12
|
*/
|
|
11
13
|
import { getCheckMeta } from "../check-meta.js";
|
|
12
14
|
import { e, fileLink, gc } from "./components.js";
|
|
13
|
-
import {
|
|
15
|
+
import { categoryPage, filesPage, issuesPage, overviewPage } from "./pages.js";
|
|
14
16
|
import { CSS } from "./styles.js";
|
|
15
|
-
const GROUPS = [
|
|
16
|
-
{ id: "foundations", label: "Foundations", checks: ["structure", "lint", "types", "type-safety", "standards"] },
|
|
17
|
-
{ id: "quality", label: "Quality", checks: ["complexity", "duplication", "error-handling", "docs"] },
|
|
18
|
-
{ id: "testing", label: "Testing", checks: ["testing"] },
|
|
19
|
-
{ id: "arch", label: "Architecture", checks: ["architecture"] },
|
|
20
|
-
{ id: "security", label: "Security", checks: ["secrets", "security", "dependencies"] },
|
|
21
|
-
{ id: "llm", label: "
|
|
17
|
+
export const GROUPS = [
|
|
18
|
+
{ id: "foundations", label: "Foundations", file: "foundations.html", checks: ["structure", "lint", "types", "type-safety", "standards"] },
|
|
19
|
+
{ id: "quality", label: "Quality", file: "quality.html", checks: ["complexity", "duplication", "error-handling", "react", "accessibility", "docs"] },
|
|
20
|
+
{ id: "testing", label: "Testing", file: "testing.html", checks: ["testing"] },
|
|
21
|
+
{ id: "arch", label: "Architecture", file: "architecture.html", checks: ["architecture", "performance"] },
|
|
22
|
+
{ id: "security", label: "Security", file: "security.html", checks: ["secrets", "security", "dependencies"] },
|
|
23
|
+
{ id: "llm", label: "AI Readiness", file: "ai-readiness.html", checks: ["confusion", "context"] },
|
|
24
|
+
{ id: "ai", label: "AI Analysis", file: "ai-analysis.html", checks: ["doc-coherence", "code-coherence"] },
|
|
22
25
|
];
|
|
23
|
-
export function
|
|
26
|
+
export function generatePages(report, historyDir) {
|
|
27
|
+
const pages = new Map();
|
|
24
28
|
const allChecks = report.checks;
|
|
25
29
|
const checkMap = new Map(allChecks.map((c) => [c.name, c]));
|
|
26
|
-
const active = allChecks.filter((c) => !c.details.skipped);
|
|
30
|
+
const active = allChecks.filter((c) => !c.details.skipped && !c.details.comingSoon);
|
|
27
31
|
const ru = report.meta.repoUrl;
|
|
28
32
|
const br = report.meta.branch;
|
|
29
33
|
const fl = (path, line) => fileLink(path, line, ru, br);
|
|
30
34
|
const totalIssues = allChecks.reduce((s, c) => s + c.issues.length, 0);
|
|
31
35
|
const proj = report.meta.cwd.split("/").pop() || "project";
|
|
32
|
-
// ── File heatmap: aggregate issues per file across all checks ──
|
|
33
36
|
const fileIssues = new Map();
|
|
34
37
|
for (const c of allChecks) {
|
|
35
38
|
for (const iss of c.issues) {
|
|
36
39
|
if (!iss.file)
|
|
37
40
|
continue;
|
|
38
|
-
const f = iss.file.split(":")[0];
|
|
41
|
+
const f = iss.file.split(":")[0];
|
|
39
42
|
const entry = fileIssues.get(f) || { errors: 0, warnings: 0, checks: new Set() };
|
|
40
43
|
if (iss.severity === "error")
|
|
41
44
|
entry.errors++;
|
|
@@ -48,42 +51,87 @@ export function generateHTML(report) {
|
|
|
48
51
|
const topFiles = [...fileIssues.entries()]
|
|
49
52
|
.map(([file, d]) => ({ file, total: d.errors + d.warnings, errors: d.errors, warnings: d.warnings, checks: [...d.checks] }))
|
|
50
53
|
.sort((a, b) => b.total - a.total)
|
|
51
|
-
.slice(0,
|
|
52
|
-
// ── Category averages ──
|
|
54
|
+
.slice(0, 30);
|
|
53
55
|
const catScores = GROUPS.map((g) => {
|
|
54
56
|
const checks = g.checks.map((n) => checkMap.get(n)).filter(Boolean);
|
|
55
57
|
const scored = checks.filter((c) => !c.details.skipped);
|
|
56
58
|
const avg = scored.length > 0 ? Math.round(scored.reduce((s, c) => s + c.score, 0) / scored.length) : 0;
|
|
57
59
|
return { ...g, avg, checks };
|
|
58
60
|
});
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
const w = (id, sidebar, content) => wrap(proj, id, report, totalIssues, sidebar, content);
|
|
62
|
+
// ── Overview: sidebar shows score + category summary ──
|
|
63
|
+
const overviewSidebar = sidebarScore(report)
|
|
64
|
+
+ catScores.map((cs) => {
|
|
65
|
+
const isPremium = cs.checks.every((c) => c.details.comingSoon);
|
|
66
|
+
const clr = isPremium ? "#6366f1" : gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
|
|
67
|
+
const label = isPremium
|
|
68
|
+
? `<span class="pro-badge" style="font-size:0.5rem;padding:0.08rem 0.35rem">PRO</span>`
|
|
69
|
+
: `<span style="color:${clr}">${cs.avg}</span>`;
|
|
70
|
+
return `<a class="side-cat" href="${cs.file}">${cs.label} ${label}</a>`;
|
|
71
|
+
}).join("")
|
|
72
|
+
+ sidebarViews(totalIssues, fileIssues.size);
|
|
73
|
+
pages.set("index.html", w("overview", overviewSidebar, overviewPage(report, active, totalIssues, catScores, allChecks, topFiles, fl, historyDir)));
|
|
74
|
+
// ── Category pages: sidebar shows the checks within this category ──
|
|
75
|
+
for (let i = 0; i < GROUPS.length; i++) {
|
|
76
|
+
const g = GROUPS[i];
|
|
77
|
+
const cs = catScores[i];
|
|
78
|
+
const catSidebar = sidebarScore(report)
|
|
79
|
+
+ `<div class="side-section"><div class="side-cat-title">${cs.label}</div>`
|
|
80
|
+
+ cs.checks.map((c) => {
|
|
81
|
+
const sk = c.details.skipped;
|
|
82
|
+
const premium = c.details.comingSoon;
|
|
83
|
+
const meta = getCheckMeta(c.name);
|
|
84
|
+
const badge = premium ? `<span style="color:#6366f1">PRO</span>` : `<span style="color:${sk ? "#555" : gc(c.grade)}">${sk ? "\u2014" : c.grade} ${sk ? "" : c.score}</span>`;
|
|
85
|
+
return `<a class="side-check" onclick="var t=document.querySelector('[data-sub=\\'${cs.id}-${c.name}\\']');if(t)sub(t,'${cs.id}')" title="${e(meta.label)}">${badge} ${e(meta.label)}</a>`;
|
|
86
|
+
}).join("")
|
|
87
|
+
+ `</div>`
|
|
88
|
+
+ sidebarViews(totalIssues, fileIssues.size);
|
|
89
|
+
pages.set(g.file, w(g.id, catSidebar, categoryPage(cs, fl)));
|
|
90
|
+
}
|
|
91
|
+
// ── Issues: sidebar shows severity breakdown ──
|
|
92
|
+
const allIssuesList = allChecks.flatMap((c) => c.issues);
|
|
93
|
+
const errCount = allIssuesList.filter((i) => i.severity === "error").length;
|
|
94
|
+
const warnCount = allIssuesList.filter((i) => i.severity === "warning").length;
|
|
95
|
+
const infoCount = allIssuesList.filter((i) => i.severity === "info").length;
|
|
96
|
+
const issuesSidebar = sidebarScore(report)
|
|
97
|
+
+ `<div class="side-section"><div class="side-cat-title">Breakdown</div>`
|
|
98
|
+
+ `<div class="side-stat"><span style="color:var(--fail)">${errCount}</span> errors</div>`
|
|
99
|
+
+ `<div class="side-stat"><span style="color:var(--warn)">${warnCount}</span> warnings</div>`
|
|
100
|
+
+ `<div class="side-stat"><span style="color:var(--info)">${infoCount}</span> info</div>`
|
|
101
|
+
+ `</div>`
|
|
102
|
+
+ sidebarViews(totalIssues, fileIssues.size);
|
|
103
|
+
pages.set("issues.html", w("issues", issuesSidebar, issuesPage(allChecks, totalIssues, fl)));
|
|
104
|
+
// ── Files: sidebar shows file stats ──
|
|
105
|
+
const filesSidebar = sidebarScore(report)
|
|
106
|
+
+ `<div class="side-section"><div class="side-cat-title">File Health</div>`
|
|
107
|
+
+ `<div class="side-stat"><span style="color:var(--text)">${fileIssues.size}</span> files with issues</div>`
|
|
108
|
+
+ `<div class="side-stat"><span style="color:var(--fail)">${topFiles.filter(f => f.errors > 0).length}</span> with errors</div>`
|
|
109
|
+
+ `</div>`
|
|
110
|
+
+ sidebarViews(totalIssues, fileIssues.size);
|
|
111
|
+
pages.set("files.html", w("files", filesSidebar, filesPage(topFiles, fileIssues, fl)));
|
|
112
|
+
return pages;
|
|
113
|
+
}
|
|
114
|
+
export function generateHTML(report, historyDir) {
|
|
115
|
+
return generatePages(report, historyDir).get("index.html");
|
|
116
|
+
}
|
|
117
|
+
// ── Sidebar fragments ──
|
|
118
|
+
function sidebarScore(report) {
|
|
119
|
+
return `<div class="side-section"><div class="side-label">Score</div><div class="side-score" style="color:${gc(report.grade)}">${report.grade} ${report.score}</div></div>`;
|
|
120
|
+
}
|
|
121
|
+
function sidebarViews(totalIssues, fileCount) {
|
|
122
|
+
return `<div class="side-section side-views"><div class="side-label" style="margin-top:0.3rem">Views</div><a class="side-check" href="issues.html">Issues <span style="color:var(--muted)">${totalIssues}</span></a><a class="side-check" href="files.html">Files <span style="color:var(--muted)">${fileCount}</span></a></div>`;
|
|
123
|
+
}
|
|
124
|
+
// ── Page wrapper ──
|
|
125
|
+
function wrap(proj, currentId, report, totalIssues, sidebar, content) {
|
|
126
|
+
const navItems = [
|
|
127
|
+
{ id: "overview", label: "Overview", file: "index.html" },
|
|
128
|
+
...GROUPS.map((g) => ({ id: g.id, label: g.label, file: g.file })),
|
|
129
|
+
{ id: "issues", label: `Issues (${totalIssues})`, file: "issues.html" },
|
|
130
|
+
{ id: "files", label: "Files", file: "files.html" },
|
|
66
131
|
];
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
const sidebar = catScores
|
|
70
|
-
.map((cs) => {
|
|
71
|
-
const clr = gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
|
|
72
|
-
return `<div class="side-section"><a class="side-cat" onclick="go('${cs.id}')">${cs.label} <span style="color:${clr}">${cs.avg}</span></a>${cs.checks
|
|
73
|
-
.map((c) => {
|
|
74
|
-
const sk = c.details.skipped;
|
|
75
|
-
const meta = getCheckMeta(c.name);
|
|
76
|
-
return `<a class="side-check" onclick="go('${cs.id}')" title="${e(meta.label)}"><span style="color:${sk ? "#555" : gc(c.grade)}">${sk ? "\u2014" : c.grade}</span> ${e(meta.label)}</a>`;
|
|
77
|
-
})
|
|
78
|
-
.join("")}</div>`;
|
|
79
|
-
})
|
|
132
|
+
const nav = navItems
|
|
133
|
+
.map((t) => `<a class="tn${t.id === currentId ? " active" : ""}" href="${t.file}">${t.label}</a>`)
|
|
80
134
|
.join("");
|
|
81
|
-
// ── Assemble pages ──
|
|
82
|
-
const overview = overviewPage(report, active, totalIssues, catScores);
|
|
83
|
-
const catPages = categoryPages(catScores, fl);
|
|
84
|
-
const issues = issuesPage(allChecks, totalIssues, fl);
|
|
85
|
-
const files = filesPage(topFiles, fl);
|
|
86
|
-
const heatmap = heatmapPage(fileIssues, fl);
|
|
87
135
|
return `<!DOCTYPE html>
|
|
88
136
|
<html lang="en">
|
|
89
137
|
<head>
|
|
@@ -95,44 +143,36 @@ export function generateHTML(report) {
|
|
|
95
143
|
<body>
|
|
96
144
|
|
|
97
145
|
<nav class="top">
|
|
98
|
-
<
|
|
99
|
-
|
|
146
|
+
<a class="logo" href="index.html"><span>VibeCode</span> QA</a>
|
|
147
|
+
<button class="hamburger" onclick="toggleMenu()" aria-label="Menu">☰</button>
|
|
148
|
+
<div class="nav-scroll">${nav}</div>
|
|
100
149
|
</nav>
|
|
101
150
|
|
|
102
|
-
<aside class="side">
|
|
103
|
-
|
|
104
|
-
${sidebar}
|
|
105
|
-
</aside>
|
|
151
|
+
<aside class="side" id="sidebar">${sidebar}</aside>
|
|
152
|
+
|
|
106
153
|
<div class="content">
|
|
107
|
-
${
|
|
108
|
-
${catPages}
|
|
109
|
-
${issues}
|
|
110
|
-
${files}
|
|
111
|
-
${heatmap}
|
|
154
|
+
${content}
|
|
112
155
|
<div class="footer">Generated by <a href="https://vibecodeqa.online">VibeCode QA</a> v${report.version} — <code>npx @vibecodeqa/cli</code></div>
|
|
113
156
|
</div>
|
|
114
157
|
|
|
115
158
|
<script>
|
|
116
|
-
function
|
|
117
|
-
document.
|
|
118
|
-
document.
|
|
119
|
-
window.scrollTo(0,0);
|
|
159
|
+
function toggleMenu(){
|
|
160
|
+
document.querySelector('.nav-scroll').classList.toggle('open');
|
|
161
|
+
document.getElementById('sidebar').classList.toggle('open');
|
|
120
162
|
}
|
|
121
163
|
function sub(el,cat){
|
|
122
164
|
const id=el.dataset.sub;
|
|
123
165
|
el.parentElement.querySelectorAll('.sn').forEach(n=>n.classList.remove('active'));
|
|
124
166
|
el.classList.add('active');
|
|
125
|
-
document.querySelectorAll('
|
|
167
|
+
document.querySelectorAll('.sp').forEach(s=>{s.classList.toggle('active',s.dataset.sub===id)});
|
|
126
168
|
}
|
|
127
|
-
// Copy-prompt buttons — read from data-attribute (no inline JS with user data)
|
|
128
169
|
document.addEventListener('click',function(ev){
|
|
129
170
|
var btn=ev.target.closest('.cp-btn');
|
|
130
171
|
if(!btn)return;
|
|
131
|
-
|
|
172
|
+
var text=btn.dataset.prompt||'';
|
|
173
|
+
try{navigator.clipboard.writeText(text)}catch(e){var ta=document.createElement('textarea');ta.value=text;ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta)}
|
|
132
174
|
btn.textContent='\\u2713';setTimeout(function(){btn.textContent='\\ud83d\\udccb'},1000);
|
|
133
175
|
});
|
|
134
|
-
// Init: show overview
|
|
135
|
-
document.querySelector('.tn').classList.add('active');
|
|
136
176
|
</script>
|
|
137
177
|
</body></html>`;
|
|
138
178
|
}
|
package/dist/report/pages.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { CheckResult, VibeReport } from "../types.js";
|
|
|
3
3
|
export interface CatScore {
|
|
4
4
|
id: string;
|
|
5
5
|
label: string;
|
|
6
|
+
file?: string;
|
|
6
7
|
checks: CheckResult[];
|
|
7
8
|
avg: number;
|
|
8
9
|
}
|
|
@@ -14,11 +15,10 @@ export interface FileEntry {
|
|
|
14
15
|
checks: string[];
|
|
15
16
|
}
|
|
16
17
|
type FL = (path: string, line?: number) => string;
|
|
17
|
-
export declare function overviewPage(report: VibeReport, active: CheckResult[], totalIssues: number, catScores: CatScore[]): string;
|
|
18
|
-
export declare function
|
|
18
|
+
export declare function overviewPage(report: VibeReport, active: CheckResult[], totalIssues: number, catScores: CatScore[], allChecks: CheckResult[], topFiles: FileEntry[], fl: FL, historyDir?: string): string;
|
|
19
|
+
export declare function categoryPage(cs: CatScore, fl: FL): string;
|
|
19
20
|
export declare function issuesPage(allChecks: CheckResult[], totalIssues: number, fl: FL): string;
|
|
20
|
-
export declare function filesPage(topFiles: FileEntry[],
|
|
21
|
-
export declare function heatmapPage(fileIssues: Map<string, {
|
|
21
|
+
export declare function filesPage(topFiles: FileEntry[], fileIssues: Map<string, {
|
|
22
22
|
errors: number;
|
|
23
23
|
warnings: number;
|
|
24
24
|
checks: Set<string>;
|
package/dist/report/pages.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
/** Page renderers for the HTML report. */
|
|
2
2
|
import { getCheckMeta } from "../check-meta.js";
|
|
3
|
-
import {
|
|
3
|
+
import { loadHistory } from "../history.js";
|
|
4
|
+
import { generateArchSVG, generateDSM, generatePackageDiagram } from "../runners/architecture.js";
|
|
4
5
|
import { e, gc, pc } from "./components.js";
|
|
5
|
-
import { buildRadar, buildRing } from "./svg.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
import { buildPyramid, buildRadar, buildRing, buildTimeline } from "./svg.js";
|
|
7
|
+
// ── Overview ──────────────────────────────────────────────────────────
|
|
8
|
+
export function overviewPage(report, active, totalIssues, catScores, allChecks, topFiles, fl, historyDir) {
|
|
9
|
+
const hero = `<div class="hero">
|
|
10
|
+
${buildRing(report.score, gc(report.grade))}
|
|
11
|
+
<div class="hc">
|
|
12
|
+
<span class="hg" style="color:${gc(report.grade)}">${report.grade}</span>
|
|
13
|
+
<span class="hs" style="color:${gc(report.grade)}">${report.score}/100</span>
|
|
14
|
+
<span class="hd">${active.length} checks \u00b7 ${totalIssues} issues \u00b7 ${report.meta.duration}ms</span>
|
|
15
|
+
</div>
|
|
16
|
+
</div>`;
|
|
17
|
+
const scoredCats = catScores.filter((cs) => cs.checks.some((c) => !c.details.skipped && !c.details.comingSoon));
|
|
18
|
+
const radarSvg = scoredCats.length >= 3 ? buildRadar(scoredCats.map((cs) => ({ label: cs.label, score: cs.avg }))) : "";
|
|
14
19
|
const catCards = catScores
|
|
15
20
|
.map((cs) => {
|
|
16
21
|
const clr = gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
|
|
@@ -20,147 +25,192 @@ export function overviewPage(report, active, totalIssues, catScores) {
|
|
|
20
25
|
return `<span class="mc" style="color:${sk ? "#555" : gc(c.grade)}" title="${e(c.name)}: ${sk ? "skip" : c.score}">${sk ? "\u2014" : c.grade}</span>`;
|
|
21
26
|
})
|
|
22
27
|
.join("");
|
|
23
|
-
|
|
28
|
+
const href = cs.file || `${cs.id}.html`;
|
|
29
|
+
return `<a class="cc" href="${href}"><div class="cc-s" style="color:${clr}">${cs.avg}</div><div class="cc-l">${cs.label}</div><div class="cc-m">${mini}</div></a>`;
|
|
30
|
+
})
|
|
31
|
+
.join("");
|
|
32
|
+
let timelineSection = "";
|
|
33
|
+
if (historyDir) {
|
|
34
|
+
const history = loadHistory(historyDir);
|
|
35
|
+
if (history.length >= 2) {
|
|
36
|
+
const timelineSvg = buildTimeline(history.map((h) => ({ score: h.score, timestamp: h.timestamp })));
|
|
37
|
+
timelineSection = `<div class="ov-section"><h3>Score Timeline</h3><div class="timeline">${timelineSvg}</div></div>`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const barChart = active
|
|
41
|
+
.sort((a, b) => a.score - b.score)
|
|
42
|
+
.map((c) => {
|
|
43
|
+
return `<div class="brow"><span class="bl">${e(c.name)}</span><div class="bb"><div class="bf" style="width:${c.score}%;background:${gc(c.grade)}"></div></div><span class="bv" style="color:${gc(c.grade)}">${c.grade} ${c.score}</span></div>`;
|
|
24
44
|
})
|
|
25
45
|
.join("");
|
|
26
|
-
const
|
|
27
|
-
|
|
46
|
+
const allIssues = allChecks.flatMap((c) => c.issues.map((i) => ({ check: c.name, ...i })));
|
|
47
|
+
const sortedIssues = allIssues
|
|
48
|
+
.sort((a, b) => (a.severity === "error" ? 0 : a.severity === "warning" ? 1 : 2) - (b.severity === "error" ? 0 : b.severity === "warning" ? 1 : 2))
|
|
49
|
+
.slice(0, 10);
|
|
50
|
+
let topIssuesHtml = "";
|
|
51
|
+
if (sortedIssues.length > 0) {
|
|
52
|
+
const rows = sortedIssues
|
|
53
|
+
.map((i) => {
|
|
54
|
+
const loc = i.file ? fl(i.file.split(":")[0], i.line) : "";
|
|
55
|
+
return `<div class="ov-issue ${i.severity}"><span class="is">${i.severity[0].toUpperCase()}</span><span class="ov-check">${e(i.check)}</span>${loc ? `<span class="ov-loc">${loc}</span>` : ""}<span class="ov-msg">${e(i.message)}</span></div>`;
|
|
56
|
+
})
|
|
57
|
+
.join("");
|
|
58
|
+
const viewAll = allIssues.length > 10 ? `<a class="ov-link" href="issues.html">View all ${allIssues.length} issues \u2192</a>` : "";
|
|
59
|
+
topIssuesHtml = `<div class="ov-section"><h3>Top Issues</h3>${rows}${viewAll}</div>`;
|
|
60
|
+
}
|
|
61
|
+
let fileHotspotsHtml = "";
|
|
62
|
+
if (topFiles.length > 0) {
|
|
63
|
+
const fileRows = topFiles.slice(0, 5).map((f) => {
|
|
64
|
+
const pct = Math.min(100, f.total * 5);
|
|
65
|
+
return `<div class="fr"><span class="ff">${fl(f.file)}</span><div class="fb"><div class="fbf" style="width:${pct}%;background:${f.errors > 0 ? "var(--fail)" : "var(--warn)"}"></div></div><span class="fv">${f.errors}E ${f.warnings}W</span></div>`;
|
|
66
|
+
}).join("");
|
|
67
|
+
const viewAll = topFiles.length > 5 ? `<a class="ov-link" href="files.html">View all ${topFiles.length} files \u2192</a>` : "";
|
|
68
|
+
fileHotspotsHtml = `<div class="ov-section"><h3>File Hotspots</h3>${fileRows}${viewAll}</div>`;
|
|
69
|
+
}
|
|
70
|
+
const stackHtml = Object.entries(report.meta.stack)
|
|
71
|
+
.filter(([, v]) => v !== "none" && v !== "unknown")
|
|
72
|
+
.map(([k, v]) => `<span>${k}: <b>${v}</b></span>`)
|
|
73
|
+
.join("");
|
|
74
|
+
return `
|
|
28
75
|
<div class="dash">
|
|
29
|
-
|
|
30
|
-
${buildRing(ringPct, gc(report.grade))}
|
|
31
|
-
<div class="hc"><span class="hg" style="color:${gc(report.grade)}">${report.grade}</span><span class="hs" style="color:${gc(report.grade)}">${report.score}/100</span><span class="hd">${active.length} checks \u00b7 ${totalIssues} issues \u00b7 ${report.meta.duration}ms</span></div>
|
|
32
|
-
</div>
|
|
76
|
+
${hero}
|
|
33
77
|
<div class="radar">${radarSvg}</div>
|
|
34
78
|
</div>
|
|
35
79
|
<div class="cats">${catCards}</div>
|
|
36
|
-
|
|
37
|
-
<div class="bars">${barChart}</div>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
.join("")}</div>
|
|
42
|
-
</div>`;
|
|
80
|
+
${timelineSection}
|
|
81
|
+
<div class="ov-section"><h3>All Checks</h3><div class="bars">${barChart}</div></div>
|
|
82
|
+
${topIssuesHtml}
|
|
83
|
+
${fileHotspotsHtml}
|
|
84
|
+
<div class="stack">${stackHtml}</div>`;
|
|
43
85
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
86
|
+
// ── Single category page ──────────────────────────────────────────
|
|
87
|
+
export function categoryPage(cs, fl) {
|
|
88
|
+
const subNav = cs.checks
|
|
89
|
+
.map((c, i) => {
|
|
90
|
+
const sk = c.details.skipped;
|
|
91
|
+
const premium = c.details.comingSoon;
|
|
92
|
+
const badge = premium ? "PRO" : sk ? "\u2014" : c.grade;
|
|
93
|
+
const clr = premium ? "#6366f1" : sk ? "#555" : gc(c.grade);
|
|
94
|
+
return `<a class="sn${i === 0 ? " active" : ""}${premium ? " sn-pro" : ""}" data-sub="${cs.id}-${c.name}" onclick="sub(this,'${cs.id}')">${e(c.name)} <span style="color:${clr}">${badge}</span></a>`;
|
|
95
|
+
})
|
|
96
|
+
.join("");
|
|
97
|
+
const subPages = cs.checks
|
|
98
|
+
.map((c, i) => {
|
|
99
|
+
const meta = getCheckMeta(c.name);
|
|
100
|
+
const sk = c.details.skipped;
|
|
101
|
+
const premium = c.details.comingSoon;
|
|
102
|
+
const detailsFiltered = Object.entries(c.details)
|
|
103
|
+
.filter(([k]) => k !== "skipped" && k !== "reason" && k !== "graph")
|
|
104
|
+
.map(([k, v]) => {
|
|
105
|
+
const d = Array.isArray(v) ? v.join(", ") : typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
106
|
+
return `<div class="kv"><span class="k">${e(k)}</span><span class="v">${e(d)}</span></div>`;
|
|
51
107
|
})
|
|
52
108
|
.join("");
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
.
|
|
59
|
-
.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
.
|
|
64
|
-
// Group issues by file
|
|
65
|
-
const byFile = new Map();
|
|
66
|
-
const noFile = [];
|
|
67
|
-
for (const iss of c.issues) {
|
|
68
|
-
const f = iss.file?.split(":")[0];
|
|
69
|
-
if (f) {
|
|
70
|
-
const arr = byFile.get(f) || [];
|
|
71
|
-
arr.push(iss);
|
|
72
|
-
byFile.set(f, arr);
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
noFile.push(iss);
|
|
76
|
-
}
|
|
109
|
+
const byFile = new Map();
|
|
110
|
+
const noFile = [];
|
|
111
|
+
for (const iss of c.issues) {
|
|
112
|
+
const f = iss.file?.split(":")[0];
|
|
113
|
+
if (f) {
|
|
114
|
+
const arr = byFile.get(f) || [];
|
|
115
|
+
arr.push(iss);
|
|
116
|
+
byFile.set(f, arr);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
noFile.push(iss);
|
|
77
120
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
issuesHtml += `</div>`;
|
|
121
|
+
}
|
|
122
|
+
let issuesHtml = "";
|
|
123
|
+
for (const [file, issues] of byFile) {
|
|
124
|
+
issuesHtml += `<div class="fg"><div class="fn">${fl(file)} <span class="fc">${issues.length}</span></div>`;
|
|
125
|
+
for (const iss of issues) {
|
|
126
|
+
const prompt = `Fix this issue in ${file}${iss.line ? `:${iss.line}` : ""}\n${iss.severity}: ${iss.message}${iss.rule ? ` (${iss.rule})` : ""}\nCheck: ${c.name}`;
|
|
127
|
+
issuesHtml += `<div class="ir ${iss.severity}"><span class="is">${iss.severity[0].toUpperCase()}</span>${iss.line ? `<span class="il">${iss.line}</span>` : ""}<span class="im">${e(iss.message)}</span>${iss.rule ? `<span class="iru">${e(iss.rule)}</span>` : ""}<button class="cp-btn" data-prompt="${e(prompt)}" title="Copy fix prompt">\ud83d\udccb</button></div>`;
|
|
86
128
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
issuesHtml +=
|
|
129
|
+
issuesHtml += `</div>`;
|
|
130
|
+
}
|
|
131
|
+
if (noFile.length > 0) {
|
|
132
|
+
issuesHtml += `<div class="fg"><div class="fn">General</div>`;
|
|
133
|
+
for (const iss of noFile) {
|
|
134
|
+
issuesHtml += `<div class="ir ${iss.severity}"><span class="is">${iss.severity[0].toUpperCase()}</span><span class="im">${e(iss.message)}</span>${iss.rule ? `<span class="iru">${e(iss.rule)}</span>` : ""}</div>`;
|
|
93
135
|
}
|
|
136
|
+
issuesHtml += `</div>`;
|
|
137
|
+
}
|
|
138
|
+
if (premium) {
|
|
139
|
+
const det = c.details;
|
|
140
|
+
const desc = det.description || meta.description;
|
|
141
|
+
const detailKvs = Object.entries(det)
|
|
142
|
+
.filter(([k]) => !["premium", "comingSoon", "reason", "description"].includes(k))
|
|
143
|
+
.map(([k, v]) => `<div class="kv"><span class="k">${e(k)}</span><span class="v">${e(Array.isArray(v) ? v.join(", ") : String(v))}</span></div>`)
|
|
144
|
+
.join("");
|
|
94
145
|
return `<div class="sp${i === 0 ? " active" : ""}" data-sub="${cs.id}-${c.name}">
|
|
146
|
+
<div class="pro-card">
|
|
147
|
+
<div class="pro-badge">PRO</div>
|
|
148
|
+
<h3 style="margin-bottom:0.5rem;color:var(--text)">${e(meta.label)}</h3>
|
|
149
|
+
<p class="pro-desc">${e(desc)}</p>
|
|
150
|
+
${meta.risk ? `<div class="info-panel"><div class="ip-row"><span class="ip-label">Risk</span><span>${e(meta.risk)}</span></div></div>` : ""}
|
|
151
|
+
${detailKvs ? `<div class="kvs" style="margin-top:0.8rem">${detailKvs}</div>` : ""}
|
|
152
|
+
<p class="pro-cta">Coming soon with VibeCode QA Pro</p>
|
|
153
|
+
</div>
|
|
154
|
+
</div>`;
|
|
155
|
+
}
|
|
156
|
+
return `<div class="sp${i === 0 ? " active" : ""}" data-sub="${cs.id}-${c.name}">
|
|
95
157
|
<div class="ch-head"><span class="ch-g" style="color:${sk ? "#555" : gc(c.grade)}">${sk ? "\u2014" : c.grade}</span><div><b>${e(meta.label)}</b><span class="ch-s">${sk ? "skipped" : `${c.score}/100`} \u00b7 weight ${meta.weight}% \u00b7 ${c.duration}ms \u00b7 ${c.issues.length} issues</span></div><span class="pri" style="color:${pc(meta.priority)}">${meta.priority}</span></div>
|
|
96
158
|
${meta.description ? `<div class="info-panel"><div class="ip-row"><span class="ip-label">What</span><span>${e(meta.description)}</span></div><div class="ip-row"><span class="ip-label">Risk</span><span>${e(meta.risk)}</span></div><div class="ip-row"><span class="ip-label">Fix</span><span>${e(meta.recommendation)}</span></div></div>` : ""}
|
|
97
159
|
${sk ? `<p class="skip-r">${e(c.details.reason || "skipped")}</p>` : ""}
|
|
98
|
-
${c.name === "architecture" && !sk ? `<div class="arch-svg">${generateArchSVG(c.details)}</div>` : ""}
|
|
160
|
+
${c.name === "architecture" && !sk ? `<h3 style="margin-top:1.5rem">Dependency Graph</h3><div class="arch-svg">${generateArchSVG(c.details)}</div><h3 style="margin-top:1.5rem">Package Diagram</h3><div class="arch-svg">${generatePackageDiagram(c.details)}</div><h3 style="margin-top:1.5rem">Dependency Matrix (DSM)</h3><div class="arch-svg">${generateDSM(c.details)}</div>` : ""}
|
|
161
|
+
${c.name === "testing" && !sk && c.details.pyramid ? `<div class="arch-svg">${buildPyramid(c.details.pyramid)}</div>` : ""}
|
|
99
162
|
${detailsFiltered ? `<div class="kvs">${detailsFiltered}</div>` : ""}
|
|
100
163
|
${issuesHtml ? `<div class="iss-list">${issuesHtml}</div>` : '<p style="color:var(--muted);font-size:0.8rem;margin-top:1rem">No issues found.</p>'}
|
|
101
164
|
</div>`;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
165
|
+
})
|
|
166
|
+
.join("");
|
|
167
|
+
const clr = gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
|
|
168
|
+
return `
|
|
106
169
|
<div class="cat-head"><span style="color:${clr};font-size:1.8rem;font-weight:900">${cs.avg}</span><span style="color:${clr}">/100</span><span style="color:var(--muted);margin-left:0.5rem">${cs.label}</span></div>
|
|
107
170
|
<div class="bar2"><div class="bf2" style="width:${cs.avg}%;background:${clr}"></div></div>
|
|
108
171
|
<div class="sub-nav">${subNav}</div>
|
|
109
|
-
${subPages}
|
|
110
|
-
</div>`;
|
|
111
|
-
}
|
|
112
|
-
return catPagesHtml;
|
|
172
|
+
${subPages}`;
|
|
113
173
|
}
|
|
174
|
+
// ── Issues view ──────────────────────────────────────────
|
|
114
175
|
export function issuesPage(allChecks, totalIssues, fl) {
|
|
115
176
|
const allIssues = allChecks.flatMap((c) => c.issues.map((i) => ({ check: c.name, ...i })));
|
|
177
|
+
const errorCount = allIssues.filter((i) => i.severity === "error").length;
|
|
178
|
+
const warnCount = allIssues.filter((i) => i.severity === "warning").length;
|
|
179
|
+
const infoCount = allIssues.filter((i) => i.severity === "info").length;
|
|
116
180
|
const issueRows = allIssues
|
|
181
|
+
.sort((a, b) => (a.severity === "error" ? 0 : a.severity === "warning" ? 1 : 2) - (b.severity === "error" ? 0 : b.severity === "warning" ? 1 : 2))
|
|
117
182
|
.slice(0, 200)
|
|
118
183
|
.map((i) => {
|
|
119
184
|
const loc = i.file ? fl(i.file.split(":")[0], i.line) : "";
|
|
120
185
|
return `<tr class="${i.severity}"><td class="is2">${i.severity[0].toUpperCase()}</td><td class="ic2">${e(i.check)}</td><td class="il2">${loc}</td><td>${e(i.message)}</td><td class="iru2">${e(i.rule || "")}</td></tr>`;
|
|
121
186
|
})
|
|
122
187
|
.join("");
|
|
123
|
-
return
|
|
188
|
+
return `
|
|
124
189
|
<h2>All Issues <span style="color:var(--muted);font-weight:400">${totalIssues}</span></h2>
|
|
125
|
-
<div class="isf">${
|
|
190
|
+
<div class="isf"><span style="color:var(--fail)">${errorCount} errors</span> \u00b7 <span style="color:var(--warn)">${warnCount} warnings</span>${infoCount > 0 ? ` \u00b7 <span style="color:var(--info)">${infoCount} info</span>` : ""}</div>
|
|
126
191
|
<table class="it"><thead><tr><th></th><th>Check</th><th>Location</th><th>Message</th><th>Rule</th></tr></thead><tbody>${issueRows}</tbody></table>
|
|
127
|
-
${allIssues.length > 200 ? `<p style="color:var(--muted);text-align:center;margin-top:1rem">Showing 200 of ${allIssues.length}</p>` : ""}
|
|
128
|
-
</div>`;
|
|
192
|
+
${allIssues.length > 200 ? `<p style="color:var(--muted);text-align:center;margin-top:1rem">Showing 200 of ${allIssues.length}</p>` : ""}`;
|
|
129
193
|
}
|
|
130
|
-
|
|
131
|
-
|
|
194
|
+
// ── Files view ───────────────────────────────────────────
|
|
195
|
+
export function filesPage(topFiles, fileIssues, fl) {
|
|
196
|
+
if (topFiles.length === 0) {
|
|
197
|
+
return `<h2>File Health</h2><p style="color:var(--muted)">No file-level issues found.</p>`;
|
|
198
|
+
}
|
|
199
|
+
const maxIssues = Math.max(...topFiles.map((f) => f.total));
|
|
200
|
+
const heatmapRows = topFiles
|
|
201
|
+
.slice(0, 30)
|
|
132
202
|
.map((f) => {
|
|
133
|
-
const
|
|
134
|
-
|
|
203
|
+
const intensity = maxIssues > 0 ? f.total / maxIssues : 0;
|
|
204
|
+
const r = Math.round(239 * intensity);
|
|
205
|
+
const g = Math.round(68 * (1 - intensity) + 197 * (f.errors === 0 ? 0.3 : 0));
|
|
206
|
+
const color = `rgb(${r},${g},30)`;
|
|
207
|
+
const barW = Math.max(4, Math.round(intensity * 200));
|
|
208
|
+
return `<div class="hm-row"><span class="hm-name">${fl(f.file)}</span><div class="hm-bar" style="width:${barW}px;background:${color}" title="${f.total} issues (${f.checks.join(", ")})"></div><span class="hm-count">${f.errors}E ${f.warnings}W</span><span class="hm-checks">${f.checks.join(", ")}</span></div>`;
|
|
135
209
|
})
|
|
136
210
|
.join("");
|
|
137
|
-
return
|
|
138
|
-
<h2>File
|
|
139
|
-
<p style="color:var(--muted);font-size:0.78rem;margin-bottom:1rem"
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
export function heatmapPage(fileIssues, fl) {
|
|
144
|
-
const heatmapFiles = [...fileIssues.entries()].sort((a, b) => b[1].errors + b[1].warnings - a[1].errors - a[1].warnings).slice(0, 30);
|
|
145
|
-
let heatmapHtml = "";
|
|
146
|
-
if (heatmapFiles.length > 0) {
|
|
147
|
-
const maxIssues = Math.max(...heatmapFiles.map(([, d]) => d.errors + d.warnings));
|
|
148
|
-
heatmapHtml = heatmapFiles
|
|
149
|
-
.map(([file, d]) => {
|
|
150
|
-
const total = d.errors + d.warnings;
|
|
151
|
-
const intensity = maxIssues > 0 ? total / maxIssues : 0;
|
|
152
|
-
const r = Math.round(239 * intensity); // red channel
|
|
153
|
-
const g = Math.round(68 * (1 - intensity) + 197 * (d.errors === 0 ? 0.3 : 0)); // green
|
|
154
|
-
const color = `rgb(${r},${g},30)`;
|
|
155
|
-
const barW = Math.max(4, Math.round(intensity * 200));
|
|
156
|
-
const checks = [...d.checks].join(", ");
|
|
157
|
-
return `<div class="hm-row"><span class="hm-name">${fl(file)}</span><div class="hm-bar" style="width:${barW}px;background:${color}" title="${total} issues (${checks})"></div><span class="hm-count">${d.errors}E ${d.warnings}W</span></div>`;
|
|
158
|
-
})
|
|
159
|
-
.join("");
|
|
160
|
-
}
|
|
161
|
-
return `<div id="p-heatmap" class="page">
|
|
162
|
-
<h2>Code Heatmap</h2>
|
|
163
|
-
<p style="color:var(--muted);font-size:0.78rem;margin-bottom:1rem">Visual density of issues per file. Red = errors, orange = warnings. Bar width = relative issue count.</p>
|
|
164
|
-
${heatmapHtml || '<p style="color:var(--muted)">No issues to visualize.</p>'}
|
|
165
|
-
</div>`;
|
|
211
|
+
return `
|
|
212
|
+
<h2>File Health</h2>
|
|
213
|
+
<p style="color:var(--muted);font-size:0.78rem;margin-bottom:1rem">${fileIssues.size} files with issues across ${topFiles.reduce((s, f) => { for (const c of f.checks)
|
|
214
|
+
s.add(c); return s; }, new Set()).size} checks.</p>
|
|
215
|
+
${heatmapRows}`;
|
|
166
216
|
}
|