@vibecodeqa/cli 0.21.0 → 0.23.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/dist/check-meta.js +1 -1
- package/dist/cli.js +126 -106
- package/dist/fs-utils.js +17 -2
- package/dist/report/components.d.ts +8 -0
- package/dist/report/components.js +4 -0
- package/dist/report/favicon.d.ts +2 -0
- package/dist/report/favicon.js +2 -0
- package/dist/report/html.js +45 -34
- package/dist/report/pages.js +24 -18
- package/dist/report/svg.js +4 -2
- package/dist/runners/accessibility.js +37 -6
- package/dist/runners/architecture.d.ts +2 -0
- package/dist/runners/architecture.js +155 -1
- package/dist/runners/best-practices.js +187 -40
- package/dist/runners/dependencies.js +3 -1
- package/dist/runners/duplication.js +8 -1
- package/dist/runners/error-handling.js +16 -3
- package/dist/runners/performance.js +3 -1
- package/dist/runners/react.js +43 -7
- package/dist/runners/standards.js +4 -1
- package/dist/runners/type-safety.js +3 -1
- package/package.json +1 -1
package/dist/check-meta.js
CHANGED
|
@@ -179,7 +179,7 @@ export const CHECK_META = {
|
|
|
179
179
|
weight: 4,
|
|
180
180
|
description: "Checks common accessibility violations: images without alt text, click handlers on non-interactive elements without keyboard support, form controls without labels, autoFocus usage, positive tabIndex, and missing html lang attribute.",
|
|
181
181
|
risk: "1 in 4 adults has a disability (CDC). Missing alt text makes images invisible to screen readers. Click-only divs exclude keyboard users. Unlabeled inputs are unusable with assistive technology. Missing lang attribute breaks screen reader pronunciation.",
|
|
182
|
-
recommendation:
|
|
182
|
+
recommendation: 'Add alt text to all images (use alt="" for decorative). Use <button> for clickable elements, not <div onClick>. Label all form controls with <label>, aria-label, or aria-labelledby. Set lang on <html>.',
|
|
183
183
|
},
|
|
184
184
|
performance: {
|
|
185
185
|
name: "performance",
|
package/dist/cli.js
CHANGED
|
@@ -4,21 +4,21 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFile
|
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { detectRepoUrl, detectStack } from "./detect.js";
|
|
6
6
|
import { generatePages } from "./report/html.js";
|
|
7
|
+
import { runAccessibility } from "./runners/accessibility.js";
|
|
7
8
|
import { runArchitecture } from "./runners/architecture.js";
|
|
8
9
|
import { runBestPractices } from "./runners/best-practices.js";
|
|
10
|
+
import { runCodeCoherence } from "./runners/code-coherence.js";
|
|
9
11
|
import { runComplexity } from "./runners/complexity.js";
|
|
10
12
|
import { runConfusion } from "./runners/confusion.js";
|
|
11
13
|
import { runContext } from "./runners/context.js";
|
|
12
14
|
import { runDependencies } from "./runners/dependencies.js";
|
|
15
|
+
import { runDocCoherence } from "./runners/doc-coherence.js";
|
|
13
16
|
import { runDocs } from "./runners/docs.js";
|
|
14
17
|
import { runDuplication } from "./runners/duplication.js";
|
|
15
|
-
import { runPerformance } from "./runners/performance.js";
|
|
16
18
|
import { runErrorHandling } from "./runners/error-handling.js";
|
|
17
19
|
import { runLint } from "./runners/lint.js";
|
|
20
|
+
import { runPerformance } from "./runners/performance.js";
|
|
18
21
|
import { runReact } from "./runners/react.js";
|
|
19
|
-
import { runAccessibility } from "./runners/accessibility.js";
|
|
20
|
-
import { runDocCoherence } from "./runners/doc-coherence.js";
|
|
21
|
-
import { runCodeCoherence } from "./runners/code-coherence.js";
|
|
22
22
|
import { runSecrets } from "./runners/secrets.js";
|
|
23
23
|
import { runSecurity } from "./runners/security.js";
|
|
24
24
|
import { runStandards } from "./runners/standards.js";
|
|
@@ -31,17 +31,22 @@ import { computeTrend, formatTrend } from "./trend.js";
|
|
|
31
31
|
import { gradeFromScore } from "./types.js";
|
|
32
32
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
33
33
|
const VERSION = pkg.version;
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
34
|
+
function parseFlags() {
|
|
35
|
+
const args = process.argv.slice(2);
|
|
36
|
+
const flags = new Set(args.filter((a) => a.startsWith("--")));
|
|
37
|
+
const cwd = resolve(args.find((a) => !a.startsWith("--")) || ".");
|
|
38
|
+
return {
|
|
39
|
+
cwd,
|
|
40
|
+
outputDir: join(cwd, ".vibe-check"),
|
|
41
|
+
jsonOnly: flags.has("--json"),
|
|
42
|
+
ciMode: flags.has("--ci"),
|
|
43
|
+
skipTests: flags.has("--skip-tests"),
|
|
44
|
+
watchMode: flags.has("--watch"),
|
|
45
|
+
badgeMode: flags.has("--badge"),
|
|
46
|
+
sarifMode: flags.has("--sarif"),
|
|
47
|
+
uploadMode: flags.has("--upload"),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
45
50
|
function color(grade) {
|
|
46
51
|
if (grade === "A")
|
|
47
52
|
return "\x1b[32m";
|
|
@@ -49,23 +54,7 @@ function color(grade) {
|
|
|
49
54
|
return "\x1b[33m";
|
|
50
55
|
return "\x1b[31m";
|
|
51
56
|
}
|
|
52
|
-
|
|
53
|
-
const start = Date.now();
|
|
54
|
-
if (!jsonOnly) {
|
|
55
|
-
console.log("");
|
|
56
|
-
console.log(` \x1b[1m\x1b[38;5;141mvcqa\x1b[0m v${VERSION}`);
|
|
57
|
-
console.log(` \x1b[2m${cwd}\x1b[0m`);
|
|
58
|
-
console.log("");
|
|
59
|
-
}
|
|
60
|
-
const stack = detectStack(cwd);
|
|
61
|
-
if (!jsonOnly) {
|
|
62
|
-
const parts = [stack.language, stack.framework, stack.bundler, stack.testRunner, stack.linter, stack.packageManager].filter((v) => v !== "none" && v !== "unknown");
|
|
63
|
-
console.log(` stack: ${parts.join(" + ")}`);
|
|
64
|
-
console.log("");
|
|
65
|
-
}
|
|
66
|
-
const checks = [];
|
|
67
|
-
const isDart = stack.language === "dart";
|
|
68
|
-
// All runners grouped by category
|
|
57
|
+
function runChecks(cwd, stack, skipTests, isDart, jsonOnly) {
|
|
69
58
|
const runners = [
|
|
70
59
|
// Foundations
|
|
71
60
|
{ name: "structure", fn: () => runStructure(cwd, stack) },
|
|
@@ -97,6 +86,7 @@ async function main() {
|
|
|
97
86
|
{ name: "doc-coherence", fn: () => runDocCoherence(cwd) },
|
|
98
87
|
{ name: "code-coherence", fn: () => runCodeCoherence(cwd) },
|
|
99
88
|
];
|
|
89
|
+
const checks = [];
|
|
100
90
|
for (const runner of runners) {
|
|
101
91
|
if (!jsonOnly)
|
|
102
92
|
process.stdout.write(` ${runner.name.padEnd(14)}`);
|
|
@@ -113,20 +103,9 @@ async function main() {
|
|
|
113
103
|
console.log(`${c}${label.padEnd(5)}${scoreStr}\x1b[0m \x1b[2m${result.duration}ms\x1b[0m${issueStr}`);
|
|
114
104
|
}
|
|
115
105
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const totalIssues = checks.reduce((s, c) => s + c.issues.length, 0);
|
|
120
|
-
const report = {
|
|
121
|
-
version: VERSION,
|
|
122
|
-
timestamp: new Date().toISOString(),
|
|
123
|
-
score,
|
|
124
|
-
grade,
|
|
125
|
-
checks,
|
|
126
|
-
meta: { cwd, node: process.version, duration, stack, ...detectRepoUrl(cwd) },
|
|
127
|
-
};
|
|
128
|
-
// Trend comparison (read previous report before overwriting)
|
|
129
|
-
const trend = computeTrend(report, outputDir);
|
|
106
|
+
return checks;
|
|
107
|
+
}
|
|
108
|
+
async function writeOutputs(report, outputDir, flags) {
|
|
130
109
|
if (!existsSync(outputDir))
|
|
131
110
|
mkdirSync(outputDir, { recursive: true });
|
|
132
111
|
// Save to history before overwriting current report
|
|
@@ -159,58 +138,127 @@ async function main() {
|
|
|
159
138
|
writeFileSync(join(reportDir, filename), html);
|
|
160
139
|
}
|
|
161
140
|
// Badge SVG
|
|
162
|
-
if (badgeMode) {
|
|
141
|
+
if (flags.badgeMode) {
|
|
163
142
|
const { buildBadge } = await import("./report/svg.js");
|
|
164
|
-
const badgeSvg = buildBadge(score, grade);
|
|
143
|
+
const badgeSvg = buildBadge(report.score, report.grade);
|
|
165
144
|
writeFileSync(join(outputDir, "badge.svg"), badgeSvg);
|
|
166
145
|
}
|
|
167
146
|
// SARIF output for GitHub Code Scanning
|
|
168
|
-
if (sarifMode) {
|
|
147
|
+
if (flags.sarifMode) {
|
|
169
148
|
const { generateSARIF } = await import("./report/sarif.js");
|
|
170
149
|
writeFileSync(join(outputDir, "report.sarif"), generateSARIF(report));
|
|
171
150
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const res = await fetch("https://api.vibecodeqa.online/api/reports", {
|
|
178
|
-
method: "POST",
|
|
179
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
180
|
-
body: JSON.stringify({ repo, report }),
|
|
181
|
-
});
|
|
182
|
-
if (res.ok) {
|
|
183
|
-
const data = await res.json();
|
|
184
|
-
if (!jsonOnly)
|
|
185
|
-
console.log(` \x1b[32m\u2713 Uploaded to dashboard\x1b[0m \x1b[2m(${data.totalReports || 1} reports)\x1b[0m`);
|
|
186
|
-
}
|
|
187
|
-
else if (!jsonOnly) {
|
|
188
|
-
console.log(` \x1b[33m\u26a0 Upload failed: ${res.status}\x1b[0m \x1b[2m(set VCQA_TOKEN env var)\x1b[0m`);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
catch {
|
|
192
|
-
if (!jsonOnly)
|
|
193
|
-
console.log(` \x1b[33m\u26a0 Upload failed (network error)\x1b[0m`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
if (jsonOnly) {
|
|
151
|
+
}
|
|
152
|
+
function printResults(report, trend, flags, outputDir) {
|
|
153
|
+
const { score, grade, checks } = report;
|
|
154
|
+
const totalIssues = checks.reduce((s, c) => s + c.issues.length, 0);
|
|
155
|
+
if (flags.jsonOnly) {
|
|
197
156
|
console.log(JSON.stringify(report));
|
|
198
157
|
}
|
|
199
158
|
else {
|
|
200
159
|
const gc = color(grade);
|
|
201
160
|
console.log("");
|
|
202
|
-
console.log(` ${gc}\x1b[1m${grade}\x1b[0m ${gc}${score}/100\x1b[0m \x1b[2m${checks.length} checks · ${totalIssues} issues · ${duration}ms\x1b[0m`);
|
|
161
|
+
console.log(` ${gc}\x1b[1m${grade}\x1b[0m ${gc}${score}/100\x1b[0m \x1b[2m${checks.length} checks · ${totalIssues} issues · ${report.meta.duration}ms\x1b[0m`);
|
|
203
162
|
if (trend)
|
|
204
163
|
console.log(formatTrend(trend));
|
|
205
164
|
console.log("");
|
|
206
165
|
console.log(` \x1b[2mReport: ${join(outputDir, "report/index.html")}\x1b[0m`);
|
|
207
166
|
console.log(` \x1b[2mJSON: ${join(outputDir, "report.json")}\x1b[0m`);
|
|
208
|
-
if (badgeMode)
|
|
167
|
+
if (flags.badgeMode)
|
|
209
168
|
console.log(` \x1b[2mBadge: ${join(outputDir, "badge.svg")}\x1b[0m`);
|
|
210
|
-
if (sarifMode)
|
|
169
|
+
if (flags.sarifMode)
|
|
211
170
|
console.log(` \x1b[2mSARIF: ${join(outputDir, "report.sarif")}\x1b[0m`);
|
|
212
171
|
console.log("");
|
|
213
172
|
}
|
|
173
|
+
}
|
|
174
|
+
async function handleUpload(report, cwd, jsonOnly) {
|
|
175
|
+
const repo = report.meta.repoUrl?.replace(/^https:\/\/github\.com\//, "") || cwd.split("/").pop() || "project";
|
|
176
|
+
const token = process.env.VCQA_TOKEN || "";
|
|
177
|
+
try {
|
|
178
|
+
const res = await fetch("https://api.vibecodeqa.online/api/reports", {
|
|
179
|
+
method: "POST",
|
|
180
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
181
|
+
body: JSON.stringify({ repo, report }),
|
|
182
|
+
});
|
|
183
|
+
if (res.ok) {
|
|
184
|
+
const data = (await res.json());
|
|
185
|
+
if (!jsonOnly)
|
|
186
|
+
console.log(` \x1b[32m\u2713 Uploaded to dashboard\x1b[0m \x1b[2m(${data.totalReports || 1} reports)\x1b[0m`);
|
|
187
|
+
}
|
|
188
|
+
else if (!jsonOnly) {
|
|
189
|
+
console.log(` \x1b[33m\u26a0 Upload failed: ${res.status}\x1b[0m \x1b[2m(set VCQA_TOKEN env var)\x1b[0m`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
if (!jsonOnly)
|
|
194
|
+
console.log(` \x1b[33m\u26a0 Upload failed (network error)\x1b[0m`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function startWatch(cwd) {
|
|
198
|
+
const { watch } = await import("node:fs");
|
|
199
|
+
const srcDirs = ["src", "web/src"].map((d) => join(cwd, d)).filter((d) => existsSync(d));
|
|
200
|
+
if (srcDirs.length === 0) {
|
|
201
|
+
console.log(" \x1b[31mNo src/ directory to watch\x1b[0m");
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
console.log(" \x1b[2mWatching for changes... (Ctrl+C to stop)\x1b[0m");
|
|
205
|
+
console.log("");
|
|
206
|
+
let debounce = null;
|
|
207
|
+
let running = false;
|
|
208
|
+
for (const dir of srcDirs) {
|
|
209
|
+
watch(dir, { recursive: true }, (_event, filename) => {
|
|
210
|
+
if (!filename || filename.includes("node_modules") || filename.includes(".vibe-check"))
|
|
211
|
+
return;
|
|
212
|
+
if (running)
|
|
213
|
+
return;
|
|
214
|
+
if (debounce)
|
|
215
|
+
clearTimeout(debounce);
|
|
216
|
+
debounce = setTimeout(async () => {
|
|
217
|
+
running = true;
|
|
218
|
+
console.log(` \x1b[2mChanged: ${filename} — re-scanning...\x1b[0m`);
|
|
219
|
+
await main().catch(() => { });
|
|
220
|
+
running = false;
|
|
221
|
+
}, 500);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
// Keep process alive
|
|
225
|
+
await new Promise(() => { });
|
|
226
|
+
}
|
|
227
|
+
async function main() {
|
|
228
|
+
const flags = parseFlags();
|
|
229
|
+
const { cwd, outputDir, jsonOnly, ciMode, skipTests, watchMode } = flags;
|
|
230
|
+
const start = Date.now();
|
|
231
|
+
if (!jsonOnly) {
|
|
232
|
+
console.log("");
|
|
233
|
+
console.log(` \x1b[1m\x1b[38;5;141mvcqa\x1b[0m v${VERSION}`);
|
|
234
|
+
console.log(` \x1b[2m${cwd}\x1b[0m`);
|
|
235
|
+
console.log("");
|
|
236
|
+
}
|
|
237
|
+
const stack = detectStack(cwd);
|
|
238
|
+
if (!jsonOnly) {
|
|
239
|
+
const parts = [stack.language, stack.framework, stack.bundler, stack.testRunner, stack.linter, stack.packageManager].filter((v) => v !== "none" && v !== "unknown");
|
|
240
|
+
console.log(` stack: ${parts.join(" + ")}`);
|
|
241
|
+
console.log("");
|
|
242
|
+
}
|
|
243
|
+
const isDart = stack.language === "dart";
|
|
244
|
+
const checks = runChecks(cwd, stack, skipTests, isDart, jsonOnly);
|
|
245
|
+
const score = computeScore(checks);
|
|
246
|
+
const grade = gradeFromScore(score);
|
|
247
|
+
const duration = Date.now() - start;
|
|
248
|
+
const report = {
|
|
249
|
+
version: VERSION,
|
|
250
|
+
timestamp: new Date().toISOString(),
|
|
251
|
+
score,
|
|
252
|
+
grade,
|
|
253
|
+
checks,
|
|
254
|
+
meta: { cwd, node: process.version, duration, stack, ...detectRepoUrl(cwd) },
|
|
255
|
+
};
|
|
256
|
+
const trend = computeTrend(report, outputDir);
|
|
257
|
+
await writeOutputs(report, outputDir, flags);
|
|
258
|
+
printResults(report, trend, flags, outputDir);
|
|
259
|
+
if (flags.uploadMode) {
|
|
260
|
+
await handleUpload(report, cwd, jsonOnly);
|
|
261
|
+
}
|
|
214
262
|
if (ciMode && score < 60) {
|
|
215
263
|
process.exit(1);
|
|
216
264
|
}
|
|
@@ -224,36 +272,8 @@ async function main() {
|
|
|
224
272
|
/* failed to open browser */
|
|
225
273
|
}
|
|
226
274
|
}
|
|
227
|
-
// Watch mode — re-run on file changes
|
|
228
275
|
if (watchMode) {
|
|
229
|
-
|
|
230
|
-
const srcDirs = ["src", "web/src"].map((d) => join(cwd, d)).filter((d) => existsSync(d));
|
|
231
|
-
if (srcDirs.length === 0) {
|
|
232
|
-
console.log(" \x1b[31mNo src/ directory to watch\x1b[0m");
|
|
233
|
-
process.exit(1);
|
|
234
|
-
}
|
|
235
|
-
console.log(" \x1b[2mWatching for changes... (Ctrl+C to stop)\x1b[0m");
|
|
236
|
-
console.log("");
|
|
237
|
-
let debounce = null;
|
|
238
|
-
let running = false;
|
|
239
|
-
for (const dir of srcDirs) {
|
|
240
|
-
watch(dir, { recursive: true }, (_event, filename) => {
|
|
241
|
-
if (!filename || filename.includes("node_modules") || filename.includes(".vibe-check"))
|
|
242
|
-
return;
|
|
243
|
-
if (running)
|
|
244
|
-
return; // prevent concurrent re-runs (M5)
|
|
245
|
-
if (debounce)
|
|
246
|
-
clearTimeout(debounce);
|
|
247
|
-
debounce = setTimeout(async () => {
|
|
248
|
-
running = true;
|
|
249
|
-
console.log(` \x1b[2mChanged: ${filename} — re-scanning...\x1b[0m`);
|
|
250
|
-
await main().catch(() => { });
|
|
251
|
-
running = false;
|
|
252
|
-
}, 500);
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
// Keep process alive
|
|
256
|
-
await new Promise(() => { });
|
|
276
|
+
await startWatch(cwd);
|
|
257
277
|
}
|
|
258
278
|
}
|
|
259
279
|
main().catch((err) => {
|
package/dist/fs-utils.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
/** Shared filesystem utilities — eliminates duplicate file-walking across runners. */
|
|
2
2
|
import { lstatSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { basename, extname, join } from "node:path";
|
|
4
|
-
const SKIP_DIRS = new Set([
|
|
4
|
+
const SKIP_DIRS = new Set([
|
|
5
|
+
"node_modules",
|
|
6
|
+
"dist",
|
|
7
|
+
".git",
|
|
8
|
+
".vibe-check",
|
|
9
|
+
"coverage",
|
|
10
|
+
"test-results",
|
|
11
|
+
"__pycache__",
|
|
12
|
+
".dart_tool",
|
|
13
|
+
"build",
|
|
14
|
+
".flutter-plugins",
|
|
15
|
+
]);
|
|
5
16
|
const CODE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".dart"]);
|
|
6
17
|
const ALL_EXTS = new Set([...CODE_EXTS, ".json", ".env", ".yaml", ".yml", ".toml"]);
|
|
7
18
|
/** Walk source directories and return all code files. */
|
|
@@ -76,7 +87,11 @@ function walk(dir, cwd, out, exts) {
|
|
|
76
87
|
continue;
|
|
77
88
|
const content = readFileSync(full, "utf-8");
|
|
78
89
|
const relPath = full.replace(`${cwd}/`, "");
|
|
79
|
-
const isTest = entry.includes(".test.") ||
|
|
90
|
+
const isTest = entry.includes(".test.") ||
|
|
91
|
+
entry.includes(".spec.") ||
|
|
92
|
+
entry.endsWith("_test.dart") ||
|
|
93
|
+
relPath.includes("__tests__") ||
|
|
94
|
+
relPath.includes("test/");
|
|
80
95
|
out.push({
|
|
81
96
|
path: relPath,
|
|
82
97
|
fullPath: full,
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
/** Reusable HTML components and helpers for the report. */
|
|
2
2
|
import type { Priority } from "../check-meta.js";
|
|
3
|
+
import type { CheckResult } from "../types.js";
|
|
4
|
+
/** Type-safe accessor for check detail flags. */
|
|
5
|
+
export declare function det(c: CheckResult): {
|
|
6
|
+
skipped?: boolean;
|
|
7
|
+
comingSoon?: boolean;
|
|
8
|
+
reason?: string;
|
|
9
|
+
[k: string]: unknown;
|
|
10
|
+
};
|
|
3
11
|
/** HTML-escape a string. */
|
|
4
12
|
export declare function e(s: string): string;
|
|
5
13
|
/** Make a file path a clickable GitHub link if repoUrl is available. */
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
/** Reusable HTML components and helpers for the report. */
|
|
2
|
+
/** Type-safe accessor for check detail flags. */
|
|
3
|
+
export function det(c) {
|
|
4
|
+
return c.details;
|
|
5
|
+
}
|
|
2
6
|
/** HTML-escape a string. */
|
|
3
7
|
export function e(s) {
|
|
4
8
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** Simple SVG favicon — "VQ" monogram in accent purple. */
|
|
2
|
+
export declare const FAVICON_SVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 32 32\"><rect width=\"32\" height=\"32\" rx=\"6\" fill=\"#818cf8\"/><text x=\"16\" y=\"22\" text-anchor=\"middle\" font-family=\"system-ui,sans-serif\" font-size=\"16\" font-weight=\"900\" fill=\"#fff\">VQ</text></svg>";
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** Simple SVG favicon — "VQ" monogram in accent purple. */
|
|
2
|
+
export const FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#818cf8"/><text x="16" y="22" text-anchor="middle" font-family="system-ui,sans-serif" font-size="16" font-weight="900" fill="#fff">VQ</text></svg>`;
|
package/dist/report/html.js
CHANGED
|
@@ -11,12 +11,18 @@
|
|
|
11
11
|
* Mobile: Hamburger toggles both top nav dropdown and sidebar panel.
|
|
12
12
|
*/
|
|
13
13
|
import { getCheckMeta } from "../check-meta.js";
|
|
14
|
-
import { e, fileLink, gc } from "./components.js";
|
|
14
|
+
import { det, e, fileLink, gc } from "./components.js";
|
|
15
|
+
import { FAVICON_SVG } from "./favicon.js";
|
|
15
16
|
import { categoryPage, filesPage, issuesPage, overviewPage } from "./pages.js";
|
|
16
17
|
import { CSS } from "./styles.js";
|
|
17
18
|
export const GROUPS = [
|
|
18
19
|
{ id: "foundations", label: "Foundations", file: "foundations.html", checks: ["structure", "lint", "types", "type-safety", "standards"] },
|
|
19
|
-
{
|
|
20
|
+
{
|
|
21
|
+
id: "quality",
|
|
22
|
+
label: "Quality",
|
|
23
|
+
file: "quality.html",
|
|
24
|
+
checks: ["complexity", "duplication", "error-handling", "react", "accessibility", "docs", "best-practices"],
|
|
25
|
+
},
|
|
20
26
|
{ id: "testing", label: "Testing", file: "testing.html", checks: ["testing"] },
|
|
21
27
|
{ id: "arch", label: "Architecture", file: "architecture.html", checks: ["architecture", "performance"] },
|
|
22
28
|
{ id: "security", label: "Security", file: "security.html", checks: ["secrets", "security", "dependencies"] },
|
|
@@ -27,7 +33,7 @@ export function generatePages(report, historyDir) {
|
|
|
27
33
|
const pages = new Map();
|
|
28
34
|
const allChecks = report.checks;
|
|
29
35
|
const checkMap = new Map(allChecks.map((c) => [c.name, c]));
|
|
30
|
-
const active = allChecks.filter((c) => !c.
|
|
36
|
+
const active = allChecks.filter((c) => !det(c).skipped && !det(c).comingSoon);
|
|
31
37
|
const ru = report.meta.repoUrl;
|
|
32
38
|
const br = report.meta.branch;
|
|
33
39
|
const fl = (path, line) => fileLink(path, line, ru, br);
|
|
@@ -54,38 +60,44 @@ export function generatePages(report, historyDir) {
|
|
|
54
60
|
.slice(0, 30);
|
|
55
61
|
const catScores = GROUPS.map((g) => {
|
|
56
62
|
const checks = g.checks.map((n) => checkMap.get(n)).filter(Boolean);
|
|
57
|
-
const scored = checks.filter((c) => !c.
|
|
63
|
+
const scored = checks.filter((c) => !det(c).skipped);
|
|
58
64
|
const avg = scored.length > 0 ? Math.round(scored.reduce((s, c) => s + c.score, 0) / scored.length) : 0;
|
|
59
65
|
return { ...g, avg, checks };
|
|
60
66
|
});
|
|
61
67
|
const w = (id, sidebar, content) => wrap(proj, id, report, totalIssues, sidebar, content);
|
|
62
68
|
// ── Overview: sidebar shows score + category summary ──
|
|
63
|
-
const overviewSidebar = sidebarScore(report)
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
const overviewSidebar = sidebarScore(report) +
|
|
70
|
+
catScores
|
|
71
|
+
.map((cs) => {
|
|
72
|
+
const isPremium = cs.checks.every((c) => det(c).comingSoon);
|
|
66
73
|
const clr = isPremium ? "#6366f1" : gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
|
|
67
74
|
const label = isPremium
|
|
68
75
|
? `<span class="pro-badge" style="font-size:0.5rem;padding:0.08rem 0.35rem">PRO</span>`
|
|
69
76
|
: `<span style="color:${clr}">${cs.avg}</span>`;
|
|
70
77
|
return `<a class="side-cat" href="${cs.file}">${cs.label} ${label}</a>`;
|
|
71
|
-
})
|
|
72
|
-
|
|
78
|
+
})
|
|
79
|
+
.join("") +
|
|
80
|
+
sidebarViews(totalIssues, fileIssues.size);
|
|
73
81
|
pages.set("index.html", w("overview", overviewSidebar, overviewPage(report, active, totalIssues, catScores, allChecks, topFiles, fl, historyDir)));
|
|
74
82
|
// ── Category pages: sidebar shows the checks within this category ──
|
|
75
83
|
for (let i = 0; i < GROUPS.length; i++) {
|
|
76
84
|
const g = GROUPS[i];
|
|
77
85
|
const cs = catScores[i];
|
|
78
|
-
const catSidebar = sidebarScore(report)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
86
|
+
const catSidebar = sidebarScore(report) +
|
|
87
|
+
`<div class="side-section"><div class="side-cat-title">${cs.label}</div>` +
|
|
88
|
+
cs.checks
|
|
89
|
+
.map((c) => {
|
|
90
|
+
const sk = det(c).skipped;
|
|
91
|
+
const premium = det(c).comingSoon;
|
|
83
92
|
const meta = getCheckMeta(c.name);
|
|
84
|
-
const badge = premium
|
|
93
|
+
const badge = premium
|
|
94
|
+
? `<span style="color:#6366f1">PRO</span>`
|
|
95
|
+
: `<span style="color:${sk ? "#555" : gc(c.grade)}">${sk ? "\u2014" : c.grade} ${sk ? "" : c.score}</span>`;
|
|
85
96
|
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
|
-
})
|
|
87
|
-
|
|
88
|
-
+
|
|
97
|
+
})
|
|
98
|
+
.join("") +
|
|
99
|
+
`</div>` +
|
|
100
|
+
sidebarViews(totalIssues, fileIssues.size);
|
|
89
101
|
pages.set(g.file, w(g.id, catSidebar, categoryPage(cs, fl)));
|
|
90
102
|
}
|
|
91
103
|
// ── Issues: sidebar shows severity breakdown ──
|
|
@@ -93,21 +105,21 @@ export function generatePages(report, historyDir) {
|
|
|
93
105
|
const errCount = allIssuesList.filter((i) => i.severity === "error").length;
|
|
94
106
|
const warnCount = allIssuesList.filter((i) => i.severity === "warning").length;
|
|
95
107
|
const infoCount = allIssuesList.filter((i) => i.severity === "info").length;
|
|
96
|
-
const issuesSidebar = sidebarScore(report)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
108
|
+
const issuesSidebar = sidebarScore(report) +
|
|
109
|
+
`<div class="side-section"><div class="side-cat-title">Breakdown</div>` +
|
|
110
|
+
`<div class="side-stat"><span style="color:var(--fail)">${errCount}</span> errors</div>` +
|
|
111
|
+
`<div class="side-stat"><span style="color:var(--warn)">${warnCount}</span> warnings</div>` +
|
|
112
|
+
`<div class="side-stat"><span style="color:var(--info)">${infoCount}</span> info</div>` +
|
|
113
|
+
`</div>` +
|
|
114
|
+
sidebarViews(totalIssues, fileIssues.size);
|
|
103
115
|
pages.set("issues.html", w("issues", issuesSidebar, issuesPage(allChecks, totalIssues, fl)));
|
|
104
116
|
// ── Files: sidebar shows file stats ──
|
|
105
|
-
const filesSidebar = sidebarScore(report)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
117
|
+
const filesSidebar = sidebarScore(report) +
|
|
118
|
+
`<div class="side-section"><div class="side-cat-title">File Health</div>` +
|
|
119
|
+
`<div class="side-stat"><span style="color:var(--text)">${fileIssues.size}</span> files with issues</div>` +
|
|
120
|
+
`<div class="side-stat"><span style="color:var(--fail)">${topFiles.filter((f) => f.errors > 0).length}</span> with errors</div>` +
|
|
121
|
+
`</div>` +
|
|
122
|
+
sidebarViews(totalIssues, fileIssues.size);
|
|
111
123
|
pages.set("files.html", w("files", filesSidebar, filesPage(topFiles, fileIssues, fl)));
|
|
112
124
|
return pages;
|
|
113
125
|
}
|
|
@@ -129,14 +141,13 @@ function wrap(proj, currentId, report, totalIssues, sidebar, content) {
|
|
|
129
141
|
{ id: "issues", label: `Issues (${totalIssues})`, file: "issues.html" },
|
|
130
142
|
{ id: "files", label: "Files", file: "files.html" },
|
|
131
143
|
];
|
|
132
|
-
const nav = navItems
|
|
133
|
-
.map((t) => `<a class="tn${t.id === currentId ? " active" : ""}" href="${t.file}">${t.label}</a>`)
|
|
134
|
-
.join("");
|
|
144
|
+
const nav = navItems.map((t) => `<a class="tn${t.id === currentId ? " active" : ""}" href="${t.file}">${t.label}</a>`).join("");
|
|
135
145
|
return `<!DOCTYPE html>
|
|
136
146
|
<html lang="en">
|
|
137
147
|
<head>
|
|
138
148
|
<meta charset="utf-8">
|
|
139
149
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
150
|
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,${encodeURIComponent(FAVICON_SVG)}">
|
|
140
151
|
<title>VibeCode QA \u2014 ${e(proj)}</title>
|
|
141
152
|
<style>${CSS}</style>
|
|
142
153
|
</head>
|
package/dist/report/pages.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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, generateDSM, generatePackageDiagram } from "../runners/architecture.js";
|
|
5
|
-
import { e, gc, pc } from "./components.js";
|
|
4
|
+
import { generateArchSVG, generateDSM, generatePackageDiagram, generateSequenceDiagram } from "../runners/architecture.js";
|
|
5
|
+
import { det, 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) {
|
|
@@ -14,14 +14,14 @@ export function overviewPage(report, active, totalIssues, catScores, allChecks,
|
|
|
14
14
|
<span class="hd">${active.length} checks \u00b7 ${totalIssues} issues \u00b7 ${report.meta.duration}ms</span>
|
|
15
15
|
</div>
|
|
16
16
|
</div>`;
|
|
17
|
-
const scoredCats = catScores.filter((cs) => cs.checks.some((c) => !c.
|
|
17
|
+
const scoredCats = catScores.filter((cs) => cs.checks.some((c) => !det(c).skipped && !det(c).comingSoon));
|
|
18
18
|
const radarSvg = scoredCats.length >= 3 ? buildRadar(scoredCats.map((cs) => ({ label: cs.label, score: cs.avg }))) : "";
|
|
19
19
|
const catCards = catScores
|
|
20
20
|
.map((cs) => {
|
|
21
21
|
const clr = gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
|
|
22
22
|
const mini = cs.checks
|
|
23
23
|
.map((c) => {
|
|
24
|
-
const sk = c.
|
|
24
|
+
const sk = det(c).skipped;
|
|
25
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>`;
|
|
26
26
|
})
|
|
27
27
|
.join("");
|
|
@@ -60,10 +60,13 @@ export function overviewPage(report, active, totalIssues, catScores, allChecks,
|
|
|
60
60
|
}
|
|
61
61
|
let fileHotspotsHtml = "";
|
|
62
62
|
if (topFiles.length > 0) {
|
|
63
|
-
const fileRows = topFiles
|
|
63
|
+
const fileRows = topFiles
|
|
64
|
+
.slice(0, 5)
|
|
65
|
+
.map((f) => {
|
|
64
66
|
const pct = Math.min(100, f.total * 5);
|
|
65
67
|
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
|
-
})
|
|
68
|
+
})
|
|
69
|
+
.join("");
|
|
67
70
|
const viewAll = topFiles.length > 5 ? `<a class="ov-link" href="files.html">View all ${topFiles.length} files \u2192</a>` : "";
|
|
68
71
|
fileHotspotsHtml = `<div class="ov-section"><h3>File Hotspots</h3>${fileRows}${viewAll}</div>`;
|
|
69
72
|
}
|
|
@@ -87,8 +90,8 @@ ${fileHotspotsHtml}
|
|
|
87
90
|
export function categoryPage(cs, fl) {
|
|
88
91
|
const subNav = cs.checks
|
|
89
92
|
.map((c, i) => {
|
|
90
|
-
const sk = c.
|
|
91
|
-
const premium = c.
|
|
93
|
+
const sk = det(c).skipped;
|
|
94
|
+
const premium = det(c).comingSoon;
|
|
92
95
|
const badge = premium ? "PRO" : sk ? "\u2014" : c.grade;
|
|
93
96
|
const clr = premium ? "#6366f1" : sk ? "#555" : gc(c.grade);
|
|
94
97
|
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>`;
|
|
@@ -97,8 +100,8 @@ export function categoryPage(cs, fl) {
|
|
|
97
100
|
const subPages = cs.checks
|
|
98
101
|
.map((c, i) => {
|
|
99
102
|
const meta = getCheckMeta(c.name);
|
|
100
|
-
const sk = c.
|
|
101
|
-
const premium = c.
|
|
103
|
+
const sk = det(c).skipped;
|
|
104
|
+
const premium = det(c).comingSoon;
|
|
102
105
|
const detailsFiltered = Object.entries(c.details)
|
|
103
106
|
.filter(([k]) => k !== "skipped" && k !== "reason" && k !== "graph")
|
|
104
107
|
.map(([k, v]) => {
|
|
@@ -136,9 +139,9 @@ export function categoryPage(cs, fl) {
|
|
|
136
139
|
issuesHtml += `</div>`;
|
|
137
140
|
}
|
|
138
141
|
if (premium) {
|
|
139
|
-
const
|
|
140
|
-
const desc =
|
|
141
|
-
const detailKvs = Object.entries(
|
|
142
|
+
const d = c.details;
|
|
143
|
+
const desc = d.description || meta.description;
|
|
144
|
+
const detailKvs = Object.entries(d)
|
|
142
145
|
.filter(([k]) => !["premium", "comingSoon", "reason", "description"].includes(k))
|
|
143
146
|
.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
147
|
.join("");
|
|
@@ -156,9 +159,9 @@ ${detailKvs ? `<div class="kvs" style="margin-top:0.8rem">${detailKvs}</div>` :
|
|
|
156
159
|
return `<div class="sp${i === 0 ? " active" : ""}" data-sub="${cs.id}-${c.name}">
|
|
157
160
|
<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>
|
|
158
161
|
${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>` : ""}
|
|
159
|
-
${sk ? `<p class="skip-r">${e(c.
|
|
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.
|
|
162
|
+
${sk ? `<p class="skip-r">${e(det(c).reason || "skipped")}</p>` : ""}
|
|
163
|
+
${c.name === "architecture" && !sk ? `${c.details.containerSvg ? `<h3 style="margin-top:1.5rem">Container Diagram</h3><div class="arch-svg">${c.details.containerSvg}</div>` : ""}<h3 style="margin-top:1.5rem">Dependency Graph</h3><div class="arch-svg">${generateArchSVG(c.details)}</div><h3 style="margin-top:1.5rem">Sequence Diagram</h3><div class="arch-svg">${generateSequenceDiagram(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>` : ""}
|
|
164
|
+
${c.name === "testing" && !sk && det(c).pyramid ? `<div class="arch-svg">${buildPyramid(det(c).pyramid)}</div>` : ""}
|
|
162
165
|
${detailsFiltered ? `<div class="kvs">${detailsFiltered}</div>` : ""}
|
|
163
166
|
${issuesHtml ? `<div class="iss-list">${issuesHtml}</div>` : '<p style="color:var(--muted);font-size:0.8rem;margin-top:1rem">No issues found.</p>'}
|
|
164
167
|
</div>`;
|
|
@@ -210,7 +213,10 @@ export function filesPage(topFiles, fileIssues, fl) {
|
|
|
210
213
|
.join("");
|
|
211
214
|
return `
|
|
212
215
|
<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) => {
|
|
214
|
-
|
|
216
|
+
<p style="color:var(--muted);font-size:0.78rem;margin-bottom:1rem">${fileIssues.size} files with issues across ${topFiles.reduce((s, f) => {
|
|
217
|
+
for (const c of f.checks)
|
|
218
|
+
s.add(c);
|
|
219
|
+
return s;
|
|
220
|
+
}, new Set()).size} checks.</p>
|
|
215
221
|
${heatmapRows}`;
|
|
216
222
|
}
|