@vibecodeqa/cli 0.17.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 +34 -2
- package/dist/cli.js +35 -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 +17 -10
- package/dist/report/html.js +106 -73
- package/dist/report/pages.d.ts +2 -1
- package/dist/report/pages.js +88 -82
- 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 +82 -36
- 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.js +15 -10
- 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,37 +1,38 @@
|
|
|
1
|
-
/** Generate a multi-page
|
|
1
|
+
/** Generate a multi-page HTML report as separate files.
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Sidebar:
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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.
|
|
12
12
|
*/
|
|
13
13
|
import { getCheckMeta } from "../check-meta.js";
|
|
14
14
|
import { e, fileLink, gc } from "./components.js";
|
|
15
|
-
import {
|
|
15
|
+
import { categoryPage, filesPage, issuesPage, overviewPage } from "./pages.js";
|
|
16
16
|
import { CSS } from "./styles.js";
|
|
17
|
-
const GROUPS = [
|
|
18
|
-
{ id: "foundations", label: "Foundations", checks: ["structure", "lint", "types", "type-safety", "standards"] },
|
|
19
|
-
{ id: "quality", label: "Quality", checks: ["complexity", "duplication", "error-handling", "react", "accessibility", "docs"] },
|
|
20
|
-
{ id: "testing", label: "Testing", checks: ["testing"] },
|
|
21
|
-
{ id: "arch", label: "Architecture", checks: ["architecture"] },
|
|
22
|
-
{ id: "security", label: "Security", checks: ["secrets", "security", "dependencies"] },
|
|
23
|
-
{ id: "llm", label: "AI Readiness", checks: ["confusion", "context"] },
|
|
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"] },
|
|
24
25
|
];
|
|
25
|
-
export function
|
|
26
|
+
export function generatePages(report, historyDir) {
|
|
27
|
+
const pages = new Map();
|
|
26
28
|
const allChecks = report.checks;
|
|
27
29
|
const checkMap = new Map(allChecks.map((c) => [c.name, c]));
|
|
28
|
-
const active = allChecks.filter((c) => !c.details.skipped);
|
|
30
|
+
const active = allChecks.filter((c) => !c.details.skipped && !c.details.comingSoon);
|
|
29
31
|
const ru = report.meta.repoUrl;
|
|
30
32
|
const br = report.meta.branch;
|
|
31
33
|
const fl = (path, line) => fileLink(path, line, ru, br);
|
|
32
34
|
const totalIssues = allChecks.reduce((s, c) => s + c.issues.length, 0);
|
|
33
35
|
const proj = report.meta.cwd.split("/").pop() || "project";
|
|
34
|
-
// ── Aggregate file issues across all checks ──
|
|
35
36
|
const fileIssues = new Map();
|
|
36
37
|
for (const c of allChecks) {
|
|
37
38
|
for (const iss of c.issues) {
|
|
@@ -51,45 +52,86 @@ export function generateHTML(report, historyDir) {
|
|
|
51
52
|
.map(([file, d]) => ({ file, total: d.errors + d.warnings, errors: d.errors, warnings: d.warnings, checks: [...d.checks] }))
|
|
52
53
|
.sort((a, b) => b.total - a.total)
|
|
53
54
|
.slice(0, 30);
|
|
54
|
-
// ── Category averages ──
|
|
55
55
|
const catScores = GROUPS.map((g) => {
|
|
56
56
|
const checks = g.checks.map((n) => checkMap.get(n)).filter(Boolean);
|
|
57
57
|
const scored = checks.filter((c) => !c.details.skipped);
|
|
58
58
|
const avg = scored.length > 0 ? Math.round(scored.reduce((s, c) => s + c.score, 0) / scored.length) : 0;
|
|
59
59
|
return { ...g, avg, checks };
|
|
60
60
|
});
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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" },
|
|
65
131
|
];
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
const viewNav = [
|
|
69
|
-
{ id: "issues", label: `Issues (${totalIssues})` },
|
|
70
|
-
{ id: "files", label: "Files" },
|
|
71
|
-
]
|
|
72
|
-
.map((t) => `<a class="tn tn-view" data-page="${t.id}" onclick="go('${t.id}')">${t.label}</a>`)
|
|
132
|
+
const nav = navItems
|
|
133
|
+
.map((t) => `<a class="tn${t.id === currentId ? " active" : ""}" href="${t.file}">${t.label}</a>`)
|
|
73
134
|
.join("");
|
|
74
|
-
// ── Sidebar ──
|
|
75
|
-
const sidebarDims = catScores
|
|
76
|
-
.map((cs) => {
|
|
77
|
-
const clr = gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
|
|
78
|
-
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
|
|
79
|
-
.map((c) => {
|
|
80
|
-
const sk = c.details.skipped;
|
|
81
|
-
const meta = getCheckMeta(c.name);
|
|
82
|
-
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>`;
|
|
83
|
-
})
|
|
84
|
-
.join("")}</div>`;
|
|
85
|
-
})
|
|
86
|
-
.join("");
|
|
87
|
-
const sidebarViews = `<div class="side-section side-views"><div class="side-views-label">Views</div><a class="side-check" onclick="go('issues')">Issues <span style="color:var(--muted)">${totalIssues}</span></a><a class="side-check" onclick="go('files')">Files <span style="color:var(--muted)">${fileIssues.size}</span></a></div>`;
|
|
88
|
-
// ── Assemble pages ──
|
|
89
|
-
const overview = overviewPage(report, active, totalIssues, catScores, allChecks, topFiles, fl, historyDir);
|
|
90
|
-
const catPages = categoryPages(catScores, fl);
|
|
91
|
-
const issues = issuesPage(allChecks, totalIssues, fl);
|
|
92
|
-
const files = filesPage(topFiles, fileIssues, fl);
|
|
93
135
|
return `<!DOCTYPE html>
|
|
94
136
|
<html lang="en">
|
|
95
137
|
<head>
|
|
@@ -101,45 +143,36 @@ export function generateHTML(report, historyDir) {
|
|
|
101
143
|
<body>
|
|
102
144
|
|
|
103
145
|
<nav class="top">
|
|
104
|
-
<
|
|
105
|
-
<
|
|
106
|
-
<div class="nav-
|
|
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>
|
|
107
149
|
</nav>
|
|
108
150
|
|
|
109
|
-
<aside class="side">
|
|
110
|
-
|
|
111
|
-
${sidebarDims}
|
|
112
|
-
${sidebarViews}
|
|
113
|
-
</aside>
|
|
151
|
+
<aside class="side" id="sidebar">${sidebar}</aside>
|
|
152
|
+
|
|
114
153
|
<div class="content">
|
|
115
|
-
${
|
|
116
|
-
${catPages}
|
|
117
|
-
${issues}
|
|
118
|
-
${files}
|
|
154
|
+
${content}
|
|
119
155
|
<div class="footer">Generated by <a href="https://vibecodeqa.online">VibeCode QA</a> v${report.version} — <code>npx @vibecodeqa/cli</code></div>
|
|
120
156
|
</div>
|
|
121
157
|
|
|
122
158
|
<script>
|
|
123
|
-
function
|
|
124
|
-
document.
|
|
125
|
-
document.
|
|
126
|
-
window.scrollTo(0,0);
|
|
159
|
+
function toggleMenu(){
|
|
160
|
+
document.querySelector('.nav-scroll').classList.toggle('open');
|
|
161
|
+
document.getElementById('sidebar').classList.toggle('open');
|
|
127
162
|
}
|
|
128
163
|
function sub(el,cat){
|
|
129
164
|
const id=el.dataset.sub;
|
|
130
165
|
el.parentElement.querySelectorAll('.sn').forEach(n=>n.classList.remove('active'));
|
|
131
166
|
el.classList.add('active');
|
|
132
|
-
document.querySelectorAll('
|
|
167
|
+
document.querySelectorAll('.sp').forEach(s=>{s.classList.toggle('active',s.dataset.sub===id)});
|
|
133
168
|
}
|
|
134
|
-
// Copy-prompt buttons
|
|
135
169
|
document.addEventListener('click',function(ev){
|
|
136
170
|
var btn=ev.target.closest('.cp-btn');
|
|
137
171
|
if(!btn)return;
|
|
138
|
-
|
|
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)}
|
|
139
174
|
btn.textContent='\\u2713';setTimeout(function(){btn.textContent='\\ud83d\\udccb'},1000);
|
|
140
175
|
});
|
|
141
|
-
// Init: show overview
|
|
142
|
-
document.querySelector('.tn').classList.add('active');
|
|
143
176
|
</script>
|
|
144
177
|
</body></html>`;
|
|
145
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
|
}
|
|
@@ -15,7 +16,7 @@ export interface FileEntry {
|
|
|
15
16
|
}
|
|
16
17
|
type FL = (path: string, line?: number) => string;
|
|
17
18
|
export declare function overviewPage(report: VibeReport, active: CheckResult[], totalIssues: number, catScores: CatScore[], allChecks: CheckResult[], topFiles: FileEntry[], fl: FL, historyDir?: string): string;
|
|
18
|
-
export declare function
|
|
19
|
+
export declare function categoryPage(cs: CatScore, fl: FL): string;
|
|
19
20
|
export declare function issuesPage(allChecks: CheckResult[], totalIssues: number, fl: FL): string;
|
|
20
21
|
export declare function filesPage(topFiles: FileEntry[], fileIssues: Map<string, {
|
|
21
22
|
errors: number;
|
package/dist/report/pages.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/** Page renderers for the HTML report. */
|
|
2
2
|
import { getCheckMeta } from "../check-meta.js";
|
|
3
3
|
import { loadHistory } from "../history.js";
|
|
4
|
-
import { generateArchSVG } from "../runners/architecture.js";
|
|
4
|
+
import { generateArchSVG, generateDSM, generatePackageDiagram } from "../runners/architecture.js";
|
|
5
5
|
import { e, gc, pc } from "./components.js";
|
|
6
6
|
import { buildPyramid, buildRadar, buildRing, buildTimeline } from "./svg.js";
|
|
7
7
|
// ── Overview ──────────────────────────────────────────────────────────
|
|
8
8
|
export function overviewPage(report, active, totalIssues, catScores, allChecks, topFiles, fl, historyDir) {
|
|
9
|
-
// Hero: score ring + grade
|
|
10
9
|
const hero = `<div class="hero">
|
|
11
10
|
${buildRing(report.score, gc(report.grade))}
|
|
12
11
|
<div class="hc">
|
|
@@ -15,9 +14,8 @@ export function overviewPage(report, active, totalIssues, catScores, allChecks,
|
|
|
15
14
|
<span class="hd">${active.length} checks \u00b7 ${totalIssues} issues \u00b7 ${report.meta.duration}ms</span>
|
|
16
15
|
</div>
|
|
17
16
|
</div>`;
|
|
18
|
-
|
|
19
|
-
const radarSvg = buildRadar(
|
|
20
|
-
// Category cards
|
|
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 }))) : "";
|
|
21
19
|
const catCards = catScores
|
|
22
20
|
.map((cs) => {
|
|
23
21
|
const clr = gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
|
|
@@ -27,10 +25,10 @@ export function overviewPage(report, active, totalIssues, catScores, allChecks,
|
|
|
27
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>`;
|
|
28
26
|
})
|
|
29
27
|
.join("");
|
|
30
|
-
|
|
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>`;
|
|
31
30
|
})
|
|
32
31
|
.join("");
|
|
33
|
-
// Score timeline (from history)
|
|
34
32
|
let timelineSection = "";
|
|
35
33
|
if (historyDir) {
|
|
36
34
|
const history = loadHistory(historyDir);
|
|
@@ -39,14 +37,12 @@ export function overviewPage(report, active, totalIssues, catScores, allChecks,
|
|
|
39
37
|
timelineSection = `<div class="ov-section"><h3>Score Timeline</h3><div class="timeline">${timelineSvg}</div></div>`;
|
|
40
38
|
}
|
|
41
39
|
}
|
|
42
|
-
// All checks bar chart
|
|
43
40
|
const barChart = active
|
|
44
41
|
.sort((a, b) => a.score - b.score)
|
|
45
42
|
.map((c) => {
|
|
46
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>`;
|
|
47
44
|
})
|
|
48
45
|
.join("");
|
|
49
|
-
// Top issues preview (10 most severe)
|
|
50
46
|
const allIssues = allChecks.flatMap((c) => c.issues.map((i) => ({ check: c.name, ...i })));
|
|
51
47
|
const sortedIssues = allIssues
|
|
52
48
|
.sort((a, b) => (a.severity === "error" ? 0 : a.severity === "warning" ? 1 : 2) - (b.severity === "error" ? 0 : b.severity === "warning" ? 1 : 2))
|
|
@@ -59,25 +55,23 @@ export function overviewPage(report, active, totalIssues, catScores, allChecks,
|
|
|
59
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>`;
|
|
60
56
|
})
|
|
61
57
|
.join("");
|
|
62
|
-
const viewAll = allIssues.length > 10 ? `<a class="ov-link"
|
|
58
|
+
const viewAll = allIssues.length > 10 ? `<a class="ov-link" href="issues.html">View all ${allIssues.length} issues \u2192</a>` : "";
|
|
63
59
|
topIssuesHtml = `<div class="ov-section"><h3>Top Issues</h3>${rows}${viewAll}</div>`;
|
|
64
60
|
}
|
|
65
|
-
// File hotspots preview (top 5)
|
|
66
61
|
let fileHotspotsHtml = "";
|
|
67
62
|
if (topFiles.length > 0) {
|
|
68
63
|
const fileRows = topFiles.slice(0, 5).map((f) => {
|
|
69
64
|
const pct = Math.min(100, f.total * 5);
|
|
70
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>`;
|
|
71
66
|
}).join("");
|
|
72
|
-
const viewAll = topFiles.length > 5 ? `<a class="ov-link"
|
|
67
|
+
const viewAll = topFiles.length > 5 ? `<a class="ov-link" href="files.html">View all ${topFiles.length} files \u2192</a>` : "";
|
|
73
68
|
fileHotspotsHtml = `<div class="ov-section"><h3>File Hotspots</h3>${fileRows}${viewAll}</div>`;
|
|
74
69
|
}
|
|
75
|
-
// Stack badges
|
|
76
70
|
const stackHtml = Object.entries(report.meta.stack)
|
|
77
71
|
.filter(([, v]) => v !== "none" && v !== "unknown")
|
|
78
72
|
.map(([k, v]) => `<span>${k}: <b>${v}</b></span>`)
|
|
79
73
|
.join("");
|
|
80
|
-
return
|
|
74
|
+
return `
|
|
81
75
|
<div class="dash">
|
|
82
76
|
${hero}
|
|
83
77
|
<div class="radar">${radarSvg}</div>
|
|
@@ -87,82 +81,97 @@ ${timelineSection}
|
|
|
87
81
|
<div class="ov-section"><h3>All Checks</h3><div class="bars">${barChart}</div></div>
|
|
88
82
|
${topIssuesHtml}
|
|
89
83
|
${fileHotspotsHtml}
|
|
90
|
-
<div class="stack">${stackHtml}</div
|
|
91
|
-
</div>`;
|
|
84
|
+
<div class="stack">${stackHtml}</div>`;
|
|
92
85
|
}
|
|
93
|
-
// ──
|
|
94
|
-
export function
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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>`;
|
|
101
107
|
})
|
|
102
108
|
.join("");
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
.
|
|
109
|
-
.
|
|
110
|
-
|
|
111
|
-
return `<div class="kv"><span class="k">${e(k)}</span><span class="v">${e(d)}</span></div>`;
|
|
112
|
-
})
|
|
113
|
-
.join("");
|
|
114
|
-
// Group issues by file
|
|
115
|
-
const byFile = new Map();
|
|
116
|
-
const noFile = [];
|
|
117
|
-
for (const iss of c.issues) {
|
|
118
|
-
const f = iss.file?.split(":")[0];
|
|
119
|
-
if (f) {
|
|
120
|
-
const arr = byFile.get(f) || [];
|
|
121
|
-
arr.push(iss);
|
|
122
|
-
byFile.set(f, arr);
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
noFile.push(iss);
|
|
126
|
-
}
|
|
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);
|
|
127
117
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
issuesHtml += `<div class="fg"><div class="fn">${fl(file)} <span class="fc">${issues.length}</span></div>`;
|
|
131
|
-
for (const iss of issues) {
|
|
132
|
-
const prompt = `Fix this issue in ${file}${iss.line ? `:${iss.line}` : ""}\n${iss.severity}: ${iss.message}${iss.rule ? ` (${iss.rule})` : ""}\nCheck: ${c.name}`;
|
|
133
|
-
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>`;
|
|
134
|
-
}
|
|
135
|
-
issuesHtml += `</div>`;
|
|
118
|
+
else {
|
|
119
|
+
noFile.push(iss);
|
|
136
120
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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>`;
|
|
128
|
+
}
|
|
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>`;
|
|
143
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("");
|
|
144
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}">
|
|
145
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>
|
|
146
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>` : ""}
|
|
147
159
|
${sk ? `<p class="skip-r">${e(c.details.reason || "skipped")}</p>` : ""}
|
|
148
|
-
${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>` : ""}
|
|
149
161
|
${c.name === "testing" && !sk && c.details.pyramid ? `<div class="arch-svg">${buildPyramid(c.details.pyramid)}</div>` : ""}
|
|
150
162
|
${detailsFiltered ? `<div class="kvs">${detailsFiltered}</div>` : ""}
|
|
151
163
|
${issuesHtml ? `<div class="iss-list">${issuesHtml}</div>` : '<p style="color:var(--muted);font-size:0.8rem;margin-top:1rem">No issues found.</p>'}
|
|
152
164
|
</div>`;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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 `
|
|
157
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>
|
|
158
170
|
<div class="bar2"><div class="bf2" style="width:${cs.avg}%;background:${clr}"></div></div>
|
|
159
171
|
<div class="sub-nav">${subNav}</div>
|
|
160
|
-
${subPages}
|
|
161
|
-
</div>`;
|
|
162
|
-
}
|
|
163
|
-
return catPagesHtml;
|
|
172
|
+
${subPages}`;
|
|
164
173
|
}
|
|
165
|
-
// ── Issues view
|
|
174
|
+
// ── Issues view ──────────────────────────────────────────
|
|
166
175
|
export function issuesPage(allChecks, totalIssues, fl) {
|
|
167
176
|
const allIssues = allChecks.flatMap((c) => c.issues.map((i) => ({ check: c.name, ...i })));
|
|
168
177
|
const errorCount = allIssues.filter((i) => i.severity === "error").length;
|
|
@@ -176,19 +185,17 @@ export function issuesPage(allChecks, totalIssues, fl) {
|
|
|
176
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>`;
|
|
177
186
|
})
|
|
178
187
|
.join("");
|
|
179
|
-
return
|
|
188
|
+
return `
|
|
180
189
|
<h2>All Issues <span style="color:var(--muted);font-weight:400">${totalIssues}</span></h2>
|
|
181
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>
|
|
182
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>
|
|
183
|
-
${allIssues.length > 200 ? `<p style="color:var(--muted);text-align:center;margin-top:1rem">Showing 200 of ${allIssues.length}</p>` : ""}
|
|
184
|
-
</div>`;
|
|
192
|
+
${allIssues.length > 200 ? `<p style="color:var(--muted);text-align:center;margin-top:1rem">Showing 200 of ${allIssues.length}</p>` : ""}`;
|
|
185
193
|
}
|
|
186
|
-
// ── Files view
|
|
194
|
+
// ── Files view ───────────────────────────────────────────
|
|
187
195
|
export function filesPage(topFiles, fileIssues, fl) {
|
|
188
196
|
if (topFiles.length === 0) {
|
|
189
|
-
return `<
|
|
197
|
+
return `<h2>File Health</h2><p style="color:var(--muted)">No file-level issues found.</p>`;
|
|
190
198
|
}
|
|
191
|
-
// Heatmap (visual density bars)
|
|
192
199
|
const maxIssues = Math.max(...topFiles.map((f) => f.total));
|
|
193
200
|
const heatmapRows = topFiles
|
|
194
201
|
.slice(0, 30)
|
|
@@ -201,10 +208,9 @@ export function filesPage(topFiles, fileIssues, fl) {
|
|
|
201
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>`;
|
|
202
209
|
})
|
|
203
210
|
.join("");
|
|
204
|
-
return
|
|
211
|
+
return `
|
|
205
212
|
<h2>File Health</h2>
|
|
206
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)
|
|
207
|
-
s.add(c); return s; }, new Set()).size} checks
|
|
208
|
-
${heatmapRows}
|
|
209
|
-
</div>`;
|
|
214
|
+
s.add(c); return s; }, new Set()).size} checks.</p>
|
|
215
|
+
${heatmapRows}`;
|
|
210
216
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** SARIF 2.1.0 output for GitHub Code Scanning integration. */
|
|
2
|
+
import { getCheckMeta } from "../check-meta.js";
|
|
3
|
+
export function generateSARIF(report) {
|
|
4
|
+
const rules = [];
|
|
5
|
+
const ruleIndex = new Map();
|
|
6
|
+
const results = [];
|
|
7
|
+
for (const check of report.checks) {
|
|
8
|
+
const meta = getCheckMeta(check.name);
|
|
9
|
+
for (const issue of check.issues) {
|
|
10
|
+
const ruleId = issue.rule || check.name;
|
|
11
|
+
// Register rule if not seen
|
|
12
|
+
if (!ruleIndex.has(ruleId)) {
|
|
13
|
+
ruleIndex.set(ruleId, rules.length);
|
|
14
|
+
rules.push({
|
|
15
|
+
id: ruleId,
|
|
16
|
+
name: ruleId,
|
|
17
|
+
shortDescription: { text: meta.label },
|
|
18
|
+
fullDescription: meta.description ? { text: meta.description } : undefined,
|
|
19
|
+
defaultConfiguration: { level: severityToLevel(issue.severity) },
|
|
20
|
+
helpUri: "https://vibecodeqa.online",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const result = {
|
|
24
|
+
ruleId,
|
|
25
|
+
level: severityToLevel(issue.severity),
|
|
26
|
+
message: { text: `[${check.name}] ${issue.message}` },
|
|
27
|
+
};
|
|
28
|
+
if (issue.file) {
|
|
29
|
+
const filePath = issue.file.split(":")[0];
|
|
30
|
+
result.locations = [
|
|
31
|
+
{
|
|
32
|
+
physicalLocation: {
|
|
33
|
+
artifactLocation: { uri: filePath },
|
|
34
|
+
...(issue.line ? { region: { startLine: issue.line } } : {}),
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
results.push(result);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const sarif = {
|
|
43
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
44
|
+
version: "2.1.0",
|
|
45
|
+
runs: [
|
|
46
|
+
{
|
|
47
|
+
tool: {
|
|
48
|
+
driver: {
|
|
49
|
+
name: "VibeCode QA",
|
|
50
|
+
version: report.version,
|
|
51
|
+
informationUri: "https://vibecodeqa.online",
|
|
52
|
+
rules,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
results,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
return JSON.stringify(sarif, null, 2);
|
|
60
|
+
}
|
|
61
|
+
function severityToLevel(severity) {
|
|
62
|
+
if (severity === "error")
|
|
63
|
+
return "error";
|
|
64
|
+
if (severity === "warning")
|
|
65
|
+
return "warning";
|
|
66
|
+
return "note";
|
|
67
|
+
}
|