@vibecodeqa/cli 0.42.0 → 0.44.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 +130 -165
- package/dist/check-meta.js +59 -6
- package/dist/cli.js +299 -762
- package/dist/commands/explain.d.ts +2 -0
- package/dist/commands/explain.js +33 -0
- package/dist/commands/fix.d.ts +6 -0
- package/dist/commands/fix.js +157 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +96 -0
- package/dist/commands/shared.d.ts +4 -0
- package/dist/commands/shared.js +80 -0
- package/dist/core.d.ts +1 -0
- package/dist/core.js +12 -1
- package/dist/delta.d.ts +45 -0
- package/dist/delta.js +158 -0
- package/dist/detect.js +2 -2
- package/dist/pr-comment.d.ts +1 -1
- package/dist/pr-comment.js +23 -4
- package/dist/report/html.d.ts +1 -1
- package/dist/report/html.js +7 -2
- package/dist/report/pages.d.ts +2 -0
- package/dist/report/pages.js +167 -0
- package/dist/report/styles.d.ts +1 -1
- package/dist/report/styles.js +37 -0
- package/dist/runners/accessibility.js +4 -1
- package/dist/runners/best-practices.js +1 -1
- package/dist/runners/confusion.js +28 -17
- package/dist/runners/design-consistency.d.ts +12 -0
- package/dist/runners/design-consistency.js +125 -0
- package/dist/runners/error-handling.js +18 -2
- package/dist/runners/file-cohesion.d.ts +17 -0
- package/dist/runners/file-cohesion.js +177 -0
- package/dist/runners/frontend-health.d.ts +14 -0
- package/dist/runners/frontend-health.js +206 -0
- package/dist/runners/html-quality.d.ts +8 -0
- package/dist/runners/html-quality.js +203 -0
- package/dist/runners/lint.js +6 -1
- package/dist/runners/react.js +1 -0
- package/dist/runners/secrets.js +7 -2
- package/dist/runners/security.js +7 -1
- package/dist/runners/standards.d.ts +2 -2
- package/dist/runners/standards.js +45 -12
- package/dist/runners/structure.js +1 -1
- package/dist/runners/styling.d.ts +15 -0
- package/dist/runners/styling.js +280 -0
- package/dist/runners/testing.js +3 -1
- package/package.json +2 -2
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** vcqa explain — deep-dive explanation of a check. */
|
|
2
|
+
import { getCheckMeta } from "../check-meta.js";
|
|
3
|
+
export async function runExplain(checkName) {
|
|
4
|
+
if (!checkName) {
|
|
5
|
+
console.log("\n \x1b[1mUsage:\x1b[0m vcqa explain <check>\n");
|
|
6
|
+
console.log(" Available checks:");
|
|
7
|
+
const { CHECK_META } = await import("../check-meta.js");
|
|
8
|
+
for (const [name, meta] of Object.entries(CHECK_META)) {
|
|
9
|
+
console.log(` \x1b[1m${name.padEnd(16)}\x1b[0m ${meta.label} (${meta.category}, ${meta.weight}%)`);
|
|
10
|
+
}
|
|
11
|
+
console.log("");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const meta = getCheckMeta(checkName);
|
|
15
|
+
if (!meta.description || meta.description.length < 20) {
|
|
16
|
+
console.log(`\n \x1b[31mUnknown check: ${checkName}\x1b[0m`);
|
|
17
|
+
console.log(" Run \x1b[1mvcqa explain\x1b[0m to see available checks.\n");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
console.log("");
|
|
21
|
+
console.log(` \x1b[1m\x1b[38;5;141m${meta.label}\x1b[0m \x1b[2m${meta.category} · ${meta.priority} priority · ${meta.weight}% weight\x1b[0m`);
|
|
22
|
+
console.log("");
|
|
23
|
+
console.log(` \x1b[1mWhat:\x1b[0m ${meta.description}`);
|
|
24
|
+
console.log("");
|
|
25
|
+
console.log(` \x1b[1mRisk:\x1b[0m ${meta.risk}`);
|
|
26
|
+
console.log("");
|
|
27
|
+
console.log(` \x1b[1mFix:\x1b[0m ${meta.recommendation}`);
|
|
28
|
+
if (meta.deeperTools?.length) {
|
|
29
|
+
console.log("");
|
|
30
|
+
console.log(` \x1b[1mGo deeper:\x1b[0m ${meta.deeperTools.join(", ")}`);
|
|
31
|
+
}
|
|
32
|
+
console.log("");
|
|
33
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/** vcqa fix — auto-fix + AI-powered fixing with delta report. */
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { aiFixIssues, collectFixableIssues } from "../ai-fix.js";
|
|
5
|
+
import { scan } from "../core.js";
|
|
6
|
+
import { computeDelta, formatDeltaMarkdown } from "../delta.js";
|
|
7
|
+
import { detectStack } from "../detect.js";
|
|
8
|
+
import { suggestFix, validateCwd } from "./shared.js";
|
|
9
|
+
export async function runFix(cwd, opts = {}) {
|
|
10
|
+
console.log("");
|
|
11
|
+
console.log(` \x1b[1m\x1b[38;5;141mvcqa fix${opts.ai ? " --ai" : ""}${opts.dryRun ? " --dry-run" : ""}${opts.checkFilter ? ` --check ${opts.checkFilter}` : ""}\x1b[0m`);
|
|
12
|
+
console.log(` \x1b[2m${cwd}\x1b[0m`);
|
|
13
|
+
console.log("");
|
|
14
|
+
validateCwd(cwd);
|
|
15
|
+
// 0. Baseline scan (before fixes)
|
|
16
|
+
console.log(" \x1b[1mBaseline scan...\x1b[0m");
|
|
17
|
+
const beforeReport = await scan(cwd, { skipTests: true });
|
|
18
|
+
console.log(` \x1b[2mBaseline: ${beforeReport.grade} ${beforeReport.score}/100\x1b[0m`);
|
|
19
|
+
console.log("");
|
|
20
|
+
const stack = detectStack(cwd);
|
|
21
|
+
let fixed = 0;
|
|
22
|
+
// 1. Auto-fix structure issues (missing files)
|
|
23
|
+
if (!existsSync(join(cwd, ".gitignore"))) {
|
|
24
|
+
writeFileSync(join(cwd, ".gitignore"), "node_modules\ndist\n.vibe-check\ncoverage\n.env\n.env.local\n");
|
|
25
|
+
console.log(" \x1b[32m\u2713 Created .gitignore\x1b[0m");
|
|
26
|
+
fixed++;
|
|
27
|
+
}
|
|
28
|
+
if (existsSync(join(cwd, ".gitignore"))) {
|
|
29
|
+
const gi = readFileSync(join(cwd, ".gitignore"), "utf-8");
|
|
30
|
+
if (!gi.includes(".vibe-check")) {
|
|
31
|
+
writeFileSync(join(cwd, ".gitignore"), gi.trimEnd() + "\n.vibe-check/\n");
|
|
32
|
+
console.log(" \x1b[32m\u2713 Added .vibe-check/ to .gitignore\x1b[0m");
|
|
33
|
+
fixed++;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const tsconfigPath = join(cwd, "tsconfig.json");
|
|
37
|
+
if (existsSync(tsconfigPath)) {
|
|
38
|
+
try {
|
|
39
|
+
const raw = readFileSync(tsconfigPath, "utf-8");
|
|
40
|
+
const tsconfig = JSON.parse(raw);
|
|
41
|
+
if (!tsconfig.compilerOptions?.strict) {
|
|
42
|
+
tsconfig.compilerOptions = { ...tsconfig.compilerOptions, strict: true };
|
|
43
|
+
writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
|
|
44
|
+
console.log(' \x1b[32m\u2713 Enabled "strict": true in tsconfig.json\x1b[0m');
|
|
45
|
+
fixed++;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch { /* can't parse tsconfig */ }
|
|
49
|
+
}
|
|
50
|
+
// 2. Run linter auto-fix
|
|
51
|
+
if (stack.linter === "biome") {
|
|
52
|
+
console.log(" \x1b[1mFormatting with Biome...\x1b[0m");
|
|
53
|
+
const { execSync } = await import("node:child_process");
|
|
54
|
+
try {
|
|
55
|
+
execSync("npx biome check --write .", { cwd, stdio: "inherit", timeout: 30_000 });
|
|
56
|
+
fixed++;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
console.log(" \x1b[33mBiome had issues (some may be unfixable)\x1b[0m");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else if (stack.linter === "eslint") {
|
|
63
|
+
console.log(" \x1b[1mFixing with ESLint...\x1b[0m");
|
|
64
|
+
const { execSync } = await import("node:child_process");
|
|
65
|
+
try {
|
|
66
|
+
execSync("npx eslint --fix src/", { cwd, stdio: "inherit", timeout: 30_000 });
|
|
67
|
+
fixed++;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
console.log(" \x1b[33mESLint had issues (some may be unfixable)\x1b[0m");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// 3. AI-powered fix mode
|
|
74
|
+
if (opts.ai) {
|
|
75
|
+
console.log("");
|
|
76
|
+
console.log(" \x1b[1mScanning for AI-fixable issues...\x1b[0m");
|
|
77
|
+
const midReport = await scan(cwd, { skipTests: true });
|
|
78
|
+
const aiIssues = collectFixableIssues(midReport.checks, suggestFix, opts.checkFilter);
|
|
79
|
+
if (aiIssues.length === 0) {
|
|
80
|
+
console.log(" \x1b[2mNo fixable issues found.\x1b[0m");
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
console.log(` \x1b[1mAI fixing ${Math.min(aiIssues.length, 10)} issues${opts.dryRun ? " (dry run)" : ""}...\x1b[0m`);
|
|
84
|
+
console.log("");
|
|
85
|
+
await aiFixIssues(cwd, aiIssues, { dryRun: opts.dryRun || false });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// 4. Final scan + delta report
|
|
89
|
+
console.log("");
|
|
90
|
+
console.log(" \x1b[1mFinal scan...\x1b[0m");
|
|
91
|
+
const afterReport = await scan(cwd, { skipTests: true });
|
|
92
|
+
const delta = computeDelta(beforeReport, afterReport);
|
|
93
|
+
// Print delta summary
|
|
94
|
+
const scoreColor = delta.scoreDelta > 0 ? "32" : delta.scoreDelta < 0 ? "31" : "2";
|
|
95
|
+
const arrow = delta.scoreDelta > 0 ? "\u2191" : delta.scoreDelta < 0 ? "\u2193" : "=";
|
|
96
|
+
console.log("");
|
|
97
|
+
console.log(` \x1b[1mScore:\x1b[0m \x1b[2m${beforeReport.grade} ${beforeReport.score}\x1b[0m \u2192 \x1b[${afterReport.score >= 75 ? "32" : afterReport.score >= 60 ? "33" : "31"}m${afterReport.grade} ${afterReport.score}\x1b[0m \x1b[${scoreColor}m(${arrow}${Math.abs(delta.scoreDelta)})\x1b[0m`);
|
|
98
|
+
if (delta.fixed.length > 0) {
|
|
99
|
+
console.log(` \x1b[32m${delta.fixed.length} issues fixed\x1b[0m`);
|
|
100
|
+
// Show top fixed by check
|
|
101
|
+
const byCheck = new Map();
|
|
102
|
+
for (const f of delta.fixed)
|
|
103
|
+
byCheck.set(f.check, (byCheck.get(f.check) || 0) + 1);
|
|
104
|
+
for (const [check, count] of [...byCheck.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5)) {
|
|
105
|
+
console.log(` \x1b[32m\u2713\x1b[0m ${check}: ${count} fixed`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (delta.introduced.length > 0) {
|
|
109
|
+
console.log(` \x1b[31m${delta.introduced.length} new issues\x1b[0m`);
|
|
110
|
+
}
|
|
111
|
+
// Show per-check score changes
|
|
112
|
+
const changed = delta.checks.filter((c) => c.delta !== 0).sort((a, b) => b.delta - a.delta);
|
|
113
|
+
if (changed.length > 0) {
|
|
114
|
+
console.log("");
|
|
115
|
+
for (const c of changed.slice(0, 5)) {
|
|
116
|
+
const color = c.delta > 0 ? "32" : "31";
|
|
117
|
+
console.log(` \x1b[${color}m${c.delta > 0 ? "+" : ""}${c.delta}\x1b[0m ${c.name} (${c.before} \u2192 ${c.after})`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Save delta markdown
|
|
121
|
+
if (!opts.dryRun) {
|
|
122
|
+
const outDir = join(cwd, ".vibe-check");
|
|
123
|
+
mkdirSync(outDir, { recursive: true });
|
|
124
|
+
const md = formatDeltaMarkdown(delta);
|
|
125
|
+
writeFileSync(join(outDir, "delta.md"), md);
|
|
126
|
+
console.log("");
|
|
127
|
+
console.log(` \x1b[2mDelta report: .vibe-check/delta.md\x1b[0m`);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
console.log("");
|
|
131
|
+
console.log(" \x1b[2mDry run — no files modified. Remove --dry-run to apply.\x1b[0m");
|
|
132
|
+
}
|
|
133
|
+
// Non-AI mode: show remaining fix suggestions
|
|
134
|
+
if (!opts.ai) {
|
|
135
|
+
const fixable = [];
|
|
136
|
+
for (const c of afterReport.checks) {
|
|
137
|
+
for (const iss of c.issues) {
|
|
138
|
+
if (!iss.file || typeof iss.file !== "string" || !iss.line)
|
|
139
|
+
continue;
|
|
140
|
+
const fix = suggestFix(c.name, iss.rule || "", iss.message);
|
|
141
|
+
if (fix)
|
|
142
|
+
fixable.push({ check: c.name, file: iss.file, line: iss.line, message: iss.message, fix });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const top = fixable.slice(0, 5);
|
|
146
|
+
if (top.length > 0) {
|
|
147
|
+
console.log("");
|
|
148
|
+
console.log(` \x1b[1mRemaining fixes available:\x1b[0m \x1b[2mrun \x1b[0m\x1b[1mvcqa fix --ai\x1b[0m`);
|
|
149
|
+
for (const f of top) {
|
|
150
|
+
console.log(` \x1b[2m${f.file}:${f.line}\x1b[0m ${f.fix}`);
|
|
151
|
+
}
|
|
152
|
+
if (fixable.length > 5)
|
|
153
|
+
console.log(` \x1b[2m+${fixable.length - 5} more\x1b[0m`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
console.log("");
|
|
157
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** vcqa init — set up CI workflow + recommended configs. */
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { detectStack } from "../detect.js";
|
|
5
|
+
import { validateCwd } from "./shared.js";
|
|
6
|
+
export async function runInit(cwd) {
|
|
7
|
+
console.log("");
|
|
8
|
+
console.log(` \x1b[1m\x1b[38;5;141mvcqa init\x1b[0m`);
|
|
9
|
+
console.log(` \x1b[2m${cwd}\x1b[0m`);
|
|
10
|
+
console.log("");
|
|
11
|
+
validateCwd(cwd);
|
|
12
|
+
const stack = detectStack(cwd);
|
|
13
|
+
let created = 0;
|
|
14
|
+
// 1. GitHub Actions workflow
|
|
15
|
+
const workflowDir = join(cwd, ".github", "workflows");
|
|
16
|
+
const workflowPath = join(workflowDir, "vibecodeqa.yml");
|
|
17
|
+
if (!existsSync(workflowPath)) {
|
|
18
|
+
try {
|
|
19
|
+
mkdirSync(workflowDir, { recursive: true });
|
|
20
|
+
writeFileSync(workflowPath, `name: VibeCode QA
|
|
21
|
+
on: [pull_request]
|
|
22
|
+
permissions: { contents: read }
|
|
23
|
+
jobs:
|
|
24
|
+
scan:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
- run: npx @vibecodeqa/cli --ci --fail-under 70 --sarif --badge
|
|
29
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
30
|
+
if: always()
|
|
31
|
+
with:
|
|
32
|
+
sarif_file: .vibe-check/report.sarif
|
|
33
|
+
`);
|
|
34
|
+
console.log(` \x1b[32m+\x1b[0m .github/workflows/vibecodeqa.yml`);
|
|
35
|
+
created++;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
console.log(` \x1b[31m!\x1b[0m .github/workflows/vibecodeqa.yml (write failed — check permissions)`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
console.log(` \x1b[2m=\x1b[0m .github/workflows/vibecodeqa.yml (exists)`);
|
|
43
|
+
}
|
|
44
|
+
// 2. Biome config (if biome is a dep but no config exists)
|
|
45
|
+
if ((stack.linter === "biome" || existsSync(join(cwd, "node_modules", "@biomejs", "biome"))) &&
|
|
46
|
+
!existsSync(join(cwd, "biome.json")) &&
|
|
47
|
+
!existsSync(join(cwd, "biome.jsonc"))) {
|
|
48
|
+
writeFileSync(join(cwd, "biome.json"), JSON.stringify({
|
|
49
|
+
$schema: "https://biomejs.dev/schemas/2.0.0/schema.json",
|
|
50
|
+
formatter: { indentStyle: "tab", lineWidth: 120 },
|
|
51
|
+
linter: { enabled: true, rules: { recommended: true } },
|
|
52
|
+
organizeImports: { enabled: true },
|
|
53
|
+
}, null, "\t") + "\n");
|
|
54
|
+
console.log(` \x1b[32m+\x1b[0m biome.json`);
|
|
55
|
+
created++;
|
|
56
|
+
}
|
|
57
|
+
// 3. Create .vcqa.json if not present
|
|
58
|
+
const vcqaConfigPath = join(cwd, ".vcqa.json");
|
|
59
|
+
if (!existsSync(vcqaConfigPath)) {
|
|
60
|
+
const { CHECK_META } = await import("../check-meta.js");
|
|
61
|
+
const checksConfig = {};
|
|
62
|
+
for (const name of Object.keys(CHECK_META)) {
|
|
63
|
+
checksConfig[name] = {};
|
|
64
|
+
}
|
|
65
|
+
const config = {
|
|
66
|
+
_comment: "vcqa config — docs: https://vibecodeqa.online/skills",
|
|
67
|
+
checks: checksConfig,
|
|
68
|
+
_checks_help: "Set { \"enabled\": false } to disable. Add \"ignore\": [\"generated/**\"] to skip files per-check.",
|
|
69
|
+
ignore: [],
|
|
70
|
+
_ignore_help: "Global file patterns to skip: [\"vendor/**\", \"*.generated.ts\", \"proto/**\"]",
|
|
71
|
+
failUnder: 60,
|
|
72
|
+
_failUnder_help: "Exit with code 1 if score below this. Overridden by --fail-under flag.",
|
|
73
|
+
};
|
|
74
|
+
writeFileSync(vcqaConfigPath, JSON.stringify(config, null, 2) + "\n");
|
|
75
|
+
console.log(` \x1b[32m+\x1b[0m .vcqa.json`);
|
|
76
|
+
created++;
|
|
77
|
+
}
|
|
78
|
+
// 4. Add .vibe-check to .gitignore
|
|
79
|
+
const gitignorePath = join(cwd, ".gitignore");
|
|
80
|
+
if (existsSync(gitignorePath)) {
|
|
81
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
82
|
+
if (!content.includes(".vibe-check")) {
|
|
83
|
+
writeFileSync(gitignorePath, content.trimEnd() + "\n.vibe-check/\n");
|
|
84
|
+
console.log(` \x1b[32m+\x1b[0m .gitignore (added .vibe-check/)`);
|
|
85
|
+
created++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
console.log("");
|
|
89
|
+
if (created > 0) {
|
|
90
|
+
console.log(` \x1b[32mCreated ${created} file(s).\x1b[0m Run \x1b[1mnpx @vibecodeqa/cli\x1b[0m to scan.`);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
console.log(` \x1b[2mAlready set up. Run npx @vibecodeqa/cli to scan.\x1b[0m`);
|
|
94
|
+
}
|
|
95
|
+
console.log("");
|
|
96
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Shared utilities for CLI commands. */
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
3
|
+
export function validateCwd(cwd) {
|
|
4
|
+
if (!existsSync(cwd)) {
|
|
5
|
+
console.error(` \x1b[31mError: path does not exist: ${cwd}\x1b[0m`);
|
|
6
|
+
process.exit(1);
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
if (!statSync(cwd).isDirectory()) {
|
|
10
|
+
console.error(` \x1b[31mError: not a directory: ${cwd}\x1b[0m`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
console.error(` \x1b[31mError: cannot access: ${cwd}\x1b[0m`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Map common issue rules to actionable fix suggestions. */
|
|
20
|
+
export function suggestFix(check, rule, message) {
|
|
21
|
+
if (rule === "empty-catch")
|
|
22
|
+
return "Add error logging: catch(e) { console.error(e); }";
|
|
23
|
+
if (rule === "throw-string")
|
|
24
|
+
return 'Replace throw "msg" with throw new Error("msg")';
|
|
25
|
+
if (rule === "swallowed-promise")
|
|
26
|
+
return "Add logging: .catch((e) => { console.error(e); })";
|
|
27
|
+
if (rule === "floating-promise")
|
|
28
|
+
return "Add await or .catch() to handle the promise";
|
|
29
|
+
if (rule === "unsafe-json-parse")
|
|
30
|
+
return "Wrap in try-catch: try { JSON.parse(x) } catch { /* handle */ }";
|
|
31
|
+
if (rule === "no-error-boundary")
|
|
32
|
+
return "Add <ErrorBoundary> wrapper in your React app root";
|
|
33
|
+
if (rule === "img-alt")
|
|
34
|
+
return 'Add alt attribute: <img alt="description" ...>';
|
|
35
|
+
if (rule === "click-events")
|
|
36
|
+
return 'Add role="button" and onKeyDown handler';
|
|
37
|
+
if (rule === "vue-v-for-key")
|
|
38
|
+
return 'Add :key="item.id" to the v-for element';
|
|
39
|
+
if (rule === "missing-key")
|
|
40
|
+
return "Add key={item.id} to the JSX element in .map()";
|
|
41
|
+
if (rule === "index-key")
|
|
42
|
+
return "Use a stable unique ID instead of array index for key";
|
|
43
|
+
if (rule === "conditional-hook")
|
|
44
|
+
return "Move the hook call before any conditional (if/switch)";
|
|
45
|
+
if (rule === "no-tests")
|
|
46
|
+
return "Create a test file: src/__tests__/example.test.ts";
|
|
47
|
+
if (rule === "no-readme")
|
|
48
|
+
return "Create README.md with: project description, install, usage";
|
|
49
|
+
if (rule === "no-changelog")
|
|
50
|
+
return "Create CHANGELOG.md or use changesets: npx changeset init";
|
|
51
|
+
if (rule === "env-not-ignored")
|
|
52
|
+
return "Add .env to .gitignore";
|
|
53
|
+
if (rule === "secret-detected")
|
|
54
|
+
return "Move to environment variable, rotate the exposed secret";
|
|
55
|
+
if (rule === "no-ci")
|
|
56
|
+
return "Run: npx @vibecodeqa/cli init";
|
|
57
|
+
if (rule === "missing-lockfile")
|
|
58
|
+
return "Run: pnpm install (or npm install) to generate lockfile";
|
|
59
|
+
if (rule === "missing-file" && message.includes("LICENSE"))
|
|
60
|
+
return "Add LICENSE file: https://choosealicense.com/";
|
|
61
|
+
if (rule === "long-function")
|
|
62
|
+
return "Extract logic into smaller helper functions";
|
|
63
|
+
if (rule === "high-complexity")
|
|
64
|
+
return "Reduce nesting: use early returns, extract conditions";
|
|
65
|
+
if (rule === "duplicate-code")
|
|
66
|
+
return "Extract shared logic into a helper function";
|
|
67
|
+
if (rule === "circular-dep")
|
|
68
|
+
return "Extract shared types to a separate file both modules import";
|
|
69
|
+
if (rule === "god-module")
|
|
70
|
+
return "Split into focused interfaces — one responsibility per module";
|
|
71
|
+
if (rule === "process-exit")
|
|
72
|
+
return "Replace process.exit() with throw new Error()";
|
|
73
|
+
if (check === "security" && message.includes("innerHTML"))
|
|
74
|
+
return "Use textContent or DOM APIs instead";
|
|
75
|
+
if (check === "security" && message.includes("ev" + "al"))
|
|
76
|
+
return `Remove ${"ev" + "al"}() — use a safer alternative`;
|
|
77
|
+
if (check === "security" && message.includes("v-html"))
|
|
78
|
+
return 'Sanitize with DOMPurify: v-html="DOMPurify.sanitize(input)"';
|
|
79
|
+
return null;
|
|
80
|
+
}
|
package/dist/core.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface ScanOptions {
|
|
|
22
22
|
export declare function scan(cwd: string, options?: ScanOptions): Promise<VibeReport>;
|
|
23
23
|
export { CHECK_META, getCheckMeta, type CheckMeta };
|
|
24
24
|
export { computeScore } from "./score.js";
|
|
25
|
+
export { computeDelta, formatDeltaMarkdown, type ScanDelta } from "./delta.js";
|
|
25
26
|
export { loadConfig, type VcqaConfig } from "./config.js";
|
|
26
27
|
export { detectStack, detectWorkspace } from "./detect.js";
|
|
27
28
|
export { gradeFromScore } from "./types.js";
|
package/dist/core.js
CHANGED
|
@@ -23,7 +23,11 @@ import { runContainerHealth } from "./runners/container-health.js";
|
|
|
23
23
|
import { runConfusion } from "./runners/confusion.js";
|
|
24
24
|
import { runContext } from "./runners/context.js";
|
|
25
25
|
import { runDependencies } from "./runners/dependencies.js";
|
|
26
|
+
import { runDesignConsistency } from "./runners/design-consistency.js";
|
|
26
27
|
import { runEnvValidation } from "./runners/env-validation.js";
|
|
28
|
+
import { runFrontendHealth } from "./runners/frontend-health.js";
|
|
29
|
+
import { runHtmlQuality } from "./runners/html-quality.js";
|
|
30
|
+
import { runFileCohesion } from "./runners/file-cohesion.js";
|
|
27
31
|
import { runGitHygiene } from "./runners/git-hygiene.js";
|
|
28
32
|
import { runDocCoherence } from "./runners/doc-coherence.js";
|
|
29
33
|
import { runDocs } from "./runners/docs.js";
|
|
@@ -32,6 +36,7 @@ import { runErrorHandling } from "./runners/error-handling.js";
|
|
|
32
36
|
import { runLint } from "./runners/lint.js";
|
|
33
37
|
import { runMemorySafety } from "./runners/memory-safety.js";
|
|
34
38
|
import { runPerformance } from "./runners/performance.js";
|
|
39
|
+
import { runStyling } from "./runners/styling.js";
|
|
35
40
|
import { runReact } from "./runners/react.js";
|
|
36
41
|
import { runSecrets } from "./runners/secrets.js";
|
|
37
42
|
import { runSecurity } from "./runners/security.js";
|
|
@@ -61,7 +66,7 @@ export async function scan(cwd, options = {}) {
|
|
|
61
66
|
{ name: "lint", fn: () => runLint(resolvedCwd, stack, workspace) },
|
|
62
67
|
{ name: "types", fn: () => runTypeCheck(resolvedCwd, isDart, workspace) },
|
|
63
68
|
{ name: "type-safety", fn: () => runTypeSafety(resolvedCwd, isDart) },
|
|
64
|
-
{ name: "standards", fn: () => runStandards(resolvedCwd, stack) },
|
|
69
|
+
{ name: "standards", fn: () => runStandards(resolvedCwd, stack, workspace) },
|
|
65
70
|
{ name: "complexity", fn: () => runComplexity(resolvedCwd) },
|
|
66
71
|
{ name: "duplication", fn: () => runDuplication(resolvedCwd) },
|
|
67
72
|
{ name: "error-handling", fn: () => runErrorHandling(resolvedCwd, stack) },
|
|
@@ -84,8 +89,13 @@ export async function scan(cwd, options = {}) {
|
|
|
84
89
|
{ name: "doc-coherence", fn: () => runDocCoherence(resolvedCwd) },
|
|
85
90
|
{ name: "code-coherence", fn: () => runCodeCoherence(resolvedCwd) },
|
|
86
91
|
{ name: "comment-staleness", fn: () => runCommentStaleness(resolvedCwd) },
|
|
92
|
+
{ name: "html-quality", fn: () => runHtmlQuality(resolvedCwd) },
|
|
93
|
+
{ name: "frontend-health", fn: () => runFrontendHealth(resolvedCwd) },
|
|
94
|
+
{ name: "styling", fn: () => runStyling(resolvedCwd) },
|
|
87
95
|
{ name: "dead-patterns", fn: () => runDeadPatterns(resolvedCwd) },
|
|
88
96
|
{ name: "test-audit", fn: () => runTestAudit(resolvedCwd) },
|
|
97
|
+
{ name: "file-cohesion", fn: () => runFileCohesion(resolvedCwd) },
|
|
98
|
+
{ name: "design-consistency", fn: () => runDesignConsistency(resolvedCwd) },
|
|
89
99
|
];
|
|
90
100
|
// Filter checks if specified
|
|
91
101
|
const checkFilter = options.checks ? new Set(options.checks) : null;
|
|
@@ -166,6 +176,7 @@ export async function scan(cwd, options = {}) {
|
|
|
166
176
|
// ── Re-exports ──
|
|
167
177
|
export { CHECK_META, getCheckMeta };
|
|
168
178
|
export { computeScore } from "./score.js";
|
|
179
|
+
export { computeDelta, formatDeltaMarkdown } from "./delta.js";
|
|
169
180
|
export { loadConfig } from "./config.js";
|
|
170
181
|
export { detectStack, detectWorkspace } from "./detect.js";
|
|
171
182
|
export { gradeFromScore } from "./types.js";
|
package/dist/delta.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Delta report — structured diff between two scans.
|
|
2
|
+
*
|
|
3
|
+
* Used by `vcqa fix` to show before/after, and by the Actions page
|
|
4
|
+
* to display "what changed since last scan."
|
|
5
|
+
*/
|
|
6
|
+
import type { Issue, VibeReport } from "./types.js";
|
|
7
|
+
export interface DeltaIssue {
|
|
8
|
+
check: string;
|
|
9
|
+
severity: Issue["severity"];
|
|
10
|
+
message: string;
|
|
11
|
+
file?: string;
|
|
12
|
+
line?: number;
|
|
13
|
+
rule?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface CheckDelta {
|
|
16
|
+
name: string;
|
|
17
|
+
label: string;
|
|
18
|
+
before: number;
|
|
19
|
+
after: number;
|
|
20
|
+
delta: number;
|
|
21
|
+
fixed: DeltaIssue[];
|
|
22
|
+
introduced: DeltaIssue[];
|
|
23
|
+
}
|
|
24
|
+
export interface ScanDelta {
|
|
25
|
+
before: {
|
|
26
|
+
score: number;
|
|
27
|
+
grade: string;
|
|
28
|
+
timestamp: string;
|
|
29
|
+
issueCount: number;
|
|
30
|
+
};
|
|
31
|
+
after: {
|
|
32
|
+
score: number;
|
|
33
|
+
grade: string;
|
|
34
|
+
timestamp: string;
|
|
35
|
+
issueCount: number;
|
|
36
|
+
};
|
|
37
|
+
scoreDelta: number;
|
|
38
|
+
checks: CheckDelta[];
|
|
39
|
+
fixed: DeltaIssue[];
|
|
40
|
+
introduced: DeltaIssue[];
|
|
41
|
+
}
|
|
42
|
+
/** Compute a structured delta between two scan reports. */
|
|
43
|
+
export declare function computeDelta(before: VibeReport, after: VibeReport): ScanDelta;
|
|
44
|
+
/** Format a delta as a markdown report. */
|
|
45
|
+
export declare function formatDeltaMarkdown(delta: ScanDelta): string;
|
package/dist/delta.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/** Delta report — structured diff between two scans.
|
|
2
|
+
*
|
|
3
|
+
* Used by `vcqa fix` to show before/after, and by the Actions page
|
|
4
|
+
* to display "what changed since last scan."
|
|
5
|
+
*/
|
|
6
|
+
/** Fingerprint an issue for stable matching (ignores line numbers which shift after edits). */
|
|
7
|
+
function issueKey(check, iss) {
|
|
8
|
+
const file = typeof iss.file === "string" ? iss.file.split(":")[0] : "";
|
|
9
|
+
return `${check}|${iss.rule || ""}|${file}|${iss.message}`;
|
|
10
|
+
}
|
|
11
|
+
/** Compute a structured delta between two scan reports. */
|
|
12
|
+
export function computeDelta(before, after) {
|
|
13
|
+
const beforeIssueCount = before.checks.reduce((s, c) => s + c.issues.length, 0);
|
|
14
|
+
const afterIssueCount = after.checks.reduce((s, c) => s + c.issues.length, 0);
|
|
15
|
+
const checks = [];
|
|
16
|
+
const allFixed = [];
|
|
17
|
+
const allIntroduced = [];
|
|
18
|
+
for (const afterCheck of after.checks) {
|
|
19
|
+
const beforeCheck = before.checks.find((c) => c.name === afterCheck.name);
|
|
20
|
+
const beforeScore = beforeCheck?.score ?? 0;
|
|
21
|
+
// Build multiset of issue keys for before and after
|
|
22
|
+
const beforeKeys = new Map();
|
|
23
|
+
const afterKeys = new Map();
|
|
24
|
+
if (beforeCheck) {
|
|
25
|
+
for (const iss of beforeCheck.issues) {
|
|
26
|
+
const key = issueKey(afterCheck.name, iss);
|
|
27
|
+
const entry = beforeKeys.get(key);
|
|
28
|
+
if (entry)
|
|
29
|
+
entry.count++;
|
|
30
|
+
else
|
|
31
|
+
beforeKeys.set(key, { count: 1, issue: iss });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
for (const iss of afterCheck.issues) {
|
|
35
|
+
const key = issueKey(afterCheck.name, iss);
|
|
36
|
+
const entry = afterKeys.get(key);
|
|
37
|
+
if (entry)
|
|
38
|
+
entry.count++;
|
|
39
|
+
else
|
|
40
|
+
afterKeys.set(key, { count: 1, issue: iss });
|
|
41
|
+
}
|
|
42
|
+
const fixed = [];
|
|
43
|
+
const introduced = [];
|
|
44
|
+
// Fixed: in before but not in after (or count decreased)
|
|
45
|
+
for (const [key, bEntry] of beforeKeys) {
|
|
46
|
+
const aEntry = afterKeys.get(key);
|
|
47
|
+
const aCount = aEntry?.count ?? 0;
|
|
48
|
+
const diff = bEntry.count - aCount;
|
|
49
|
+
for (let i = 0; i < diff; i++) {
|
|
50
|
+
const di = {
|
|
51
|
+
check: afterCheck.name,
|
|
52
|
+
severity: bEntry.issue.severity,
|
|
53
|
+
message: bEntry.issue.message,
|
|
54
|
+
file: typeof bEntry.issue.file === "string" ? bEntry.issue.file : undefined,
|
|
55
|
+
line: bEntry.issue.line,
|
|
56
|
+
rule: bEntry.issue.rule,
|
|
57
|
+
};
|
|
58
|
+
fixed.push(di);
|
|
59
|
+
allFixed.push(di);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Introduced: in after but not in before (or count increased)
|
|
63
|
+
for (const [key, aEntry] of afterKeys) {
|
|
64
|
+
const bEntry = beforeKeys.get(key);
|
|
65
|
+
const bCount = bEntry?.count ?? 0;
|
|
66
|
+
const diff = aEntry.count - bCount;
|
|
67
|
+
for (let i = 0; i < diff; i++) {
|
|
68
|
+
const di = {
|
|
69
|
+
check: afterCheck.name,
|
|
70
|
+
severity: aEntry.issue.severity,
|
|
71
|
+
message: aEntry.issue.message,
|
|
72
|
+
file: typeof aEntry.issue.file === "string" ? aEntry.issue.file : undefined,
|
|
73
|
+
line: aEntry.issue.line,
|
|
74
|
+
rule: aEntry.issue.rule,
|
|
75
|
+
};
|
|
76
|
+
introduced.push(di);
|
|
77
|
+
allIntroduced.push(di);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
checks.push({
|
|
81
|
+
name: afterCheck.name,
|
|
82
|
+
label: afterCheck.name,
|
|
83
|
+
before: beforeScore,
|
|
84
|
+
after: afterCheck.score,
|
|
85
|
+
delta: afterCheck.score - beforeScore,
|
|
86
|
+
fixed,
|
|
87
|
+
introduced,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
before: { score: before.score, grade: before.grade, timestamp: before.timestamp, issueCount: beforeIssueCount },
|
|
92
|
+
after: { score: after.score, grade: after.grade, timestamp: after.timestamp, issueCount: afterIssueCount },
|
|
93
|
+
scoreDelta: after.score - before.score,
|
|
94
|
+
checks: checks.filter((c) => c.delta !== 0 || c.fixed.length > 0 || c.introduced.length > 0),
|
|
95
|
+
fixed: allFixed,
|
|
96
|
+
introduced: allIntroduced,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** Format a delta as a markdown report. */
|
|
100
|
+
export function formatDeltaMarkdown(delta) {
|
|
101
|
+
const arrow = delta.scoreDelta > 0 ? "+" : "";
|
|
102
|
+
const emoji = delta.scoreDelta > 0 ? "improvement" : delta.scoreDelta < 0 ? "regression" : "no change";
|
|
103
|
+
let md = `# VibeCode QA — Delta Report\n\n`;
|
|
104
|
+
md += `| | Before | After | Delta |\n|---|---|---|---|\n`;
|
|
105
|
+
md += `| **Score** | ${delta.before.grade} ${delta.before.score} | ${delta.after.grade} ${delta.after.score} | ${arrow}${delta.scoreDelta} (${emoji}) |\n`;
|
|
106
|
+
md += `| **Issues** | ${delta.before.issueCount} | ${delta.after.issueCount} | ${delta.fixed.length} fixed, ${delta.introduced.length} new |\n\n`;
|
|
107
|
+
// Per-check changes
|
|
108
|
+
const changed = delta.checks.filter((c) => c.delta !== 0);
|
|
109
|
+
if (changed.length > 0) {
|
|
110
|
+
md += `## Check Changes\n\n`;
|
|
111
|
+
md += `| Check | Before | After | Delta |\n|---|---|---|---|\n`;
|
|
112
|
+
for (const c of changed.sort((a, b) => b.delta - a.delta)) {
|
|
113
|
+
const a = c.delta > 0 ? "+" : "";
|
|
114
|
+
md += `| ${c.name} | ${c.before} | ${c.after} | ${a}${c.delta} |\n`;
|
|
115
|
+
}
|
|
116
|
+
md += "\n";
|
|
117
|
+
}
|
|
118
|
+
// Fixed issues
|
|
119
|
+
if (delta.fixed.length > 0) {
|
|
120
|
+
md += `## Fixed (${delta.fixed.length})\n\n`;
|
|
121
|
+
// Group by check
|
|
122
|
+
const byCheck = new Map();
|
|
123
|
+
for (const f of delta.fixed) {
|
|
124
|
+
const arr = byCheck.get(f.check) || [];
|
|
125
|
+
arr.push(f);
|
|
126
|
+
byCheck.set(f.check, arr);
|
|
127
|
+
}
|
|
128
|
+
for (const [check, issues] of byCheck) {
|
|
129
|
+
md += `### ${check} (${issues.length} fixed)\n`;
|
|
130
|
+
for (const iss of issues.slice(0, 10)) {
|
|
131
|
+
md += `- ${iss.file ? `\`${iss.file}\`` : ""} ${iss.message}\n`;
|
|
132
|
+
}
|
|
133
|
+
if (issues.length > 10)
|
|
134
|
+
md += `- ...and ${issues.length - 10} more\n`;
|
|
135
|
+
md += "\n";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// New issues
|
|
139
|
+
if (delta.introduced.length > 0) {
|
|
140
|
+
md += `## New Issues (${delta.introduced.length})\n\n`;
|
|
141
|
+
const byCheck = new Map();
|
|
142
|
+
for (const f of delta.introduced) {
|
|
143
|
+
const arr = byCheck.get(f.check) || [];
|
|
144
|
+
arr.push(f);
|
|
145
|
+
byCheck.set(f.check, arr);
|
|
146
|
+
}
|
|
147
|
+
for (const [check, issues] of byCheck) {
|
|
148
|
+
md += `### ${check} (${issues.length} new)\n`;
|
|
149
|
+
for (const iss of issues.slice(0, 10)) {
|
|
150
|
+
md += `- ${iss.file ? `\`${iss.file}\`` : ""} ${iss.message}\n`;
|
|
151
|
+
}
|
|
152
|
+
if (issues.length > 10)
|
|
153
|
+
md += `- ...and ${issues.length - 10} more\n`;
|
|
154
|
+
md += "\n";
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return md;
|
|
158
|
+
}
|