@vibecodeqa/cli 0.41.0 → 0.43.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 +99 -6
- package/dist/cli.js +268 -761
- 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 +131 -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.js +18 -0
- package/dist/runners/accessibility.js +4 -1
- package/dist/runners/container-health.d.ts +3 -0
- package/dist/runners/container-health.js +141 -0
- package/dist/runners/design-consistency.d.ts +12 -0
- package/dist/runners/design-consistency.js +125 -0
- package/dist/runners/env-validation.d.ts +3 -0
- package/dist/runners/env-validation.js +122 -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/git-hygiene.d.ts +3 -0
- package/dist/runners/git-hygiene.js +125 -0
- package/dist/runners/html-quality.d.ts +8 -0
- package/dist/runners/html-quality.js +203 -0
- package/dist/runners/memory-safety.d.ts +3 -0
- package/dist/runners/memory-safety.js +114 -0
- 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.js +29 -9
- package/dist/runners/styling.d.ts +15 -0
- package/dist/runners/styling.js +280 -0
- package/package.json +1 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/** Design Consistency — LLM-powered audit of visual consistency across components.
|
|
2
|
+
*
|
|
3
|
+
* Pro feature. Requires VCQA_PRO_KEY env var.
|
|
4
|
+
*
|
|
5
|
+
* Detects:
|
|
6
|
+
* - Components that define the same visual pattern differently (accidental design system)
|
|
7
|
+
* - Spacing/color/typography inconsistencies across files
|
|
8
|
+
* - Missing component extraction opportunities
|
|
9
|
+
* - Tailwind class patterns that should be @apply'd or componentized
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { getProductionFiles } from "../fs-utils.js";
|
|
15
|
+
import { gradeFromScore } from "../types.js";
|
|
16
|
+
export async function runDesignConsistency(cwd) {
|
|
17
|
+
const start = Date.now();
|
|
18
|
+
const proKey = process.env.VCQA_PRO_KEY || "";
|
|
19
|
+
if (!proKey) {
|
|
20
|
+
return {
|
|
21
|
+
name: "design-consistency",
|
|
22
|
+
score: 0,
|
|
23
|
+
grade: "F",
|
|
24
|
+
details: {
|
|
25
|
+
premium: true,
|
|
26
|
+
comingSoon: true,
|
|
27
|
+
reason: "Set VCQA_PRO_KEY to enable design consistency analysis",
|
|
28
|
+
description: "LLM-powered audit of visual consistency — finds components with duplicate styling, inconsistent spacing scales, and missing component extraction opportunities.",
|
|
29
|
+
},
|
|
30
|
+
issues: [],
|
|
31
|
+
duration: Date.now() - start,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const files = getProductionFiles(cwd);
|
|
35
|
+
const componentFiles = files.filter((f) => !f.isTest && /\.(tsx|jsx|vue|svelte)$/.test(f.path));
|
|
36
|
+
if (componentFiles.length < 2) {
|
|
37
|
+
return {
|
|
38
|
+
name: "design-consistency",
|
|
39
|
+
score: 100,
|
|
40
|
+
grade: "A",
|
|
41
|
+
details: { componentsAnalyzed: componentFiles.length },
|
|
42
|
+
issues: [],
|
|
43
|
+
duration: Date.now() - start,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Build hash of all component content for cache key
|
|
47
|
+
const h = createHash("sha256");
|
|
48
|
+
for (const f of componentFiles.sort((a, b) => a.path.localeCompare(b.path))) {
|
|
49
|
+
h.update(f.path);
|
|
50
|
+
h.update(f.content.slice(0, 2000));
|
|
51
|
+
}
|
|
52
|
+
const contentHash = h.digest("hex").slice(0, 16);
|
|
53
|
+
// Check cache
|
|
54
|
+
const cache = loadCache(cwd);
|
|
55
|
+
if (cache && cache.hash === contentHash) {
|
|
56
|
+
return {
|
|
57
|
+
name: "design-consistency",
|
|
58
|
+
score: cache.findings.length === 0 ? 100 : Math.max(20, 100 - cache.findings.length * 12),
|
|
59
|
+
grade: gradeFromScore(cache.findings.length === 0 ? 100 : Math.max(20, 100 - cache.findings.length * 12)),
|
|
60
|
+
details: { premium: true, componentsAnalyzed: componentFiles.length, cached: true },
|
|
61
|
+
issues: cache.findings,
|
|
62
|
+
duration: Date.now() - start,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// Extract styling snippets from top components (by size, most likely to have styling)
|
|
66
|
+
const candidates = componentFiles
|
|
67
|
+
.sort((a, b) => b.content.length - a.content.length)
|
|
68
|
+
.slice(0, 10);
|
|
69
|
+
const snippets = candidates.map((f) => ({
|
|
70
|
+
path: f.path,
|
|
71
|
+
content: f.content.slice(0, 2000),
|
|
72
|
+
}));
|
|
73
|
+
const issues = await analyzeDesign(snippets, proKey);
|
|
74
|
+
// Cache results
|
|
75
|
+
saveCache(cwd, { version: 1, hash: contentHash, findings: issues });
|
|
76
|
+
const score = issues.length === 0 ? 100 : Math.max(20, 100 - issues.length * 12);
|
|
77
|
+
return {
|
|
78
|
+
name: "design-consistency",
|
|
79
|
+
score,
|
|
80
|
+
grade: gradeFromScore(score),
|
|
81
|
+
details: { premium: true, componentsAnalyzed: componentFiles.length },
|
|
82
|
+
issues,
|
|
83
|
+
duration: Date.now() - start,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function analyzeDesign(files, proKey) {
|
|
87
|
+
try {
|
|
88
|
+
const res = await fetch("https://api.vibecodeqa.online/api/pro/design-consistency", {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: {
|
|
91
|
+
"Content-Type": "application/json",
|
|
92
|
+
Authorization: `Bearer ${proKey}`,
|
|
93
|
+
},
|
|
94
|
+
body: JSON.stringify({ files: files.map((f) => ({ path: f.path, content: f.content })) }),
|
|
95
|
+
});
|
|
96
|
+
if (!res.ok)
|
|
97
|
+
return [];
|
|
98
|
+
const data = (await res.json());
|
|
99
|
+
return data.findings || [];
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function loadCache(cwd) {
|
|
106
|
+
try {
|
|
107
|
+
const cachePath = join(cwd, ".vibe-check", "design-consistency-cache.json");
|
|
108
|
+
if (existsSync(cachePath)) {
|
|
109
|
+
const data = JSON.parse(readFileSync(cachePath, "utf-8"));
|
|
110
|
+
if (data.version === 1)
|
|
111
|
+
return data;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch { /* corrupt cache */ }
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
function saveCache(cwd, cache) {
|
|
118
|
+
try {
|
|
119
|
+
const dir = join(cwd, ".vibe-check");
|
|
120
|
+
if (!existsSync(dir))
|
|
121
|
+
mkdirSync(dir, { recursive: true });
|
|
122
|
+
writeFileSync(join(dir, "design-consistency-cache.json"), JSON.stringify(cache));
|
|
123
|
+
}
|
|
124
|
+
catch { /* write failed */ }
|
|
125
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/** Environment validation — checks .env hygiene, .env.example drift, and unsafe patterns. */
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { gradeFromScore } from "../types.js";
|
|
5
|
+
export function runEnvValidation(cwd) {
|
|
6
|
+
const start = Date.now();
|
|
7
|
+
const issues = [];
|
|
8
|
+
const envFiles = readdirSync(cwd).filter((f) => f.startsWith(".env"));
|
|
9
|
+
const hasEnv = envFiles.some((f) => f === ".env" || f === ".env.local");
|
|
10
|
+
const hasExample = envFiles.some((f) => f === ".env.example" || f === ".env.template");
|
|
11
|
+
// Check .gitignore includes .env
|
|
12
|
+
if (hasEnv) {
|
|
13
|
+
const gitignore = existsSync(join(cwd, ".gitignore")) ? readFileSync(join(cwd, ".gitignore"), "utf-8") : "";
|
|
14
|
+
if (!gitignore.includes(".env")) {
|
|
15
|
+
issues.push({ severity: "error", message: ".env not in .gitignore — secrets may be committed", file: ".gitignore", rule: "env-not-ignored" });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Check .env.example exists when .env does
|
|
19
|
+
if (hasEnv && !hasExample) {
|
|
20
|
+
issues.push({ severity: "warning", message: "No .env.example — other developers won't know which vars are needed", rule: "no-env-example" });
|
|
21
|
+
}
|
|
22
|
+
// Check .env.example drift — vars in .env.example should match .env
|
|
23
|
+
if (hasEnv && hasExample) {
|
|
24
|
+
const exampleFile = envFiles.find((f) => f === ".env.example" || f === ".env.template");
|
|
25
|
+
const envVars = parseEnvKeys(readFileSync(join(cwd, ".env"), "utf-8"));
|
|
26
|
+
const exampleVars = parseEnvKeys(readFileSync(join(cwd, exampleFile), "utf-8"));
|
|
27
|
+
for (const key of exampleVars) {
|
|
28
|
+
if (!envVars.has(key)) {
|
|
29
|
+
issues.push({ severity: "info", message: `${exampleFile} has ${key} but .env doesn't — may be missing`, file: exampleFile, rule: "env-example-drift" });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
for (const key of envVars) {
|
|
33
|
+
if (!exampleVars.has(key)) {
|
|
34
|
+
issues.push({ severity: "warning", message: `${key} in .env but not in ${exampleFile} — won't be documented for other developers`, file: exampleFile, rule: "env-example-drift" });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Scan .env files for unsafe patterns
|
|
39
|
+
for (const f of envFiles) {
|
|
40
|
+
if (f === ".env.example" || f === ".env.template")
|
|
41
|
+
continue;
|
|
42
|
+
const content = readFileSync(join(cwd, f), "utf-8");
|
|
43
|
+
const lines = content.split("\n");
|
|
44
|
+
for (let i = 0; i < lines.length; i++) {
|
|
45
|
+
const line = lines[i].trim();
|
|
46
|
+
if (!line || line.startsWith("#"))
|
|
47
|
+
continue;
|
|
48
|
+
// Check for values that look like they should be secret but have defaults
|
|
49
|
+
if (/^(DATABASE_URL|DB_PASSWORD|SECRET_KEY|JWT_SECRET|API_KEY|PRIVATE_KEY)=/i.test(line)) {
|
|
50
|
+
const value = line.split("=").slice(1).join("=").trim().replace(/^["']|["']$/g, "");
|
|
51
|
+
if (value && !value.startsWith("$") && !value.includes("${") && value.length < 20 && !/^(changeme|replace|todo|xxx|your[-_])/i.test(value)) {
|
|
52
|
+
issues.push({
|
|
53
|
+
severity: "warning",
|
|
54
|
+
message: `${line.split("=")[0]} appears to have a hardcoded value — use a placeholder in committed files`,
|
|
55
|
+
file: f,
|
|
56
|
+
line: i + 1,
|
|
57
|
+
rule: "env-hardcoded-secret",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Check for empty required-looking vars
|
|
62
|
+
if (/^[A-Z_]+=\s*$/.test(line)) {
|
|
63
|
+
const key = line.split("=")[0];
|
|
64
|
+
if (/KEY|SECRET|TOKEN|PASSWORD|URL/i.test(key)) {
|
|
65
|
+
issues.push({
|
|
66
|
+
severity: "info",
|
|
67
|
+
message: `${key} is empty — may cause runtime errors`,
|
|
68
|
+
file: f,
|
|
69
|
+
line: i + 1,
|
|
70
|
+
rule: "env-empty-var",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Check for env vars used in code but not in .env.example
|
|
77
|
+
if (hasExample) {
|
|
78
|
+
const exampleFile = envFiles.find((f) => f === ".env.example" || f === ".env.template");
|
|
79
|
+
const exampleVars = parseEnvKeys(readFileSync(join(cwd, exampleFile), "utf-8"));
|
|
80
|
+
// Quick scan of package.json for referenced env vars
|
|
81
|
+
if (existsSync(join(cwd, "package.json"))) {
|
|
82
|
+
try {
|
|
83
|
+
const pkg = readFileSync(join(cwd, "package.json"), "utf-8");
|
|
84
|
+
const envRefs = pkg.match(/process\.env\.([A-Z_]+)/g) || [];
|
|
85
|
+
for (const ref of new Set(envRefs)) {
|
|
86
|
+
const varName = ref.replace("process.env.", "");
|
|
87
|
+
if (!exampleVars.has(varName) && !["NODE_ENV", "CI", "HOME", "PATH", "PWD"].includes(varName)) {
|
|
88
|
+
issues.push({
|
|
89
|
+
severity: "info",
|
|
90
|
+
message: `${varName} used in code but not in ${exampleFile}`,
|
|
91
|
+
rule: "env-undocumented",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch { /* ignore */ }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const errorCount = issues.filter((i) => i.severity === "error").length;
|
|
100
|
+
const warnCount = issues.filter((i) => i.severity === "warning").length;
|
|
101
|
+
const score = Math.max(0, 100 - errorCount * 25 - warnCount * 10);
|
|
102
|
+
return {
|
|
103
|
+
name: "env-validation",
|
|
104
|
+
score,
|
|
105
|
+
grade: gradeFromScore(score),
|
|
106
|
+
details: { envFiles, hasExample },
|
|
107
|
+
issues,
|
|
108
|
+
duration: Date.now() - start,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function parseEnvKeys(content) {
|
|
112
|
+
const keys = new Set();
|
|
113
|
+
for (const line of content.split("\n")) {
|
|
114
|
+
const trimmed = line.trim();
|
|
115
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
116
|
+
continue;
|
|
117
|
+
const eq = trimmed.indexOf("=");
|
|
118
|
+
if (eq > 0)
|
|
119
|
+
keys.add(trimmed.slice(0, eq).trim());
|
|
120
|
+
}
|
|
121
|
+
return keys;
|
|
122
|
+
}
|
|
@@ -25,12 +25,22 @@ export function runErrorHandling(cwd, stack) {
|
|
|
25
25
|
const line = lines[i].trim();
|
|
26
26
|
if (line.startsWith("//") || line.startsWith("*"))
|
|
27
27
|
continue;
|
|
28
|
-
// Skip
|
|
28
|
+
// Skip suppressed lines (// ok, // intentional, eslint-disable, biome-ignore)
|
|
29
|
+
if (/\/\/\s*(?:ok|intentional|eslint-disable|biome-ignore)/.test(line))
|
|
30
|
+
continue;
|
|
31
|
+
// Skip string-only lines, metadata definitions, and return statements with string literals
|
|
29
32
|
if (/^\s*["'`].*["'`][,;]?\s*$/.test(lines[i]))
|
|
30
33
|
continue;
|
|
31
34
|
if (/\b(?:message|description|risk|recommendation|name)\s*:\s*["']/.test(line))
|
|
32
35
|
continue;
|
|
36
|
+
if (/\breturn\s+["'`]/.test(line))
|
|
37
|
+
continue;
|
|
38
|
+
if (/\brule\s*===?\s*["']/.test(line))
|
|
39
|
+
continue;
|
|
33
40
|
if (/catch\s*\([^)]*\)\s*\{\s*\}/.test(line) || /catch\s*\{\s*\}/.test(line)) {
|
|
41
|
+
// Skip intentional one-liner try-catch for optional browser APIs
|
|
42
|
+
if (/try\s*\{.*(?:localStorage|sessionStorage|document\.).*\}\s*catch/.test(line))
|
|
43
|
+
continue;
|
|
34
44
|
emptyCatch++;
|
|
35
45
|
issues.push({ severity: "error", message: "Empty catch block", file: f.path, line: i + 1, rule: "empty-catch" });
|
|
36
46
|
}
|
|
@@ -83,8 +93,14 @@ export function runErrorHandling(cwd, stack) {
|
|
|
83
93
|
const line = lines[i].trim();
|
|
84
94
|
if (line.startsWith("//") || line.startsWith("*"))
|
|
85
95
|
continue;
|
|
96
|
+
if (/\/\/\s*(?:ok|intentional|eslint-disable|biome-ignore)/.test(line))
|
|
97
|
+
continue;
|
|
98
|
+
if (/^\s*["'`].*["'`][,;]?\s*$/.test(lines[i]))
|
|
99
|
+
continue;
|
|
86
100
|
if (/\b(?:message|description|risk|recommendation|name)\s*:\s*["']/.test(line))
|
|
87
101
|
continue;
|
|
102
|
+
if (/\breturn\s+["'`]/.test(line))
|
|
103
|
+
continue;
|
|
88
104
|
// JSON.parse of external input without try-catch
|
|
89
105
|
if (/\bJSON\.parse\s*\(/.test(line)) {
|
|
90
106
|
// Only flag if parsing user/network input (not internal known-safe files)
|
|
@@ -141,7 +157,7 @@ export function runErrorHandling(cwd, stack) {
|
|
|
141
157
|
}
|
|
142
158
|
}
|
|
143
159
|
// process.exit() in non-CLI files (library code shouldn't exit)
|
|
144
|
-
if (/\bprocess\.exit\s*\(/.test(line) && !f.path.includes("cli") && !f.path.includes("bin/")) {
|
|
160
|
+
if (/\bprocess\.exit\s*\(/.test(line) && !f.path.includes("cli") && !f.path.includes("bin/") && !f.path.includes("commands/") && !f.path.includes("monitor")) {
|
|
145
161
|
processExit++;
|
|
146
162
|
issues.push({
|
|
147
163
|
severity: "warning",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** File Cohesion — detects files with multiple responsibilities.
|
|
2
|
+
*
|
|
3
|
+
* Pro feature. Requires VCQA_PRO_KEY env var.
|
|
4
|
+
*
|
|
5
|
+
* Two-phase analysis:
|
|
6
|
+
* 1. Local heuristics (always run with Pro key):
|
|
7
|
+
* - Files with exports spanning multiple domains (auth + email + db)
|
|
8
|
+
* - Mixed concern signals: HTTP handlers + business logic + data access
|
|
9
|
+
* - High export count relative to file purpose
|
|
10
|
+
*
|
|
11
|
+
* 2. LLM-powered analysis (via api.vibecodeqa.online):
|
|
12
|
+
* - Labels each file's responsibility clusters
|
|
13
|
+
* - Suggests concrete split points
|
|
14
|
+
* - "This file handles auth, session, AND email — split into 3 modules"
|
|
15
|
+
*/
|
|
16
|
+
import type { CheckResult } from "../types.js";
|
|
17
|
+
export declare function runFileCohesion(cwd: string): Promise<CheckResult>;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/** File Cohesion — detects files with multiple responsibilities.
|
|
2
|
+
*
|
|
3
|
+
* Pro feature. Requires VCQA_PRO_KEY env var.
|
|
4
|
+
*
|
|
5
|
+
* Two-phase analysis:
|
|
6
|
+
* 1. Local heuristics (always run with Pro key):
|
|
7
|
+
* - Files with exports spanning multiple domains (auth + email + db)
|
|
8
|
+
* - Mixed concern signals: HTTP handlers + business logic + data access
|
|
9
|
+
* - High export count relative to file purpose
|
|
10
|
+
*
|
|
11
|
+
* 2. LLM-powered analysis (via api.vibecodeqa.online):
|
|
12
|
+
* - Labels each file's responsibility clusters
|
|
13
|
+
* - Suggests concrete split points
|
|
14
|
+
* - "This file handles auth, session, AND email — split into 3 modules"
|
|
15
|
+
*/
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { getProductionFiles } from "../fs-utils.js";
|
|
20
|
+
import { gradeFromScore } from "../types.js";
|
|
21
|
+
const CONCERN_PATTERNS = [
|
|
22
|
+
{ name: "HTTP/routing", patterns: [/\b(app|router|server)\.(get|post|put|delete|patch|use)\b/, /\bRequest\b.*\bResponse\b/, /\breq\s*,\s*res\b/, /\bfastify\b|\bhono\b|\bexpress\b/] },
|
|
23
|
+
{ name: "database", patterns: [/\b(prisma|sequelize|typeorm|knex|drizzle)\b/, /\bSELECT\b.*\bFROM\b/i, /\.query\s*\(/, /\bcreateClient\b.*\bsupabase\b/i] },
|
|
24
|
+
{ name: "auth", patterns: [/\b(jwt|token|session|cookie|passport|oauth|login|signup|signIn|signUp)\b/i, /\bverify(Token|Session|Auth)\b/, /\bbcrypt|argon2|scrypt\b/] },
|
|
25
|
+
{ name: "email", patterns: [/\bsendMail|sendEmail|transporter|nodemailer|resend|postmark\b/i, /\bsmtp\b/i] },
|
|
26
|
+
{ name: "file I/O", patterns: [/\breadFileSync|writeFileSync|createReadStream|createWriteStream\b/, /\bfs\.(read|write|mkdir|unlink|stat)\b/] },
|
|
27
|
+
{ name: "validation", patterns: [/\bz\.(string|number|object|array)\b/, /\bJoi\.(string|number|object)\b/, /\byup\.(string|number|object)\b/, /\bvalidate\w*Schema\b/] },
|
|
28
|
+
{ name: "UI rendering", patterns: [/\bJSX\b|\breturn\s*\(?\s*</, /\buseState|useEffect|useRef|useMemo\b/, /\brender\(\)/, /\bcomponent\b/i] },
|
|
29
|
+
{ name: "state management", patterns: [/\bcreateStore|useStore|createSlice|createReducer\b/, /\bdispatch\(|getState\(\)/, /\buseSelector|useDispatch\b/] },
|
|
30
|
+
{ name: "testing", patterns: [/\bdescribe\s*\(|it\s*\(|test\s*\(|expect\s*\(/, /\bbeforeEach|afterEach|beforeAll\b/, /\bjest\.|vitest\./] },
|
|
31
|
+
{ name: "CLI", patterns: [/\bprocess\.argv\b/, /\bcommander|yargs|meow|cac\b/, /\bparse(Args|Options)\b/] },
|
|
32
|
+
];
|
|
33
|
+
export async function runFileCohesion(cwd) {
|
|
34
|
+
const start = Date.now();
|
|
35
|
+
const proKey = process.env.VCQA_PRO_KEY || "";
|
|
36
|
+
if (!proKey) {
|
|
37
|
+
return {
|
|
38
|
+
name: "file-cohesion",
|
|
39
|
+
score: 0,
|
|
40
|
+
grade: "F",
|
|
41
|
+
details: {
|
|
42
|
+
premium: true,
|
|
43
|
+
comingSoon: true,
|
|
44
|
+
reason: "Set VCQA_PRO_KEY to enable file cohesion analysis",
|
|
45
|
+
description: "Detects files with multiple responsibilities — the #1 code smell in AI-generated code. Labels each file's concern clusters and suggests concrete split points.",
|
|
46
|
+
},
|
|
47
|
+
issues: [],
|
|
48
|
+
duration: Date.now() - start,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const files = getProductionFiles(cwd);
|
|
52
|
+
const issues = [];
|
|
53
|
+
const cache = loadCache(cwd);
|
|
54
|
+
let cacheHits = 0;
|
|
55
|
+
// Phase 1: local heuristics — detect multi-concern files
|
|
56
|
+
const candidates = [];
|
|
57
|
+
for (const f of files) {
|
|
58
|
+
if (f.isTest)
|
|
59
|
+
continue;
|
|
60
|
+
const lines = f.content.split("\n").length;
|
|
61
|
+
if (lines < 100)
|
|
62
|
+
continue; // small files rarely have cohesion problems
|
|
63
|
+
const detectedConcerns = [];
|
|
64
|
+
for (const concern of CONCERN_PATTERNS) {
|
|
65
|
+
if (concern.patterns.some((p) => p.test(f.content))) {
|
|
66
|
+
detectedConcerns.push(concern.name);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (detectedConcerns.length >= 2) {
|
|
70
|
+
candidates.push({ path: f.path, content: f.content, concerns: detectedConcerns, lines });
|
|
71
|
+
// Local issue for 2+ concerns
|
|
72
|
+
issues.push({
|
|
73
|
+
severity: detectedConcerns.length >= 3 ? "error" : "warning",
|
|
74
|
+
message: `${detectedConcerns.length} concerns detected: ${detectedConcerns.join(", ")} — consider splitting`,
|
|
75
|
+
file: f.path,
|
|
76
|
+
rule: "multi-concern",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Phase 2: LLM analysis for top candidates (by line count, most likely to benefit from splitting)
|
|
81
|
+
const topCandidates = candidates
|
|
82
|
+
.sort((a, b) => b.lines - a.lines)
|
|
83
|
+
.slice(0, 5);
|
|
84
|
+
for (const candidate of topCandidates) {
|
|
85
|
+
const hash = createHash("sha256").update(candidate.content).digest("hex").slice(0, 16);
|
|
86
|
+
// Check cache
|
|
87
|
+
const cached = cache.files[candidate.path];
|
|
88
|
+
if (cached && cached.hash === hash) {
|
|
89
|
+
cacheHits++;
|
|
90
|
+
if (cached.suggestion) {
|
|
91
|
+
issues.push({
|
|
92
|
+
severity: "warning",
|
|
93
|
+
message: cached.suggestion,
|
|
94
|
+
file: candidate.path,
|
|
95
|
+
rule: "split-suggestion",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
// Call LLM for concrete split suggestions
|
|
101
|
+
const analysis = await analyzeCohesion(candidate.path, candidate.content, proKey);
|
|
102
|
+
if (analysis) {
|
|
103
|
+
cache.files[candidate.path] = { hash, concerns: analysis.concerns, suggestion: analysis.suggestion };
|
|
104
|
+
if (analysis.suggestion) {
|
|
105
|
+
issues.push({
|
|
106
|
+
severity: "warning",
|
|
107
|
+
message: analysis.suggestion,
|
|
108
|
+
file: candidate.path,
|
|
109
|
+
rule: "split-suggestion",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
saveCache(cwd, cache);
|
|
115
|
+
const totalFiles = files.filter((f) => !f.isTest).length;
|
|
116
|
+
const multiConcernFiles = candidates.length;
|
|
117
|
+
const ratio = totalFiles > 0 ? multiConcernFiles / totalFiles : 0;
|
|
118
|
+
const score = Math.round(Math.max(0, 100 - ratio * 300));
|
|
119
|
+
return {
|
|
120
|
+
name: "file-cohesion",
|
|
121
|
+
score,
|
|
122
|
+
grade: gradeFromScore(score),
|
|
123
|
+
details: {
|
|
124
|
+
premium: true,
|
|
125
|
+
totalFiles,
|
|
126
|
+
multiConcernFiles,
|
|
127
|
+
cacheHits,
|
|
128
|
+
candidates: candidates.map((c) => ({ path: c.path, concerns: c.concerns, lines: c.lines })),
|
|
129
|
+
},
|
|
130
|
+
issues,
|
|
131
|
+
duration: Date.now() - start,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async function analyzeCohesion(filePath, content, proKey) {
|
|
135
|
+
const truncated = content.slice(0, 4000);
|
|
136
|
+
try {
|
|
137
|
+
const res = await fetch("https://api.vibecodeqa.online/api/pro/file-cohesion", {
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: {
|
|
140
|
+
"Content-Type": "application/json",
|
|
141
|
+
Authorization: `Bearer ${proKey}`,
|
|
142
|
+
},
|
|
143
|
+
body: JSON.stringify({ file: filePath, content: truncated }),
|
|
144
|
+
});
|
|
145
|
+
if (!res.ok)
|
|
146
|
+
return null;
|
|
147
|
+
const data = (await res.json());
|
|
148
|
+
return {
|
|
149
|
+
concerns: data.concerns || [],
|
|
150
|
+
suggestion: data.suggestion || "",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function loadCache(cwd) {
|
|
158
|
+
try {
|
|
159
|
+
const cachePath = join(cwd, ".vibe-check", "file-cohesion-cache.json");
|
|
160
|
+
if (existsSync(cachePath)) {
|
|
161
|
+
const data = JSON.parse(readFileSync(cachePath, "utf-8"));
|
|
162
|
+
if (data.version === 1)
|
|
163
|
+
return data;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch { /* corrupt cache */ }
|
|
167
|
+
return { version: 1, files: {} };
|
|
168
|
+
}
|
|
169
|
+
function saveCache(cwd, cache) {
|
|
170
|
+
try {
|
|
171
|
+
const dir = join(cwd, ".vibe-check");
|
|
172
|
+
if (!existsSync(dir))
|
|
173
|
+
mkdirSync(dir, { recursive: true });
|
|
174
|
+
writeFileSync(join(dir, "file-cohesion-cache.json"), JSON.stringify(cache));
|
|
175
|
+
}
|
|
176
|
+
catch { /* write failed */ }
|
|
177
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Frontend health — detects HTML/framework antipatterns in component code.
|
|
2
|
+
*
|
|
3
|
+
* Checks:
|
|
4
|
+
* 1. Conflicting UI frameworks (MUI + Tailwind, Chakra + Tailwind, etc.)
|
|
5
|
+
* 2. Large/unoptimized images (no width/height, no next/image, large src)
|
|
6
|
+
* 3. Inconsistent icon systems (mixing lucide + heroicons + fontawesome)
|
|
7
|
+
* 4. Bundle-heavy imports (entire libraries instead of specific components)
|
|
8
|
+
* 5. Missing loading states (async data without suspense/skeleton)
|
|
9
|
+
* 6. Hardcoded strings (potential i18n issues)
|
|
10
|
+
* 7. Missing meta tags / head management
|
|
11
|
+
* 8. DOM nesting violations (div in p, button in a)
|
|
12
|
+
*/
|
|
13
|
+
import type { CheckResult } from "../types.js";
|
|
14
|
+
export declare function runFrontendHealth(cwd: string): CheckResult;
|