@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.
Files changed (47) hide show
  1. package/README.md +130 -165
  2. package/dist/check-meta.js +59 -6
  3. package/dist/cli.js +299 -762
  4. package/dist/commands/explain.d.ts +2 -0
  5. package/dist/commands/explain.js +33 -0
  6. package/dist/commands/fix.d.ts +6 -0
  7. package/dist/commands/fix.js +157 -0
  8. package/dist/commands/init.d.ts +2 -0
  9. package/dist/commands/init.js +96 -0
  10. package/dist/commands/shared.d.ts +4 -0
  11. package/dist/commands/shared.js +80 -0
  12. package/dist/core.d.ts +1 -0
  13. package/dist/core.js +12 -1
  14. package/dist/delta.d.ts +45 -0
  15. package/dist/delta.js +158 -0
  16. package/dist/detect.js +2 -2
  17. package/dist/pr-comment.d.ts +1 -1
  18. package/dist/pr-comment.js +23 -4
  19. package/dist/report/html.d.ts +1 -1
  20. package/dist/report/html.js +7 -2
  21. package/dist/report/pages.d.ts +2 -0
  22. package/dist/report/pages.js +167 -0
  23. package/dist/report/styles.d.ts +1 -1
  24. package/dist/report/styles.js +37 -0
  25. package/dist/runners/accessibility.js +4 -1
  26. package/dist/runners/best-practices.js +1 -1
  27. package/dist/runners/confusion.js +28 -17
  28. package/dist/runners/design-consistency.d.ts +12 -0
  29. package/dist/runners/design-consistency.js +125 -0
  30. package/dist/runners/error-handling.js +18 -2
  31. package/dist/runners/file-cohesion.d.ts +17 -0
  32. package/dist/runners/file-cohesion.js +177 -0
  33. package/dist/runners/frontend-health.d.ts +14 -0
  34. package/dist/runners/frontend-health.js +206 -0
  35. package/dist/runners/html-quality.d.ts +8 -0
  36. package/dist/runners/html-quality.js +203 -0
  37. package/dist/runners/lint.js +6 -1
  38. package/dist/runners/react.js +1 -0
  39. package/dist/runners/secrets.js +7 -2
  40. package/dist/runners/security.js +7 -1
  41. package/dist/runners/standards.d.ts +2 -2
  42. package/dist/runners/standards.js +45 -12
  43. package/dist/runners/structure.js +1 -1
  44. package/dist/runners/styling.d.ts +15 -0
  45. package/dist/runners/styling.js +280 -0
  46. package/dist/runners/testing.js +3 -1
  47. package/package.json +2 -2
@@ -0,0 +1,2 @@
1
+ /** vcqa explain — deep-dive explanation of a check. */
2
+ export declare function runExplain(checkName?: string): Promise<void>;
@@ -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,6 @@
1
+ /** vcqa fix — auto-fix + AI-powered fixing with delta report. */
2
+ export declare function runFix(cwd: string, opts?: {
3
+ ai?: boolean;
4
+ dryRun?: boolean;
5
+ checkFilter?: string;
6
+ }): Promise<void>;
@@ -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,2 @@
1
+ /** vcqa init — set up CI workflow + recommended configs. */
2
+ export declare function runInit(cwd: string): Promise<void>;
@@ -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,4 @@
1
+ /** Shared utilities for CLI commands. */
2
+ export declare function validateCwd(cwd: string): void;
3
+ /** Map common issue rules to actionable fix suggestions. */
4
+ export declare function suggestFix(check: string, rule: string, message: string): string | null;
@@ -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";
@@ -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
+ }