@vibecodeqa/cli 0.43.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/dist/cli.js +37 -7
- package/dist/commands/fix.d.ts +1 -1
- package/dist/commands/fix.js +78 -52
- package/dist/core.d.ts +1 -0
- package/dist/core.js +2 -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/best-practices.js +1 -1
- package/dist/runners/confusion.js +28 -17
- package/dist/runners/lint.js +6 -1
- package/dist/runners/standards.d.ts +2 -2
- package/dist/runners/standards.js +18 -5
- package/dist/runners/structure.js +1 -1
- package/dist/runners/styling.js +1 -1
- package/dist/runners/testing.js +3 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import { runInit } from "./commands/init.js";
|
|
|
8
8
|
import { validateCwd } from "./commands/shared.js";
|
|
9
9
|
import { loadConfig } from "./config.js";
|
|
10
10
|
import { scan } from "./core.js";
|
|
11
|
+
import { computeDelta } from "./delta.js";
|
|
11
12
|
import { detectStack, detectWorkspace } from "./detect.js";
|
|
12
13
|
import { postPRComment } from "./pr-comment.js";
|
|
13
14
|
import { generatePages } from "./report/html.js";
|
|
@@ -134,11 +135,31 @@ function printHeader(cwd, stack, workspace) {
|
|
|
134
135
|
}
|
|
135
136
|
console.log("");
|
|
136
137
|
}
|
|
137
|
-
function generateMarkdown(report, trend) {
|
|
138
|
+
function generateMarkdown(report, trend, prevReport) {
|
|
138
139
|
const { score, grade, checks } = report;
|
|
139
140
|
const gradeEmoji = grade === "A" ? "๐ข" : grade === "B" ? "๐ก" : grade === "C" ? "๐ " : "๐ด";
|
|
140
141
|
let md = `# ${gradeEmoji} VibeCode QA: ${grade} ${score}/100\n\n`;
|
|
141
|
-
|
|
142
|
+
// Delta section โ shows what changed with specific fixed/new issues
|
|
143
|
+
if (prevReport) {
|
|
144
|
+
const delta = computeDelta(prevReport, report);
|
|
145
|
+
const arrow = delta.scoreDelta > 0 ? "๐" : delta.scoreDelta < 0 ? "๐" : "โก๏ธ";
|
|
146
|
+
md += `${arrow} **${delta.scoreDelta > 0 ? "+" : ""}${delta.scoreDelta}** vs previous`;
|
|
147
|
+
if (delta.fixed.length > 0)
|
|
148
|
+
md += ` ยท **${delta.fixed.length} fixed**`;
|
|
149
|
+
if (delta.introduced.length > 0)
|
|
150
|
+
md += ` ยท ${delta.introduced.length} new`;
|
|
151
|
+
md += "\n\n";
|
|
152
|
+
// Per-check changes
|
|
153
|
+
const changed = delta.checks.filter((c) => c.delta !== 0).sort((a, b) => b.delta - a.delta);
|
|
154
|
+
if (changed.length > 0) {
|
|
155
|
+
for (const c of changed.slice(0, 8)) {
|
|
156
|
+
const a = c.delta > 0 ? "+" : "";
|
|
157
|
+
md += `- ${c.delta > 0 ? "โ
" : "โ ๏ธ"} ${c.name}: ${c.before} โ ${c.after} (${a}${c.delta})\n`;
|
|
158
|
+
}
|
|
159
|
+
md += "\n";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else if (trend) {
|
|
142
163
|
const arrow = trend.scoreDelta > 0 ? "๐" : trend.scoreDelta < 0 ? "๐" : "โก๏ธ";
|
|
143
164
|
md += `${arrow} **${trend.scoreDelta > 0 ? "+" : ""}${trend.scoreDelta}** vs previous`;
|
|
144
165
|
if (trend.fixedIssues > 0)
|
|
@@ -269,7 +290,7 @@ function getChangedFiles(cwd, base) {
|
|
|
269
290
|
}
|
|
270
291
|
}
|
|
271
292
|
// โโ Report output โโ
|
|
272
|
-
async function writeOutputs(report, outputDir, flags) {
|
|
293
|
+
async function writeOutputs(report, outputDir, flags, prevReport) {
|
|
273
294
|
mkdirSync(outputDir, { recursive: true });
|
|
274
295
|
// Always write JSON
|
|
275
296
|
writeFileSync(join(outputDir, "report.json"), JSON.stringify(report, null, 2));
|
|
@@ -289,7 +310,7 @@ async function writeOutputs(report, outputDir, flags) {
|
|
|
289
310
|
const reportDir = join(outputDir, "report");
|
|
290
311
|
mkdirSync(reportDir, { recursive: true });
|
|
291
312
|
const historyDir = join(outputDir, "history");
|
|
292
|
-
const pages = generatePages(report, historyDir);
|
|
313
|
+
const pages = generatePages(report, historyDir, prevReport);
|
|
293
314
|
for (const [filename, html] of pages) {
|
|
294
315
|
writeFileSync(join(reportDir, filename), html);
|
|
295
316
|
}
|
|
@@ -518,10 +539,19 @@ async function main() {
|
|
|
518
539
|
}
|
|
519
540
|
}
|
|
520
541
|
const trend = computeTrend(report, outputDir);
|
|
521
|
-
|
|
542
|
+
// Load previous report BEFORE writeOutputs overwrites it (for delta in markdown/PR)
|
|
543
|
+
let prevReport;
|
|
544
|
+
const prevReportPath = join(outputDir, "report.json");
|
|
545
|
+
if (existsSync(prevReportPath)) {
|
|
546
|
+
try {
|
|
547
|
+
prevReport = JSON.parse(readFileSync(prevReportPath, "utf-8"));
|
|
548
|
+
}
|
|
549
|
+
catch { /* corrupt */ }
|
|
550
|
+
}
|
|
551
|
+
await writeOutputs(report, outputDir, flags, prevReport);
|
|
522
552
|
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !quietMode && !ciMode && !watchMode;
|
|
523
553
|
if (flags.markdownMode) {
|
|
524
|
-
console.log(generateMarkdown(report, trend));
|
|
554
|
+
console.log(generateMarkdown(report, trend, prevReport));
|
|
525
555
|
}
|
|
526
556
|
else {
|
|
527
557
|
await printResults(report, trend, flags, outputDir, interactive);
|
|
@@ -531,7 +561,7 @@ async function main() {
|
|
|
531
561
|
if (flags.uploadMode)
|
|
532
562
|
await handleUpload(report, cwd, quietMode);
|
|
533
563
|
if (flags.prComment) {
|
|
534
|
-
const posted = await postPRComment(report, trend, cwd);
|
|
564
|
+
const posted = await postPRComment(report, trend, cwd, prevReport);
|
|
535
565
|
if (!quietMode) {
|
|
536
566
|
if (posted)
|
|
537
567
|
console.log(" \x1b[32m\u2713 PR comment posted\x1b[0m");
|
package/dist/commands/fix.d.ts
CHANGED
package/dist/commands/fix.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/** vcqa fix โ auto-fix + AI-powered fixing. */
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
/** vcqa fix โ auto-fix + AI-powered fixing with delta report. */
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { aiFixIssues, collectFixableIssues } from "../ai-fix.js";
|
|
5
5
|
import { scan } from "../core.js";
|
|
6
|
+
import { computeDelta, formatDeltaMarkdown } from "../delta.js";
|
|
6
7
|
import { detectStack } from "../detect.js";
|
|
7
|
-
import { gradeFromScore } from "../types.js";
|
|
8
8
|
import { suggestFix, validateCwd } from "./shared.js";
|
|
9
9
|
export async function runFix(cwd, opts = {}) {
|
|
10
10
|
console.log("");
|
|
@@ -12,9 +12,14 @@ export async function runFix(cwd, opts = {}) {
|
|
|
12
12
|
console.log(` \x1b[2m${cwd}\x1b[0m`);
|
|
13
13
|
console.log("");
|
|
14
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("");
|
|
15
20
|
const stack = detectStack(cwd);
|
|
16
21
|
let fixed = 0;
|
|
17
|
-
//
|
|
22
|
+
// 1. Auto-fix structure issues (missing files)
|
|
18
23
|
if (!existsSync(join(cwd, ".gitignore"))) {
|
|
19
24
|
writeFileSync(join(cwd, ".gitignore"), "node_modules\ndist\n.vibe-check\ncoverage\n.env\n.env.local\n");
|
|
20
25
|
console.log(" \x1b[32m\u2713 Created .gitignore\x1b[0m");
|
|
@@ -42,7 +47,7 @@ export async function runFix(cwd, opts = {}) {
|
|
|
42
47
|
}
|
|
43
48
|
catch { /* can't parse tsconfig */ }
|
|
44
49
|
}
|
|
45
|
-
//
|
|
50
|
+
// 2. Run linter auto-fix
|
|
46
51
|
if (stack.linter === "biome") {
|
|
47
52
|
console.log(" \x1b[1mFormatting with Biome...\x1b[0m");
|
|
48
53
|
const { execSync } = await import("node:child_process");
|
|
@@ -65,67 +70,88 @@ export async function runFix(cwd, opts = {}) {
|
|
|
65
70
|
console.log(" \x1b[33mESLint had issues (some may be unfixable)\x1b[0m");
|
|
66
71
|
}
|
|
67
72
|
}
|
|
68
|
-
//
|
|
69
|
-
console.log("");
|
|
70
|
-
console.log(" \x1b[1mScanning for remaining issues...\x1b[0m");
|
|
71
|
-
console.log("");
|
|
72
|
-
const report = await scan(cwd, { skipTests: true });
|
|
73
|
-
const { checks } = report;
|
|
74
|
-
const score = report.score;
|
|
75
|
-
// AI-powered fix mode
|
|
73
|
+
// 3. AI-powered fix mode
|
|
76
74
|
if (opts.ai) {
|
|
77
|
-
|
|
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);
|
|
78
79
|
if (aiIssues.length === 0) {
|
|
79
80
|
console.log(" \x1b[2mNo fixable issues found.\x1b[0m");
|
|
80
81
|
}
|
|
81
82
|
else {
|
|
82
83
|
console.log(` \x1b[1mAI fixing ${Math.min(aiIssues.length, 10)} issues${opts.dryRun ? " (dry run)" : ""}...\x1b[0m`);
|
|
83
84
|
console.log("");
|
|
84
|
-
|
|
85
|
-
const applied = results.filter((r) => r.applied).length;
|
|
86
|
-
if (applied > 0) {
|
|
87
|
-
console.log("");
|
|
88
|
-
console.log(" \x1b[1mRe-scanning...\x1b[0m");
|
|
89
|
-
const reReport = await scan(cwd, { skipTests: true });
|
|
90
|
-
const delta = reReport.score - score;
|
|
91
|
-
console.log(` Score: \x1b[${reReport.score >= 75 ? "32" : reReport.score >= 60 ? "33" : "31"}m${reReport.grade} ${reReport.score}/100\x1b[0m${delta > 0 ? ` \x1b[32m(+${delta})\x1b[0m` : ""}`);
|
|
92
|
-
console.log(` \x1b[32m${applied} AI fix(es) applied.\x1b[0m Re-run \x1b[1mnpx @vibecodeqa/cli\x1b[0m for full report.`);
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
const grade = gradeFromScore(score);
|
|
96
|
-
console.log(`\n Score: \x1b[${score >= 75 ? "32" : score >= 60 ? "33" : "31"}m${grade} ${score}/100\x1b[0m`);
|
|
97
|
-
if (opts.dryRun)
|
|
98
|
-
console.log(" \x1b[2mDry run โ no files modified. Remove --dry-run to apply.\x1b[0m");
|
|
99
|
-
}
|
|
85
|
+
await aiFixIssues(cwd, aiIssues, { dryRun: opts.dryRun || false });
|
|
100
86
|
}
|
|
101
|
-
console.log("");
|
|
102
|
-
return;
|
|
103
87
|
}
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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`);
|
|
113
106
|
}
|
|
114
107
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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) {
|
|
118
114
|
console.log("");
|
|
119
|
-
for (const
|
|
120
|
-
|
|
121
|
-
console.log(` ${
|
|
122
|
-
|
|
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) {
|
|
123
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`);
|
|
124
154
|
}
|
|
125
155
|
}
|
|
126
|
-
const grade = gradeFromScore(score);
|
|
127
|
-
console.log(` Score after fix: \x1b[${score >= 75 ? "32" : score >= 60 ? "33" : "31"}m${grade} ${score}/100\x1b[0m`);
|
|
128
|
-
if (fixed > 0)
|
|
129
|
-
console.log(` \x1b[32m${fixed} auto-fix(es) applied.\x1b[0m Re-run \x1b[1mnpx @vibecodeqa/cli\x1b[0m for full report.`);
|
|
130
156
|
console.log("");
|
|
131
157
|
}
|
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
|
@@ -66,7 +66,7 @@ export async function scan(cwd, options = {}) {
|
|
|
66
66
|
{ name: "lint", fn: () => runLint(resolvedCwd, stack, workspace) },
|
|
67
67
|
{ name: "types", fn: () => runTypeCheck(resolvedCwd, isDart, workspace) },
|
|
68
68
|
{ name: "type-safety", fn: () => runTypeSafety(resolvedCwd, isDart) },
|
|
69
|
-
{ name: "standards", fn: () => runStandards(resolvedCwd, stack) },
|
|
69
|
+
{ name: "standards", fn: () => runStandards(resolvedCwd, stack, workspace) },
|
|
70
70
|
{ name: "complexity", fn: () => runComplexity(resolvedCwd) },
|
|
71
71
|
{ name: "duplication", fn: () => runDuplication(resolvedCwd) },
|
|
72
72
|
{ name: "error-handling", fn: () => runErrorHandling(resolvedCwd, stack) },
|
|
@@ -176,6 +176,7 @@ export async function scan(cwd, options = {}) {
|
|
|
176
176
|
// โโ Re-exports โโ
|
|
177
177
|
export { CHECK_META, getCheckMeta };
|
|
178
178
|
export { computeScore } from "./score.js";
|
|
179
|
+
export { computeDelta, formatDeltaMarkdown } from "./delta.js";
|
|
179
180
|
export { loadConfig } from "./config.js";
|
|
180
181
|
export { detectStack, detectWorkspace } from "./detect.js";
|
|
181
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
|
+
}
|
package/dist/detect.js
CHANGED
|
@@ -89,7 +89,7 @@ export function detectStack(cwd, workspace) {
|
|
|
89
89
|
const linter = allDeps["@biomejs/biome"] ? "biome" : allDeps.eslint ? "eslint" : "none";
|
|
90
90
|
const packageManager = has("pnpm-lock.yaml")
|
|
91
91
|
? "pnpm"
|
|
92
|
-
: has("bun.lockb")
|
|
92
|
+
: has("bun.lockb") || has("bun.lock")
|
|
93
93
|
? "bun"
|
|
94
94
|
: has("yarn.lock")
|
|
95
95
|
? "yarn"
|
|
@@ -167,7 +167,7 @@ export function detectWorkspace(cwd) {
|
|
|
167
167
|
if (parsed.workspaces) {
|
|
168
168
|
const ws = Array.isArray(parsed.workspaces) ? parsed.workspaces : parsed.workspaces.packages || [];
|
|
169
169
|
if (ws.length > 0) {
|
|
170
|
-
tool = has("bun.lockb") ? "bun" : has("yarn.lock") ? "yarn" : "npm";
|
|
170
|
+
tool = has("bun.lockb") || has("bun.lock") ? "bun" : has("yarn.lock") ? "yarn" : "npm";
|
|
171
171
|
globs = ws;
|
|
172
172
|
}
|
|
173
173
|
}
|
package/dist/pr-comment.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/** Post scan results as a GitHub PR comment. Upserts to avoid duplicates. */
|
|
2
2
|
import type { TrendDelta } from "./trend.js";
|
|
3
3
|
import type { VibeReport } from "./types.js";
|
|
4
|
-
export declare function postPRComment(report: VibeReport, trend: TrendDelta | null, cwd: string): Promise<boolean>;
|
|
4
|
+
export declare function postPRComment(report: VibeReport, trend: TrendDelta | null, cwd: string, prevReport?: VibeReport): Promise<boolean>;
|
package/dist/pr-comment.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/** Post scan results as a GitHub PR comment. Upserts to avoid duplicates. */
|
|
2
2
|
import { execSync } from "node:child_process";
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { computeDelta } from "./delta.js";
|
|
4
5
|
const MARKER = "<!-- vcqa-report -->";
|
|
5
|
-
export async function postPRComment(report, trend, cwd) {
|
|
6
|
+
export async function postPRComment(report, trend, cwd, prevReport) {
|
|
6
7
|
const pr = detectPR(cwd);
|
|
7
8
|
if (!pr)
|
|
8
9
|
return false;
|
|
9
10
|
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
10
11
|
if (!token)
|
|
11
12
|
return false;
|
|
12
|
-
const body = buildCommentBody(report, trend);
|
|
13
|
+
const body = buildCommentBody(report, trend, prevReport);
|
|
13
14
|
// Try to find existing vcqa comment to update
|
|
14
15
|
const existingId = await findExistingComment(pr, token);
|
|
15
16
|
if (existingId) {
|
|
@@ -60,12 +61,30 @@ function detectPR(cwd) {
|
|
|
60
61
|
}
|
|
61
62
|
return null;
|
|
62
63
|
}
|
|
63
|
-
function buildCommentBody(report, trend) {
|
|
64
|
+
function buildCommentBody(report, trend, prevReport) {
|
|
64
65
|
const grade = report.grade;
|
|
65
66
|
const score = report.score;
|
|
66
67
|
const gradeEmoji = grade === "A" ? "๐ข" : grade === "B" ? "๐ก" : grade === "C" ? "๐ " : "๐ด";
|
|
67
68
|
let body = `${MARKER}\n## ${gradeEmoji} VibeCode QA: **${grade}** ${score}/100\n\n`;
|
|
68
|
-
if (
|
|
69
|
+
if (prevReport) {
|
|
70
|
+
const delta = computeDelta(prevReport, report);
|
|
71
|
+
const arrow = delta.scoreDelta > 0 ? "๐" : delta.scoreDelta < 0 ? "๐" : "โก๏ธ";
|
|
72
|
+
body += `${arrow} **${delta.scoreDelta > 0 ? "+" : ""}${delta.scoreDelta}** vs previous`;
|
|
73
|
+
if (delta.fixed.length > 0)
|
|
74
|
+
body += ` ยท **${delta.fixed.length} fixed**`;
|
|
75
|
+
if (delta.introduced.length > 0)
|
|
76
|
+
body += ` ยท ${delta.introduced.length} new`;
|
|
77
|
+
body += "\n\n";
|
|
78
|
+
const changed = delta.checks.filter((c) => c.delta !== 0).sort((a, b) => b.delta - a.delta);
|
|
79
|
+
if (changed.length > 0) {
|
|
80
|
+
for (const c of changed.slice(0, 6)) {
|
|
81
|
+
const a = c.delta > 0 ? "+" : "";
|
|
82
|
+
body += `- ${c.delta > 0 ? "โ
" : "โ ๏ธ"} ${c.name}: ${c.before} โ ${c.after} (${a}${c.delta})\n`;
|
|
83
|
+
}
|
|
84
|
+
body += "\n";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else if (trend) {
|
|
69
88
|
const arrow = trend.scoreDelta > 0 ? "๐" : trend.scoreDelta < 0 ? "๐" : "โก๏ธ";
|
|
70
89
|
body += `${arrow} **${trend.scoreDelta > 0 ? "+" : ""}${trend.scoreDelta}** vs previous`;
|
|
71
90
|
if (trend.fixedIssues > 0)
|
package/dist/report/html.d.ts
CHANGED
|
@@ -17,5 +17,5 @@ export declare const GROUPS: {
|
|
|
17
17
|
file: string;
|
|
18
18
|
checks: string[];
|
|
19
19
|
}[];
|
|
20
|
-
export declare function generatePages(report: VibeReport, historyDir?: string): Map<string, string>;
|
|
20
|
+
export declare function generatePages(report: VibeReport, historyDir?: string, prevReport?: VibeReport): Map<string, string>;
|
|
21
21
|
export declare function generateHTML(report: VibeReport, historyDir?: string): string;
|
package/dist/report/html.js
CHANGED
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
* Mobile: Hamburger toggles both top nav dropdown and sidebar panel.
|
|
12
12
|
*/
|
|
13
13
|
import { getCheckMeta } from "../check-meta.js";
|
|
14
|
+
import { computeDelta } from "../delta.js";
|
|
14
15
|
import { det, e, fileLink, gc } from "./components.js";
|
|
15
16
|
import { FAVICON_SVG } from "./favicon.js";
|
|
16
|
-
import { categoryPage, featureMapPage, filesPage, issuesPage, overviewPage, trendsPage } from "./pages.js";
|
|
17
|
+
import { actionsPage, categoryPage, featureMapPage, filesPage, issuesPage, overviewPage, trendsPage } from "./pages.js";
|
|
17
18
|
import { CSS } from "./styles.js";
|
|
18
19
|
export const GROUPS = [
|
|
19
20
|
{ id: "foundations", label: "Foundations", file: "foundations.html", checks: ["structure", "lint", "types", "type-safety", "standards"] },
|
|
@@ -29,7 +30,7 @@ export const GROUPS = [
|
|
|
29
30
|
{ id: "llm", label: "AI Readiness", file: "ai-readiness.html", checks: ["confusion", "context"] },
|
|
30
31
|
{ id: "ai", label: "AI Analysis", file: "ai-analysis.html", checks: ["doc-coherence", "code-coherence", "comment-staleness", "dead-patterns", "test-audit"] },
|
|
31
32
|
];
|
|
32
|
-
export function generatePages(report, historyDir) {
|
|
33
|
+
export function generatePages(report, historyDir, prevReport) {
|
|
33
34
|
const pages = new Map();
|
|
34
35
|
const allChecks = report.checks;
|
|
35
36
|
const checkMap = new Map(allChecks.map((c) => [c.name, c]));
|
|
@@ -107,6 +108,9 @@ export function generatePages(report, historyDir) {
|
|
|
107
108
|
// Feature Map (Pro page โ reads dead-patterns check details)
|
|
108
109
|
const deadPatternsCheck = checkMap.get("dead-patterns");
|
|
109
110
|
pages.set("feature-map.html", w("feature-map", featureMapPage(deadPatternsCheck, fl)));
|
|
111
|
+
// Compute delta from previous scan
|
|
112
|
+
const scanDelta = prevReport ? computeDelta(prevReport, report) : undefined;
|
|
113
|
+
pages.set("actions.html", w("actions", actionsPage(allChecks, fl, report.meta.stack.linter, scanDelta)));
|
|
110
114
|
pages.set("issues.html", w("issues", issuesPage(allChecks, totalIssues, fl)));
|
|
111
115
|
pages.set("files.html", w("files", filesPage(topFiles, fileIssues, fl)));
|
|
112
116
|
pages.set("trends.html", w("trends", trendsPage(historyDir)));
|
|
@@ -131,6 +135,7 @@ function wrap(proj, currentId, report, totalIssues, sidebar, content) {
|
|
|
131
135
|
{ id: "checks", label: "Checks", file: GROUPS[0].file, active: isCheckPage },
|
|
132
136
|
{ id: "feature-map", label: "Feature Map", file: "feature-map.html" },
|
|
133
137
|
{ id: "trends", label: "Trends", file: "trends.html" },
|
|
138
|
+
{ id: "actions", label: "Actions", file: "actions.html" },
|
|
134
139
|
{ id: "issues", label: `Issues (${totalIssues})`, file: "issues.html" },
|
|
135
140
|
{ id: "files", label: "Files", file: "files.html" },
|
|
136
141
|
];
|
package/dist/report/pages.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Page renderers for the HTML report. */
|
|
2
|
+
import type { ScanDelta } from "../delta.js";
|
|
2
3
|
import type { CheckResult, VibeReport } from "../types.js";
|
|
3
4
|
export interface CatScore {
|
|
4
5
|
id: string;
|
|
@@ -25,4 +26,5 @@ export declare function filesPage(topFiles: FileEntry[], fileIssues: Map<string,
|
|
|
25
26
|
}>, fl: FL): string;
|
|
26
27
|
export declare function trendsPage(historyDir: string | undefined): string;
|
|
27
28
|
export declare function featureMapPage(deadPatternsCheck: CheckResult | undefined, fl: FL): string;
|
|
29
|
+
export declare function actionsPage(allChecks: CheckResult[], fl: FL, linter: string, delta?: ScanDelta): string;
|
|
28
30
|
export {};
|
package/dist/report/pages.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { getCheckMeta } from "../check-meta.js";
|
|
5
|
+
import { suggestFix } from "../commands/shared.js";
|
|
5
6
|
import { buildCoverageMapInput, generateCoverageMap } from "../diagrams/coverage.js";
|
|
6
7
|
import { loadHistory } from "../history.js";
|
|
7
8
|
import { generateArchSVG, generateDSM, generateLayerDiagram, generatePackageDiagram, generateSequenceDiagram, } from "../runners/architecture.js";
|
|
@@ -479,3 +480,169 @@ export function featureMapPage(deadPatternsCheck, fl) {
|
|
|
479
480
|
|
|
480
481
|
<div class="fm-grid">${cards}</div>`;
|
|
481
482
|
}
|
|
483
|
+
export function actionsPage(allChecks, fl, linter, delta) {
|
|
484
|
+
// Classify issues into auto-fixable, AI-fixable, manual
|
|
485
|
+
const autoFixes = [];
|
|
486
|
+
const aiFixes = [];
|
|
487
|
+
const manualGroups = [];
|
|
488
|
+
// Auto-fixable: lint issues (biome/eslint can auto-fix), structure issues
|
|
489
|
+
const AUTO_RULES = new Set(["missing-file", "missing-lockfile", "ts-strict", "env-not-ignored", "no-ci"]);
|
|
490
|
+
// Group AI-fixable by their fix suggestion
|
|
491
|
+
const fixGroups = new Map();
|
|
492
|
+
for (const c of allChecks) {
|
|
493
|
+
if (det(c).skipped || det(c).comingSoon)
|
|
494
|
+
continue;
|
|
495
|
+
const meta = getCheckMeta(c.name);
|
|
496
|
+
const manualIssues = [];
|
|
497
|
+
for (const iss of c.issues) {
|
|
498
|
+
const file = typeof iss.file === "string" ? iss.file : undefined;
|
|
499
|
+
// Auto-fixable: lint issues or known auto-fix rules
|
|
500
|
+
if (c.name === "lint" || AUTO_RULES.has(iss.rule || "")) {
|
|
501
|
+
autoFixes.push({ check: c.name, file, line: iss.line, message: iss.message, rule: iss.rule });
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
// AI-fixable: has a suggestFix mapping
|
|
505
|
+
const fix = suggestFix(c.name, iss.rule || "", iss.message);
|
|
506
|
+
if (fix) {
|
|
507
|
+
const key = `${c.name}::${fix}`;
|
|
508
|
+
const group = fixGroups.get(key) || { check: c.name, meta, fix, items: [] };
|
|
509
|
+
group.items.push({ file, line: iss.line, message: iss.message, rule: iss.rule });
|
|
510
|
+
fixGroups.set(key, group);
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
// Manual
|
|
514
|
+
manualIssues.push({ file, line: iss.line, message: iss.message });
|
|
515
|
+
}
|
|
516
|
+
if (manualIssues.length > 0 && c.score < 90) {
|
|
517
|
+
manualGroups.push({ meta, score: c.score, issues: manualIssues });
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
for (const g of fixGroups.values())
|
|
521
|
+
aiFixes.push(g);
|
|
522
|
+
aiFixes.sort((a, b) => b.items.length - a.items.length);
|
|
523
|
+
manualGroups.sort((a, b) => a.score - b.score);
|
|
524
|
+
const totalAuto = autoFixes.length;
|
|
525
|
+
const totalAI = aiFixes.reduce((s, g) => s + g.items.length, 0);
|
|
526
|
+
const totalManual = manualGroups.reduce((s, g) => s + g.issues.length, 0);
|
|
527
|
+
// Build auto-fix section
|
|
528
|
+
let autoSection = "";
|
|
529
|
+
if (totalAuto > 0) {
|
|
530
|
+
const linterName = linter === "biome" ? "Biome" : linter === "eslint" ? "ESLint" : "linter";
|
|
531
|
+
autoSection = `
|
|
532
|
+
<div class="act-section">
|
|
533
|
+
<h3><span class="act-icon" style="background:#22c55e20;color:var(--pass)">⚡</span> Quick Fixes <span class="act-count">${totalAuto}</span></h3>
|
|
534
|
+
<p class="act-desc">Auto-fixable with one command. Run <code>npx @vibecodeqa/cli fix</code></p>
|
|
535
|
+
<div class="act-cmd"><code>npx @vibecodeqa/cli fix</code></div>
|
|
536
|
+
<details class="act-details"><summary>${totalAuto} issues (${linterName} formatting, missing files, config)</summary>
|
|
537
|
+
<table class="act-table"><tbody>${autoFixes
|
|
538
|
+
.slice(0, 50)
|
|
539
|
+
.map((i) => `<tr><td class="act-check">${e(i.check)}</td><td>${i.file ? fl(i.file.split(":")[0], i.line) : ""}</td><td>${e(i.message)}</td></tr>`)
|
|
540
|
+
.join("")}</tbody></table>
|
|
541
|
+
${totalAuto > 50 ? `<p class="muted">+${totalAuto - 50} more</p>` : ""}
|
|
542
|
+
</details>
|
|
543
|
+
</div>`;
|
|
544
|
+
}
|
|
545
|
+
// Build AI-fix section
|
|
546
|
+
let aiSection = "";
|
|
547
|
+
if (totalAI > 0) {
|
|
548
|
+
const groupRows = aiFixes
|
|
549
|
+
.slice(0, 20)
|
|
550
|
+
.map((g) => {
|
|
551
|
+
const topItems = g.items
|
|
552
|
+
.slice(0, 3)
|
|
553
|
+
.map((i) => `<div class="act-item">${i.file ? fl(i.file.split(":")[0], i.line) : ""} <span class="muted">${e(i.message)}</span></div>`)
|
|
554
|
+
.join("");
|
|
555
|
+
const more = g.items.length > 3 ? `<div class="act-item muted">+${g.items.length - 3} more</div>` : "";
|
|
556
|
+
return `<div class="act-card"><div class="act-card-head"><span class="act-check">${e(g.meta.label)}</span><span class="act-count">${g.items.length}</span></div><div class="act-fix">${e(g.fix)}</div>${topItems}${more}</div>`;
|
|
557
|
+
})
|
|
558
|
+
.join("");
|
|
559
|
+
aiSection = `
|
|
560
|
+
<div class="act-section">
|
|
561
|
+
<h3><span class="act-icon" style="background:#6366f120;color:var(--info)">✨</span> AI Fixes <span class="act-count">${totalAI}</span></h3>
|
|
562
|
+
<p class="act-desc">Fixable with AI assistance. Run <code>npx @vibecodeqa/cli fix --ai</code></p>
|
|
563
|
+
<div class="act-cmd"><code>npx @vibecodeqa/cli fix --ai</code></div>
|
|
564
|
+
<div class="act-grid">${groupRows}</div>
|
|
565
|
+
</div>`;
|
|
566
|
+
}
|
|
567
|
+
// Build manual section
|
|
568
|
+
let manualSection = "";
|
|
569
|
+
if (manualGroups.length > 0) {
|
|
570
|
+
const groupRows = manualGroups
|
|
571
|
+
.slice(0, 15)
|
|
572
|
+
.map((g) => {
|
|
573
|
+
const clr = gc(g.score >= 90 ? "A" : g.score >= 75 ? "B" : g.score >= 60 ? "C" : g.score >= 40 ? "D" : "F");
|
|
574
|
+
const topIssues = g.issues
|
|
575
|
+
.slice(0, 3)
|
|
576
|
+
.map((i) => `<div class="act-item">${i.file ? fl(i.file.split(":")[0], i.line) : ""} <span class="muted">${e(i.message)}</span></div>`)
|
|
577
|
+
.join("");
|
|
578
|
+
const more = g.issues.length > 3 ? `<div class="act-item muted">+${g.issues.length - 3} more</div>` : "";
|
|
579
|
+
return `<div class="act-card"><div class="act-card-head"><span class="act-check">${e(g.meta.label)}</span><span style="color:${clr}">${g.score}/100</span></div><div class="act-rec">${e(g.meta.recommendation)}</div>${topIssues}${more}</div>`;
|
|
580
|
+
})
|
|
581
|
+
.join("");
|
|
582
|
+
manualSection = `
|
|
583
|
+
<div class="act-section">
|
|
584
|
+
<h3><span class="act-icon" style="background:#eab30820;color:var(--warn)">🛠</span> Manual Actions <span class="act-count">${totalManual}</span></h3>
|
|
585
|
+
<p class="act-desc">Require code changes โ grouped by check with specific recommendations.</p>
|
|
586
|
+
<div class="act-grid">${groupRows}</div>
|
|
587
|
+
</div>`;
|
|
588
|
+
}
|
|
589
|
+
const noActions = totalAuto === 0 && totalAI === 0 && totalManual === 0;
|
|
590
|
+
// Delta section โ shows what changed since last scan
|
|
591
|
+
let deltaSection = "";
|
|
592
|
+
if (delta) {
|
|
593
|
+
const dColor = delta.scoreDelta > 0 ? "var(--pass)" : delta.scoreDelta < 0 ? "var(--fail)" : "var(--muted)";
|
|
594
|
+
const dArrow = delta.scoreDelta > 0 ? "▲" : delta.scoreDelta < 0 ? "▼" : "▬";
|
|
595
|
+
// Per-check changes
|
|
596
|
+
const changed = delta.checks.filter((c) => c.delta !== 0).sort((a, b) => b.delta - a.delta);
|
|
597
|
+
const checkDeltas = changed
|
|
598
|
+
.slice(0, 10)
|
|
599
|
+
.map((c) => {
|
|
600
|
+
const cc = c.delta > 0 ? "var(--pass)" : "var(--fail)";
|
|
601
|
+
return `<span class="delta-chip" style="color:${cc}">${c.name} ${c.delta > 0 ? "+" : ""}${c.delta}</span>`;
|
|
602
|
+
})
|
|
603
|
+
.join("");
|
|
604
|
+
// Fixed issues list
|
|
605
|
+
let fixedList = "";
|
|
606
|
+
if (delta.fixed.length > 0) {
|
|
607
|
+
const byCheck = new Map();
|
|
608
|
+
for (const f of delta.fixed)
|
|
609
|
+
byCheck.set(f.check, (byCheck.get(f.check) || 0) + 1);
|
|
610
|
+
fixedList = `<div class="delta-fixed"><strong style="color:var(--pass)">Fixed (${delta.fixed.length}):</strong> ${[...byCheck.entries()].sort((a, b) => b[1] - a[1]).map(([c, n]) => `${c} (${n})`).join(", ")}</div>`;
|
|
611
|
+
}
|
|
612
|
+
let newList = "";
|
|
613
|
+
if (delta.introduced.length > 0) {
|
|
614
|
+
const byCheck = new Map();
|
|
615
|
+
for (const f of delta.introduced)
|
|
616
|
+
byCheck.set(f.check, (byCheck.get(f.check) || 0) + 1);
|
|
617
|
+
newList = `<div class="delta-new"><strong style="color:var(--fail)">New (${delta.introduced.length}):</strong> ${[...byCheck.entries()].sort((a, b) => b[1] - a[1]).map(([c, n]) => `${c} (${n})`).join(", ")}</div>`;
|
|
618
|
+
}
|
|
619
|
+
deltaSection = `
|
|
620
|
+
<div class="delta-banner">
|
|
621
|
+
<div class="delta-head">
|
|
622
|
+
<span class="delta-title">What Changed</span>
|
|
623
|
+
<span class="delta-score">${delta.before.grade} ${delta.before.score} → ${delta.after.grade} ${delta.after.score}</span>
|
|
624
|
+
<span class="delta-arrow" style="color:${dColor}">${dArrow} ${delta.scoreDelta > 0 ? "+" : ""}${delta.scoreDelta}</span>
|
|
625
|
+
</div>
|
|
626
|
+
<div class="delta-stats">
|
|
627
|
+
<span style="color:var(--pass)">${delta.fixed.length} fixed</span>
|
|
628
|
+
<span style="color:var(--fail)">${delta.introduced.length} new</span>
|
|
629
|
+
<span style="color:var(--muted)">${delta.after.issueCount} remaining</span>
|
|
630
|
+
</div>
|
|
631
|
+
${checkDeltas ? `<div class="delta-checks">${checkDeltas}</div>` : ""}
|
|
632
|
+
${fixedList}${newList}
|
|
633
|
+
</div>`;
|
|
634
|
+
}
|
|
635
|
+
return `
|
|
636
|
+
<h2>Recommended Actions</h2>
|
|
637
|
+
<p class="muted" style="margin-bottom:1.5rem">${noActions ? "No actions needed โ all checks passed." : `${totalAuto + totalAI + totalManual} issues across ${totalAuto > 0 ? "3" : totalAI > 0 ? "2" : "1"} fix categories.`}</p>
|
|
638
|
+
|
|
639
|
+
${deltaSection}
|
|
640
|
+
|
|
641
|
+
<div class="act-summary">
|
|
642
|
+
<div class="act-stat"><span class="act-stat-n" style="color:var(--pass)">${totalAuto}</span><span class="act-stat-l">Auto-fixable</span></div>
|
|
643
|
+
<div class="act-stat"><span class="act-stat-n" style="color:var(--info)">${totalAI}</span><span class="act-stat-l">AI-fixable</span></div>
|
|
644
|
+
<div class="act-stat"><span class="act-stat-n" style="color:var(--warn)">${totalManual}</span><span class="act-stat-l">Manual</span></div>
|
|
645
|
+
</div>
|
|
646
|
+
|
|
647
|
+
${autoSection}${aiSection}${manualSection}`;
|
|
648
|
+
}
|
package/dist/report/styles.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** All CSS for the HTML report, extracted for maintainability. */
|
|
2
|
-
export declare const CSS = "\n:root{--bg:#09090b;--card:#111115;--border:#1e1e24;--text:#e5e5e5;--muted:#6b7280;--pass:#22c55e;--fail:#ef4444;--warn:#eab308;--info:#6366f1;--accent:#818cf8;--side-w:200px;--top-h:42px;--nav-bg:#0c0c0fdd;--side-bg:#0c0c0f;--hover:#14141a;--dim:#555;--card-alt:#0d0d12}\n[data-theme=\"light\"]{--bg:#f5f5f7;--card:#ffffff;--border:#e2e4e9;--text:#1a1a2e;--muted:#64748b;--pass:#16a34a;--fail:#dc2626;--warn:#ca8a04;--info:#4f46e5;--accent:#4f46e5;--nav-bg:#ffffffee;--side-bg:#fafafa;--hover:#eef0f5;--dim:#94a3b8;--card-alt:#f0f0f5}\nhtml{font-size:17px}\n*{margin:0;padding:0;box-sizing:border-box}\nbody{font-family:\"Inter\",system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.5}\ncode{font-family:\"SF Mono\",Menlo,monospace;font-size:0.85em}\n\n/* \u2500\u2500 Top nav \u2500\u2500 */\n.top{position:sticky;top:0;z-index:30;background:var(--nav-bg);backdrop-filter:blur(12px);border-bottom:1px solid var(--border);padding:0 1.5rem;display:flex;align-items:center;height:var(--top-h)}\n.logo{font-weight:800;font-size:1rem;margin-right:0.5rem;flex-shrink:0;text-decoration:none;color:var(--text)}\n.logo span{color:var(--accent)}\n.nav-project{font-size:0.72rem;color:var(--muted);font-weight:600;margin-right:1rem;padding:0.2rem 0.5rem;background:var(--card);border:1px solid var(--border);border-radius:4px;flex-shrink:0}\n.nav-scroll{display:flex;align-items:center;gap:0;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;flex:1}\n.nav-scroll::-webkit-scrollbar{display:none}\n.tn{padding:0 0.7rem;font-size:0.78rem;color:var(--muted);text-decoration:none;border-bottom:2px solid transparent;transition:all 0.15s;white-space:nowrap;line-height:var(--top-h)}\n.tn:hover{color:var(--text)}\n.tn.active{color:var(--text);border-bottom-color:var(--accent)}\n.hamburger{display:none;background:none;border:none;color:var(--muted);font-size:1.3rem;cursor:pointer;padding:0 0.4rem;line-height:var(--top-h)}\n\n/* \u2500\u2500 Sidebar \u2500\u2500 */\n.side{position:fixed;top:var(--top-h);left:0;bottom:0;width:var(--side-w);background:var(--side-bg);border-right:1px solid var(--border);overflow-y:auto;padding:0.6rem 0;font-size:0.7rem;z-index:20}\n.side-section{padding:0.3rem 0;border-bottom:1px solid var(--border)}\n.side-section:last-child{border-bottom:none}\n.side-label{padding:0.2rem 0.8rem;font-size:0.6rem;text-transform:uppercase;letter-spacing:0.05em;color:var(--dim);font-weight:600}\n.side-score{font-size:1.4rem;font-weight:900;padding:0.2rem 0.8rem}\n.side-cat{display:block;padding:0.3rem 0.8rem;color:var(--muted);font-weight:600;cursor:pointer;text-decoration:none;font-size:0.72rem}\n.side-cat:hover{background:var(--hover);color:var(--text)}\n.side-cat-active{color:var(--text);font-weight:700;border-left:2px solid var(--accent);padding-left:calc(0.8rem - 2px)}\n.side-cat-title{padding:0.3rem 0.8rem;font-size:0.65rem;text-transform:uppercase;letter-spacing:0.04em;color:var(--accent);font-weight:700}\n.side-check{display:block;padding:0.15rem 0.8rem 0.15rem 0.8rem;color:var(--muted);cursor:pointer;text-decoration:none;font-size:0.65rem}\n.side-check:hover{color:var(--text);background:#14141a}\n.side-check span{display:inline-block;min-width:2.5rem;font-weight:700;font-size:0.6rem}\n.side-stat{padding:0.15rem 0.8rem;font-size:0.7rem;color:var(--muted)}\n.side-stat span{font-weight:800;font-size:0.8rem}\n.side-views{padding-top:0.3rem}\n.side-views .side-check{padding-left:0.8rem}\n\n/* \u2500\u2500 Content \u2500\u2500 */\n.content{margin-left:var(--side-w);padding:1.5rem 2rem;max-width:960px}\n\n/* \u2500\u2500 Overview \u2500\u2500 */\n.dash{display:flex;gap:2rem;margin-bottom:2rem;align-items:center;flex-wrap:wrap}\n.hero{display:flex;align-items:center;gap:1rem}\n.hero svg{width:100px;height:100px}\n.hc{display:flex;flex-direction:column}\n.hg{font-size:2.5rem;font-weight:900;line-height:1}\n.hs{font-size:1rem;font-weight:600}\n.hd{font-size:0.68rem;color:var(--muted)}\n.radar{flex:1;display:flex;justify-content:center}\n.radar svg{max-width:240px;width:100%}\n.cats{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:0.6rem;margin-bottom:2rem}\n.cc{background:var(--card);border:1px solid var(--border);border-radius:0.6rem;padding:0.8rem;transition:border-color 0.15s;text-decoration:none;color:var(--text);display:block}\n.cc:hover{border-color:var(--accent)}\n.cc-s{font-size:1.8rem;font-weight:900}\n.cc-l{font-size:0.75rem;color:var(--muted)}\n.cc-m{margin-top:0.3rem;display:flex;gap:0.25rem}\n.mc{font-size:0.65rem;font-weight:800}\nh3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:0.5rem}\n\n/* \u2500\u2500 Overview sections \u2500\u2500 */\n.ov-section{margin-bottom:1.5rem}\n.ov-issue{font-size:0.68rem;font-family:\"SF Mono\",monospace;padding:0.2rem 0;display:flex;gap:0.4rem;align-items:baseline;border-bottom:1px solid var(--border)}\n.ov-issue .is{flex-shrink:0}\n.ov-issue.error .is{color:var(--fail)}\n.ov-issue.warning .is{color:var(--warn)}\n.ov-check{color:var(--muted);width:70px;flex-shrink:0;font-size:0.62rem}\n.ov-loc{color:var(--accent);flex-shrink:0;font-size:0.62rem;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.ov-msg{flex:1;word-break:break-word}\n.ov-link{display:block;margin-top:0.5rem;font-size:0.72rem;color:var(--accent);text-decoration:none}\n.ov-link:hover{text-decoration:underline}\n\n/* \u2500\u2500 Timeline \u2500\u2500 */\n.timeline{margin:0.5rem 0;overflow-x:auto}\n.timeline svg{max-width:100%}\n\n/* \u2500\u2500 Bar chart \u2500\u2500 */\n.bars{margin-bottom:1.5rem}\n.brow{display:flex;align-items:center;gap:0.4rem;margin-bottom:0.25rem;font-size:0.72rem}\n.bl{width:90px;text-align:right;color:var(--muted);flex-shrink:0}\n.bb{flex:1;height:14px;background:var(--card);border-radius:3px;overflow:hidden;border:1px solid var(--border)}\n.bf{height:100%;border-radius:2px}\n.bv{width:36px;font-weight:700;font-size:0.68rem}\n.stack{display:flex;gap:0.35rem;flex-wrap:wrap;margin-top:1rem}\n.stack span{background:var(--card);border:1px solid var(--border);padding:0.1rem 0.45rem;border-radius:9999px;font-size:0.62rem;color:var(--muted)}\n\n/* \u2500\u2500 Workspace / Repo structure \u2500\u2500 */\n.ws-info{display:flex;gap:0.6rem;align-items:center;flex-wrap:wrap;margin-bottom:0.5rem;font-size:0.72rem;color:var(--muted)}\n.ws-badge{background:var(--accent);color:#fff;padding:0.15rem 0.5rem;border-radius:4px;font-size:0.65rem;font-weight:700}\n.ws-pkgs{display:flex;flex-direction:column;gap:0.15rem}\n.ws-pkg{display:flex;gap:0.6rem;align-items:center;font-size:0.68rem;padding:0.15rem 0.4rem;background:var(--card);border-radius:4px}\n.ws-path{font-family:monospace;color:var(--text);min-width:140px}\n.ws-name{color:var(--muted);flex:1}\n.ws-flags{color:var(--muted);font-size:0.6rem}\n.ws-more{font-size:0.62rem;color:var(--muted);padding:0.2rem 0.4rem}\n\n/* \u2500\u2500 Category pages \u2500\u2500 */\n.cat-head{margin-bottom:0.3rem}\n.bar2{height:4px;background:var(--card);border-radius:2px;margin-bottom:1.5rem;overflow:hidden}\n.bf2{height:100%;border-radius:2px}\n.check-section{margin-bottom:2.5rem;padding-top:0.5rem;border-top:1px solid var(--border)}\n.check-section:first-of-type{border-top:none}\n\n/* \u2500\u2500 Check detail \u2500\u2500 */\n.ch-head{display:flex;align-items:center;gap:0.7rem;margin-bottom:0.8rem}\n.ch-g{font-size:2rem;font-weight:900}\n.ch-s{display:block;font-size:0.7rem;color:var(--muted)}\n.pri{font-size:0.62rem;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;padding:0.15rem 0.5rem;border-radius:9999px;border:1px solid currentColor;flex-shrink:0}\n.info-panel{background:var(--card-alt);border:1px solid var(--border);border-radius:0.5rem;padding:0.7rem 0.9rem;margin-bottom:1rem;font-size:0.72rem;line-height:1.6}\n.ip-row{margin-bottom:0.4rem;display:flex;gap:0.5rem}\n.ip-row:last-child{margin-bottom:0}\n.ip-label{color:var(--accent);font-weight:700;min-width:2.5rem;flex-shrink:0}\n.skip-r{color:var(--muted);font-style:italic;font-size:0.78rem}\n.kvs{display:flex;gap:0.6rem;flex-wrap:wrap;margin-bottom:1rem}\n.kv{background:var(--card);border:1px solid var(--border);border-radius:0.4rem;padding:0.3rem 0.6rem;font-size:0.7rem}\n.k{color:var(--muted);margin-right:0.3rem}\n.v{font-weight:600}\n\n/* \u2500\u2500 Issue list grouped by file \u2500\u2500 */\n.iss-list{margin-top:1rem}\n.fg{margin-bottom:0.8rem}\n.fn{font-size:0.72rem;font-weight:600;font-family:\"SF Mono\",monospace;padding:0.3rem 0;border-bottom:1px solid var(--border);margin-bottom:0.2rem;display:flex;align-items:center;gap:0.5rem}\n.fc{background:var(--border);border-radius:9999px;padding:0 0.4rem;font-size:0.6rem;color:var(--muted)}\n.ir{font-size:0.65rem;font-family:\"SF Mono\",monospace;padding:0.12rem 0 0.12rem 0.5rem;display:flex;gap:0.4rem;align-items:baseline}\n.is{font-weight:800;font-size:0.55rem;width:0.9rem;text-align:center;border-radius:2px;flex-shrink:0}\n.ir.error .is{color:var(--fail);background:#ef444418}\n.ir.warning .is{color:var(--warn);background:#eab30818}\n.ir.info .is{color:var(--info);background:#6366f118}\n.il{color:var(--accent);min-width:2rem;flex-shrink:0}\n.im{flex:1;word-break:break-word}\n.iru{color:var(--dim);font-size:0.55rem}\n\n/* \u2500\u2500 Source code snippets \u2500\u2500 */\n.src-block{background:var(--card-alt);border:1px solid var(--border);border-radius:6px;margin:0.3rem 0 0.5rem 0.5rem;padding:0.3rem 0;font-family:\"SF Mono\",Menlo,monospace;font-size:0.62rem;line-height:1.6;overflow-x:auto}\n.src-ln{padding:0 0.5rem;white-space:pre}\n.src-hl{padding:0 0.5rem;white-space:pre;background:#eab30815;border-left:2px solid var(--warn)}\n.src-num{color:var(--dim);margin-right:0.5rem;user-select:none;display:inline-block;min-width:2.5rem;text-align:right}\n.src-prompt{padding:0.3rem 0.5rem;border-top:1px solid var(--border);display:flex;justify-content:flex-end}\n.src-fix-btn{background:var(--card);border:1px solid var(--border);color:var(--accent);font-size:0.62rem;padding:0.2rem 0.6rem;border-radius:4px;cursor:pointer;font-family:inherit}\n.src-fix-btn:hover{background:var(--accent);color:#fff;border-color:var(--accent)}\n\n/* \u2500\u2500 All issues table \u2500\u2500 */\n.isf{color:var(--muted);font-size:0.75rem;margin-bottom:0.8rem}\n.it{width:100%;border-collapse:collapse;font-size:0.68rem}\n.it th{text-align:left;padding:0.35rem 0.4rem;color:var(--muted);font-size:0.62rem;text-transform:uppercase;border-bottom:1px solid var(--border)}\n.it td{padding:0.25rem 0.4rem;border-bottom:1px solid var(--border);font-family:\"SF Mono\",monospace;font-size:0.62rem}\n.it tr.error .is2{color:var(--fail)}\n.it tr.warning .is2{color:var(--warn)}\n.is2{font-weight:800;width:1rem}\n.ic2{color:var(--muted);width:70px}\n.il2{color:var(--muted)}\n.iru2{color:var(--dim);font-size:0.58rem}\n\n/* \u2500\u2500 File health \u2500\u2500 */\n.fr{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.3rem;font-size:0.7rem}\n.ff{width:200px;font-family:\"SF Mono\",monospace;font-size:0.65rem;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.fb{flex:1;height:12px;background:var(--card);border-radius:3px;overflow:hidden;border:1px solid var(--border)}\n.fbf{height:100%;border-radius:2px}\n.fv{width:50px;font-size:0.65rem;color:var(--muted);flex-shrink:0}\n.hm-row{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.2rem;font-size:0.7rem}\n.hm-name{width:200px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:\"SF Mono\",monospace;font-size:0.65rem}\n.hm-bar{height:14px;border-radius:3px;min-width:4px}\n.hm-count{color:var(--muted);font-size:0.65rem;flex-shrink:0;min-width:50px}\n.hm-checks{font-size:0.58rem;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n\n/* \u2500\u2500 Premium cards \u2500\u2500 */\n.pro-card{background:linear-gradient(135deg,#0f0f1a 0%,#13131f 100%);border:1px solid #2a2a3d;border-radius:0.75rem;padding:1.5rem;position:relative;overflow:hidden}\n.pro-card::before{content:\"\";position:absolute;top:-50%;right:-50%;width:200%;height:200%;background:radial-gradient(circle,#6366f108 0%,transparent 70%);pointer-events:none}\n.pro-badge{display:inline-block;background:linear-gradient(135deg,#6366f1,#818cf8);color:#fff;font-size:0.6rem;font-weight:800;padding:0.15rem 0.5rem;border-radius:9999px;letter-spacing:0.06em;margin-bottom:0.6rem}\n.pro-desc{color:var(--muted);font-size:0.78rem;line-height:1.6;margin-bottom:0.8rem}\n.pro-cta{color:#6366f1;font-size:0.72rem;font-weight:600;margin-top:1rem}\n.sn-pro{opacity:0.7}\n\n/* \u2500\u2500 Trends page \u2500\u2500 */\n.trend-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem;margin-top:0.5rem}\n.trend-card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:0.8rem}\n.trend-header{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.3rem}\n.trend-name{font-size:0.78rem;font-weight:700;flex:1}\n.trend-score{font-size:1.1rem;font-weight:900}\n.trend-chart{overflow:hidden}\n.trend-chart svg{width:100%;height:60px}\n.trend-table{margin-bottom:1.5rem}\n.trend-row{display:flex;align-items:center;gap:0.5rem;padding:0.25rem 0;border-bottom:1px solid var(--border);font-size:0.75rem}\n.trend-row-name{flex:1;font-weight:600}\n.trend-row-val{width:2rem;text-align:center;color:var(--muted)}\n.trend-row-arrow{color:var(--muted);font-size:0.6rem}\n.trend-row-delta{width:2.5rem;text-align:right;font-weight:700}\n\n.footer{text-align:center;color:var(--muted);font-size:0.58rem;margin-top:2rem;padding:0.8rem 0;border-top:1px solid var(--border)}\n.footer a{color:var(--muted)}\n.muted{color:var(--muted)}\n.deeper-tools code{background:var(--border);padding:0.1rem 0.4rem;border-radius:4px;font-size:0.62rem;color:var(--accent);margin-right:0.3rem}\n.flink{color:var(--accent);text-decoration:none;font-family:\"SF Mono\",monospace}.flink:hover{text-decoration:underline}\n.arch-svg{margin:1rem 0;overflow-x:auto;-webkit-overflow-scrolling:touch}\n.arch-svg svg{border-radius:8px}\n.cp-btn{background:none;border:none;cursor:pointer;font-size:0.6rem;opacity:0.3;padding:0 0.2rem;flex-shrink:0}.cp-btn:hover{opacity:1}\n.ir:hover .cp-btn{opacity:0.6}\n\n/* \u2500\u2500 Mobile: hamburger collapses both navs \u2500\u2500 */\n@media(max-width:768px){\n.hamburger{display:block}\n.nav-scroll{display:none}\n.nav-scroll.open{display:flex;position:absolute;top:var(--top-h);left:0;right:0;background:var(--bg);border-bottom:1px solid var(--border);flex-wrap:wrap;padding:0.3rem 0.5rem;z-index:25}\n.side{display:none}\n.side.open{display:block;z-index:25}\n.top{padding:0 0.8rem}\n.logo{font-size:0.85rem;margin-right:0.5rem}\n.content{margin-left:0;padding:0.8rem}\n.cats{grid-template-columns:1fr 1fr}\n.dash{flex-direction:column;gap:1rem}\n.hero svg{width:80px;height:80px}\n.hg{font-size:2rem}\n.radar svg{max-width:180px}\n.bl{width:60px;font-size:0.62rem}\n.bv{width:30px;font-size:0.6rem}\n.it{display:block;overflow-x:auto;-webkit-overflow-scrolling:touch}\n.ff{width:120px;font-size:0.58rem}\n.hm-name{width:120px;font-size:0.58rem}\n.hm-checks{display:none}\n.ov-check{width:50px}\n.ov-loc{max-width:120px}\n.ir{font-size:0.6rem}\n.ch-head{flex-wrap:wrap}\n.ch-g{font-size:1.5rem}\n.info-panel{font-size:0.68rem;padding:0.5rem 0.6rem}\n.ip-row{flex-direction:column;gap:0.1rem}\n.kvs{gap:0.4rem}\n.kv{font-size:0.62rem;padding:0.2rem 0.4rem}\n.arch-svg svg{min-width:400px}\n}\n@media(max-width:480px){\n.cats{grid-template-columns:1fr}\n.tn{padding:0 0.4rem;font-size:0.65rem}\n.ff{width:90px}\n.hm-name{width:90px}\n.ov-check{display:none}\n}\n\n/* \u2500\u2500 Feature Map (Pro) \u2500\u2500 */\n.fm-header{margin-bottom:1.5rem}\n.fm-header h2{display:flex;align-items:center;gap:0.6rem}\n.fm-stats{display:flex;gap:1.5rem;margin-bottom:2rem;padding:1rem 1.2rem;background:var(--card);border:1px solid var(--border);border-radius:12px}\n.fm-stat{display:flex;flex-direction:column;align-items:center}\n.fm-stat-n{font-size:1.6rem;font-weight:900;line-height:1.2}\n.fm-stat-l{font-size:0.65rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.04em;font-weight:600}\n.fm-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}\n.fm-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:1.2rem;transition:border-color 0.15s}\n.fm-card:hover{border-color:#333}\n.fm-card-issue{border-color:#eab30830;background:linear-gradient(135deg,var(--card) 0%,#1a1a0f 100%)}\n.fm-card-top{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:0.4rem}\n.fm-card-label{font-weight:800;font-size:0.95rem}\n.fm-card-desc{font-size:0.72rem;color:var(--muted);margin-bottom:0.5rem;line-height:1.4}\n.fm-card-dir{font-size:0.65rem;color:var(--dim);font-family:\"SF Mono\",monospace;margin-bottom:0.6rem}\n.fm-card-badge{font-size:0.6rem;font-weight:700;padding:0.15rem 0.5rem;border-radius:9999px;white-space:nowrap}\n.fm-ok{background:#22c55e18;color:var(--pass)}\n.fm-warn{background:#eab30818;color:var(--warn)}\n.fm-info{background:#6366f118;color:var(--info)}\n.fm-card-files{display:flex;flex-direction:column;gap:0.15rem;margin-bottom:0.6rem}\n.fm-file{font-size:0.68rem;color:var(--muted);font-family:\"SF Mono\",monospace}\n.fm-file a{color:var(--accent);text-decoration:none}\n.fm-file a:hover{text-decoration:underline}\n.fm-more{color:var(--dim);font-style:italic}\n.fm-findings{margin-top:0.6rem;padding-top:0.6rem;border-top:1px solid var(--border);display:flex;flex-direction:column;gap:0.3rem}\n.fm-finding{display:flex;align-items:baseline;gap:0.4rem;font-size:0.68rem;line-height:1.4}\n.fm-f-sev{font-weight:800;font-size:0.6rem;width:1rem;flex-shrink:0}\n.fm-f-warn .fm-f-sev{color:var(--warn)}\n.fm-f-info .fm-f-sev{color:var(--info)}\n.fm-f-loc{color:var(--dim);font-family:\"SF Mono\",monospace;flex-shrink:0}\n.fm-f-loc a{color:var(--accent);text-decoration:none}\n.fm-f-msg{color:var(--text)}\n.fm-f-rule{color:var(--dim);font-size:0.6rem;font-family:\"SF Mono\",monospace}\n\n/* Teaser (no Pro key) */\n.fm-teaser{margin-top:1.5rem}\n.fm-teaser-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:1rem;margin-bottom:2rem}\n.fm-card-blur{filter:blur(3px);opacity:0.5;pointer-events:none;user-select:none}\n.fm-cta{text-align:center;padding:2rem;background:linear-gradient(135deg,#0f0f1a,#13131f);border:1px solid #2a2a3d;border-radius:12px}\n.fm-cta code{background:#1a1a2e;padding:0.2rem 0.5rem;border-radius:4px;font-size:0.8rem}\n@media(max-width:640px){\n.fm-grid,.fm-teaser-grid{grid-template-columns:1fr}\n.fm-stats{flex-wrap:wrap;gap:1rem}\n}\n\n/* \u2500\u2500 Preferences panel \u2500\u2500 */\n.prefs-btn{background:none;border:1px solid var(--border);color:var(--muted);font-size:0.72rem;cursor:pointer;padding:0.2rem 0.5rem;border-radius:6px;margin-left:auto;flex-shrink:0;font-family:inherit;line-height:1.4}\n.prefs-btn:hover{color:var(--text);border-color:var(--dim)}\n.prefs-panel{display:none;position:absolute;top:var(--top-h);right:1rem;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:0.8rem 1rem;z-index:40;min-width:200px;box-shadow:0 8px 30px #0008}\n.prefs-panel.open{display:block}\n.prefs-label{font-size:0.6rem;text-transform:uppercase;letter-spacing:0.05em;color:var(--dim);font-weight:600;margin-bottom:0.3rem}\n.prefs-label:not(:first-child){margin-top:0.7rem}\n.prefs-row{display:flex;gap:0.3rem}\n.prefs-opt{background:var(--card-alt);border:1px solid var(--border);color:var(--muted);font-size:0.68rem;padding:0.25rem 0.6rem;border-radius:6px;cursor:pointer;font-family:inherit;transition:all 0.1s}\n.prefs-opt:hover{color:var(--text);border-color:var(--dim)}\n.prefs-opt.active{background:var(--accent);color:#fff;border-color:var(--accent)}\n[data-theme=\"light\"] .prefs-panel{box-shadow:0 8px 30px #0002}\n";
|
|
2
|
+
export declare const CSS = "\n:root{--bg:#09090b;--card:#111115;--border:#1e1e24;--text:#e5e5e5;--muted:#6b7280;--pass:#22c55e;--fail:#ef4444;--warn:#eab308;--info:#6366f1;--accent:#818cf8;--side-w:200px;--top-h:42px;--nav-bg:#0c0c0fdd;--side-bg:#0c0c0f;--hover:#14141a;--dim:#555;--card-alt:#0d0d12}\n[data-theme=\"light\"]{--bg:#f5f5f7;--card:#ffffff;--border:#e2e4e9;--text:#1a1a2e;--muted:#64748b;--pass:#16a34a;--fail:#dc2626;--warn:#ca8a04;--info:#4f46e5;--accent:#4f46e5;--nav-bg:#ffffffee;--side-bg:#fafafa;--hover:#eef0f5;--dim:#94a3b8;--card-alt:#f0f0f5}\nhtml{font-size:17px}\n*{margin:0;padding:0;box-sizing:border-box}\nbody{font-family:\"Inter\",system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.5}\ncode{font-family:\"SF Mono\",Menlo,monospace;font-size:0.85em}\n\n/* \u2500\u2500 Top nav \u2500\u2500 */\n.top{position:sticky;top:0;z-index:30;background:var(--nav-bg);backdrop-filter:blur(12px);border-bottom:1px solid var(--border);padding:0 1.5rem;display:flex;align-items:center;height:var(--top-h)}\n.logo{font-weight:800;font-size:1rem;margin-right:0.5rem;flex-shrink:0;text-decoration:none;color:var(--text)}\n.logo span{color:var(--accent)}\n.nav-project{font-size:0.72rem;color:var(--muted);font-weight:600;margin-right:1rem;padding:0.2rem 0.5rem;background:var(--card);border:1px solid var(--border);border-radius:4px;flex-shrink:0}\n.nav-scroll{display:flex;align-items:center;gap:0;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;flex:1}\n.nav-scroll::-webkit-scrollbar{display:none}\n.tn{padding:0 0.7rem;font-size:0.78rem;color:var(--muted);text-decoration:none;border-bottom:2px solid transparent;transition:all 0.15s;white-space:nowrap;line-height:var(--top-h)}\n.tn:hover{color:var(--text)}\n.tn.active{color:var(--text);border-bottom-color:var(--accent)}\n.hamburger{display:none;background:none;border:none;color:var(--muted);font-size:1.3rem;cursor:pointer;padding:0 0.4rem;line-height:var(--top-h)}\n\n/* \u2500\u2500 Sidebar \u2500\u2500 */\n.side{position:fixed;top:var(--top-h);left:0;bottom:0;width:var(--side-w);background:var(--side-bg);border-right:1px solid var(--border);overflow-y:auto;padding:0.6rem 0;font-size:0.7rem;z-index:20}\n.side-section{padding:0.3rem 0;border-bottom:1px solid var(--border)}\n.side-section:last-child{border-bottom:none}\n.side-label{padding:0.2rem 0.8rem;font-size:0.6rem;text-transform:uppercase;letter-spacing:0.05em;color:var(--dim);font-weight:600}\n.side-score{font-size:1.4rem;font-weight:900;padding:0.2rem 0.8rem}\n.side-cat{display:block;padding:0.3rem 0.8rem;color:var(--muted);font-weight:600;cursor:pointer;text-decoration:none;font-size:0.72rem}\n.side-cat:hover{background:var(--hover);color:var(--text)}\n.side-cat-active{color:var(--text);font-weight:700;border-left:2px solid var(--accent);padding-left:calc(0.8rem - 2px)}\n.side-cat-title{padding:0.3rem 0.8rem;font-size:0.65rem;text-transform:uppercase;letter-spacing:0.04em;color:var(--accent);font-weight:700}\n.side-check{display:block;padding:0.15rem 0.8rem 0.15rem 0.8rem;color:var(--muted);cursor:pointer;text-decoration:none;font-size:0.65rem}\n.side-check:hover{color:var(--text);background:#14141a}\n.side-check span{display:inline-block;min-width:2.5rem;font-weight:700;font-size:0.6rem}\n.side-stat{padding:0.15rem 0.8rem;font-size:0.7rem;color:var(--muted)}\n.side-stat span{font-weight:800;font-size:0.8rem}\n.side-views{padding-top:0.3rem}\n.side-views .side-check{padding-left:0.8rem}\n\n/* \u2500\u2500 Content \u2500\u2500 */\n.content{margin-left:var(--side-w);padding:1.5rem 2rem;max-width:960px}\n\n/* \u2500\u2500 Overview \u2500\u2500 */\n.dash{display:flex;gap:2rem;margin-bottom:2rem;align-items:center;flex-wrap:wrap}\n.hero{display:flex;align-items:center;gap:1rem}\n.hero svg{width:100px;height:100px}\n.hc{display:flex;flex-direction:column}\n.hg{font-size:2.5rem;font-weight:900;line-height:1}\n.hs{font-size:1rem;font-weight:600}\n.hd{font-size:0.68rem;color:var(--muted)}\n.radar{flex:1;display:flex;justify-content:center}\n.radar svg{max-width:240px;width:100%}\n.cats{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:0.6rem;margin-bottom:2rem}\n.cc{background:var(--card);border:1px solid var(--border);border-radius:0.6rem;padding:0.8rem;transition:border-color 0.15s;text-decoration:none;color:var(--text);display:block}\n.cc:hover{border-color:var(--accent)}\n.cc-s{font-size:1.8rem;font-weight:900}\n.cc-l{font-size:0.75rem;color:var(--muted)}\n.cc-m{margin-top:0.3rem;display:flex;gap:0.25rem}\n.mc{font-size:0.65rem;font-weight:800}\nh3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:0.5rem}\n\n/* \u2500\u2500 Overview sections \u2500\u2500 */\n.ov-section{margin-bottom:1.5rem}\n.ov-issue{font-size:0.68rem;font-family:\"SF Mono\",monospace;padding:0.2rem 0;display:flex;gap:0.4rem;align-items:baseline;border-bottom:1px solid var(--border)}\n.ov-issue .is{flex-shrink:0}\n.ov-issue.error .is{color:var(--fail)}\n.ov-issue.warning .is{color:var(--warn)}\n.ov-check{color:var(--muted);width:70px;flex-shrink:0;font-size:0.62rem}\n.ov-loc{color:var(--accent);flex-shrink:0;font-size:0.62rem;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.ov-msg{flex:1;word-break:break-word}\n.ov-link{display:block;margin-top:0.5rem;font-size:0.72rem;color:var(--accent);text-decoration:none}\n.ov-link:hover{text-decoration:underline}\n\n/* \u2500\u2500 Timeline \u2500\u2500 */\n.timeline{margin:0.5rem 0;overflow-x:auto}\n.timeline svg{max-width:100%}\n\n/* \u2500\u2500 Bar chart \u2500\u2500 */\n.bars{margin-bottom:1.5rem}\n.brow{display:flex;align-items:center;gap:0.4rem;margin-bottom:0.25rem;font-size:0.72rem}\n.bl{width:90px;text-align:right;color:var(--muted);flex-shrink:0}\n.bb{flex:1;height:14px;background:var(--card);border-radius:3px;overflow:hidden;border:1px solid var(--border)}\n.bf{height:100%;border-radius:2px}\n.bv{width:36px;font-weight:700;font-size:0.68rem}\n.stack{display:flex;gap:0.35rem;flex-wrap:wrap;margin-top:1rem}\n.stack span{background:var(--card);border:1px solid var(--border);padding:0.1rem 0.45rem;border-radius:9999px;font-size:0.62rem;color:var(--muted)}\n\n/* \u2500\u2500 Workspace / Repo structure \u2500\u2500 */\n.ws-info{display:flex;gap:0.6rem;align-items:center;flex-wrap:wrap;margin-bottom:0.5rem;font-size:0.72rem;color:var(--muted)}\n.ws-badge{background:var(--accent);color:#fff;padding:0.15rem 0.5rem;border-radius:4px;font-size:0.65rem;font-weight:700}\n.ws-pkgs{display:flex;flex-direction:column;gap:0.15rem}\n.ws-pkg{display:flex;gap:0.6rem;align-items:center;font-size:0.68rem;padding:0.15rem 0.4rem;background:var(--card);border-radius:4px}\n.ws-path{font-family:monospace;color:var(--text);min-width:140px}\n.ws-name{color:var(--muted);flex:1}\n.ws-flags{color:var(--muted);font-size:0.6rem}\n.ws-more{font-size:0.62rem;color:var(--muted);padding:0.2rem 0.4rem}\n\n/* \u2500\u2500 Category pages \u2500\u2500 */\n.cat-head{margin-bottom:0.3rem}\n.bar2{height:4px;background:var(--card);border-radius:2px;margin-bottom:1.5rem;overflow:hidden}\n.bf2{height:100%;border-radius:2px}\n.check-section{margin-bottom:2.5rem;padding-top:0.5rem;border-top:1px solid var(--border)}\n.check-section:first-of-type{border-top:none}\n\n/* \u2500\u2500 Check detail \u2500\u2500 */\n.ch-head{display:flex;align-items:center;gap:0.7rem;margin-bottom:0.8rem}\n.ch-g{font-size:2rem;font-weight:900}\n.ch-s{display:block;font-size:0.7rem;color:var(--muted)}\n.pri{font-size:0.62rem;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;padding:0.15rem 0.5rem;border-radius:9999px;border:1px solid currentColor;flex-shrink:0}\n.info-panel{background:var(--card-alt);border:1px solid var(--border);border-radius:0.5rem;padding:0.7rem 0.9rem;margin-bottom:1rem;font-size:0.72rem;line-height:1.6}\n.ip-row{margin-bottom:0.4rem;display:flex;gap:0.5rem}\n.ip-row:last-child{margin-bottom:0}\n.ip-label{color:var(--accent);font-weight:700;min-width:2.5rem;flex-shrink:0}\n.skip-r{color:var(--muted);font-style:italic;font-size:0.78rem}\n.kvs{display:flex;gap:0.6rem;flex-wrap:wrap;margin-bottom:1rem}\n.kv{background:var(--card);border:1px solid var(--border);border-radius:0.4rem;padding:0.3rem 0.6rem;font-size:0.7rem}\n.k{color:var(--muted);margin-right:0.3rem}\n.v{font-weight:600}\n\n/* \u2500\u2500 Issue list grouped by file \u2500\u2500 */\n.iss-list{margin-top:1rem}\n.fg{margin-bottom:0.8rem}\n.fn{font-size:0.72rem;font-weight:600;font-family:\"SF Mono\",monospace;padding:0.3rem 0;border-bottom:1px solid var(--border);margin-bottom:0.2rem;display:flex;align-items:center;gap:0.5rem}\n.fc{background:var(--border);border-radius:9999px;padding:0 0.4rem;font-size:0.6rem;color:var(--muted)}\n.ir{font-size:0.65rem;font-family:\"SF Mono\",monospace;padding:0.12rem 0 0.12rem 0.5rem;display:flex;gap:0.4rem;align-items:baseline}\n.is{font-weight:800;font-size:0.55rem;width:0.9rem;text-align:center;border-radius:2px;flex-shrink:0}\n.ir.error .is{color:var(--fail);background:#ef444418}\n.ir.warning .is{color:var(--warn);background:#eab30818}\n.ir.info .is{color:var(--info);background:#6366f118}\n.il{color:var(--accent);min-width:2rem;flex-shrink:0}\n.im{flex:1;word-break:break-word}\n.iru{color:var(--dim);font-size:0.55rem}\n\n/* \u2500\u2500 Source code snippets \u2500\u2500 */\n.src-block{background:var(--card-alt);border:1px solid var(--border);border-radius:6px;margin:0.3rem 0 0.5rem 0.5rem;padding:0.3rem 0;font-family:\"SF Mono\",Menlo,monospace;font-size:0.62rem;line-height:1.6;overflow-x:auto}\n.src-ln{padding:0 0.5rem;white-space:pre}\n.src-hl{padding:0 0.5rem;white-space:pre;background:#eab30815;border-left:2px solid var(--warn)}\n.src-num{color:var(--dim);margin-right:0.5rem;user-select:none;display:inline-block;min-width:2.5rem;text-align:right}\n.src-prompt{padding:0.3rem 0.5rem;border-top:1px solid var(--border);display:flex;justify-content:flex-end}\n.src-fix-btn{background:var(--card);border:1px solid var(--border);color:var(--accent);font-size:0.62rem;padding:0.2rem 0.6rem;border-radius:4px;cursor:pointer;font-family:inherit}\n.src-fix-btn:hover{background:var(--accent);color:#fff;border-color:var(--accent)}\n\n/* \u2500\u2500 All issues table \u2500\u2500 */\n.isf{color:var(--muted);font-size:0.75rem;margin-bottom:0.8rem}\n.it{width:100%;border-collapse:collapse;font-size:0.68rem}\n.it th{text-align:left;padding:0.35rem 0.4rem;color:var(--muted);font-size:0.62rem;text-transform:uppercase;border-bottom:1px solid var(--border)}\n.it td{padding:0.25rem 0.4rem;border-bottom:1px solid var(--border);font-family:\"SF Mono\",monospace;font-size:0.62rem}\n.it tr.error .is2{color:var(--fail)}\n.it tr.warning .is2{color:var(--warn)}\n.is2{font-weight:800;width:1rem}\n.ic2{color:var(--muted);width:70px}\n.il2{color:var(--muted)}\n.iru2{color:var(--dim);font-size:0.58rem}\n\n/* \u2500\u2500 File health \u2500\u2500 */\n.fr{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.3rem;font-size:0.7rem}\n.ff{width:200px;font-family:\"SF Mono\",monospace;font-size:0.65rem;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.fb{flex:1;height:12px;background:var(--card);border-radius:3px;overflow:hidden;border:1px solid var(--border)}\n.fbf{height:100%;border-radius:2px}\n.fv{width:50px;font-size:0.65rem;color:var(--muted);flex-shrink:0}\n.hm-row{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.2rem;font-size:0.7rem}\n.hm-name{width:200px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:\"SF Mono\",monospace;font-size:0.65rem}\n.hm-bar{height:14px;border-radius:3px;min-width:4px}\n.hm-count{color:var(--muted);font-size:0.65rem;flex-shrink:0;min-width:50px}\n.hm-checks{font-size:0.58rem;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n\n/* \u2500\u2500 Premium cards \u2500\u2500 */\n.pro-card{background:linear-gradient(135deg,#0f0f1a 0%,#13131f 100%);border:1px solid #2a2a3d;border-radius:0.75rem;padding:1.5rem;position:relative;overflow:hidden}\n.pro-card::before{content:\"\";position:absolute;top:-50%;right:-50%;width:200%;height:200%;background:radial-gradient(circle,#6366f108 0%,transparent 70%);pointer-events:none}\n.pro-badge{display:inline-block;background:linear-gradient(135deg,#6366f1,#818cf8);color:#fff;font-size:0.6rem;font-weight:800;padding:0.15rem 0.5rem;border-radius:9999px;letter-spacing:0.06em;margin-bottom:0.6rem}\n.pro-desc{color:var(--muted);font-size:0.78rem;line-height:1.6;margin-bottom:0.8rem}\n.pro-cta{color:#6366f1;font-size:0.72rem;font-weight:600;margin-top:1rem}\n.sn-pro{opacity:0.7}\n\n/* \u2500\u2500 Trends page \u2500\u2500 */\n.trend-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem;margin-top:0.5rem}\n.trend-card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:0.8rem}\n.trend-header{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.3rem}\n.trend-name{font-size:0.78rem;font-weight:700;flex:1}\n.trend-score{font-size:1.1rem;font-weight:900}\n.trend-chart{overflow:hidden}\n.trend-chart svg{width:100%;height:60px}\n.trend-table{margin-bottom:1.5rem}\n.trend-row{display:flex;align-items:center;gap:0.5rem;padding:0.25rem 0;border-bottom:1px solid var(--border);font-size:0.75rem}\n.trend-row-name{flex:1;font-weight:600}\n.trend-row-val{width:2rem;text-align:center;color:var(--muted)}\n.trend-row-arrow{color:var(--muted);font-size:0.6rem}\n.trend-row-delta{width:2.5rem;text-align:right;font-weight:700}\n\n.footer{text-align:center;color:var(--muted);font-size:0.58rem;margin-top:2rem;padding:0.8rem 0;border-top:1px solid var(--border)}\n.footer a{color:var(--muted)}\n.muted{color:var(--muted)}\n.deeper-tools code{background:var(--border);padding:0.1rem 0.4rem;border-radius:4px;font-size:0.62rem;color:var(--accent);margin-right:0.3rem}\n.flink{color:var(--accent);text-decoration:none;font-family:\"SF Mono\",monospace}.flink:hover{text-decoration:underline}\n.arch-svg{margin:1rem 0;overflow-x:auto;-webkit-overflow-scrolling:touch}\n.arch-svg svg{border-radius:8px}\n.cp-btn{background:none;border:none;cursor:pointer;font-size:0.6rem;opacity:0.3;padding:0 0.2rem;flex-shrink:0}.cp-btn:hover{opacity:1}\n.ir:hover .cp-btn{opacity:0.6}\n\n/* \u2500\u2500 Mobile: hamburger collapses both navs \u2500\u2500 */\n@media(max-width:768px){\n.hamburger{display:block}\n.nav-scroll{display:none}\n.nav-scroll.open{display:flex;position:absolute;top:var(--top-h);left:0;right:0;background:var(--bg);border-bottom:1px solid var(--border);flex-wrap:wrap;padding:0.3rem 0.5rem;z-index:25}\n.side{display:none}\n.side.open{display:block;z-index:25}\n.top{padding:0 0.8rem}\n.logo{font-size:0.85rem;margin-right:0.5rem}\n.content{margin-left:0;padding:0.8rem}\n.cats{grid-template-columns:1fr 1fr}\n.dash{flex-direction:column;gap:1rem}\n.hero svg{width:80px;height:80px}\n.hg{font-size:2rem}\n.radar svg{max-width:180px}\n.bl{width:60px;font-size:0.62rem}\n.bv{width:30px;font-size:0.6rem}\n.it{display:block;overflow-x:auto;-webkit-overflow-scrolling:touch}\n.ff{width:120px;font-size:0.58rem}\n.hm-name{width:120px;font-size:0.58rem}\n.hm-checks{display:none}\n.ov-check{width:50px}\n.ov-loc{max-width:120px}\n.ir{font-size:0.6rem}\n.ch-head{flex-wrap:wrap}\n.ch-g{font-size:1.5rem}\n.info-panel{font-size:0.68rem;padding:0.5rem 0.6rem}\n.ip-row{flex-direction:column;gap:0.1rem}\n.kvs{gap:0.4rem}\n.kv{font-size:0.62rem;padding:0.2rem 0.4rem}\n.arch-svg svg{min-width:400px}\n}\n@media(max-width:480px){\n.cats{grid-template-columns:1fr}\n.tn{padding:0 0.4rem;font-size:0.65rem}\n.ff{width:90px}\n.hm-name{width:90px}\n.ov-check{display:none}\n}\n\n/* \u2500\u2500 Feature Map (Pro) \u2500\u2500 */\n.fm-header{margin-bottom:1.5rem}\n.fm-header h2{display:flex;align-items:center;gap:0.6rem}\n.fm-stats{display:flex;gap:1.5rem;margin-bottom:2rem;padding:1rem 1.2rem;background:var(--card);border:1px solid var(--border);border-radius:12px}\n.fm-stat{display:flex;flex-direction:column;align-items:center}\n.fm-stat-n{font-size:1.6rem;font-weight:900;line-height:1.2}\n.fm-stat-l{font-size:0.65rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.04em;font-weight:600}\n.fm-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}\n.fm-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:1.2rem;transition:border-color 0.15s}\n.fm-card:hover{border-color:#333}\n.fm-card-issue{border-color:#eab30830;background:linear-gradient(135deg,var(--card) 0%,#1a1a0f 100%)}\n.fm-card-top{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:0.4rem}\n.fm-card-label{font-weight:800;font-size:0.95rem}\n.fm-card-desc{font-size:0.72rem;color:var(--muted);margin-bottom:0.5rem;line-height:1.4}\n.fm-card-dir{font-size:0.65rem;color:var(--dim);font-family:\"SF Mono\",monospace;margin-bottom:0.6rem}\n.fm-card-badge{font-size:0.6rem;font-weight:700;padding:0.15rem 0.5rem;border-radius:9999px;white-space:nowrap}\n.fm-ok{background:#22c55e18;color:var(--pass)}\n.fm-warn{background:#eab30818;color:var(--warn)}\n.fm-info{background:#6366f118;color:var(--info)}\n.fm-card-files{display:flex;flex-direction:column;gap:0.15rem;margin-bottom:0.6rem}\n.fm-file{font-size:0.68rem;color:var(--muted);font-family:\"SF Mono\",monospace}\n.fm-file a{color:var(--accent);text-decoration:none}\n.fm-file a:hover{text-decoration:underline}\n.fm-more{color:var(--dim);font-style:italic}\n.fm-findings{margin-top:0.6rem;padding-top:0.6rem;border-top:1px solid var(--border);display:flex;flex-direction:column;gap:0.3rem}\n.fm-finding{display:flex;align-items:baseline;gap:0.4rem;font-size:0.68rem;line-height:1.4}\n.fm-f-sev{font-weight:800;font-size:0.6rem;width:1rem;flex-shrink:0}\n.fm-f-warn .fm-f-sev{color:var(--warn)}\n.fm-f-info .fm-f-sev{color:var(--info)}\n.fm-f-loc{color:var(--dim);font-family:\"SF Mono\",monospace;flex-shrink:0}\n.fm-f-loc a{color:var(--accent);text-decoration:none}\n.fm-f-msg{color:var(--text)}\n.fm-f-rule{color:var(--dim);font-size:0.6rem;font-family:\"SF Mono\",monospace}\n\n/* Teaser (no Pro key) */\n.fm-teaser{margin-top:1.5rem}\n.fm-teaser-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:1rem;margin-bottom:2rem}\n.fm-card-blur{filter:blur(3px);opacity:0.5;pointer-events:none;user-select:none}\n.fm-cta{text-align:center;padding:2rem;background:linear-gradient(135deg,#0f0f1a,#13131f);border:1px solid #2a2a3d;border-radius:12px}\n.fm-cta code{background:#1a1a2e;padding:0.2rem 0.5rem;border-radius:4px;font-size:0.8rem}\n@media(max-width:640px){\n.fm-grid,.fm-teaser-grid{grid-template-columns:1fr}\n.fm-stats{flex-wrap:wrap;gap:1rem}\n}\n\n/* \u2500\u2500 Preferences panel \u2500\u2500 */\n.prefs-btn{background:none;border:1px solid var(--border);color:var(--muted);font-size:0.72rem;cursor:pointer;padding:0.2rem 0.5rem;border-radius:6px;margin-left:auto;flex-shrink:0;font-family:inherit;line-height:1.4}\n.prefs-btn:hover{color:var(--text);border-color:var(--dim)}\n.prefs-panel{display:none;position:absolute;top:var(--top-h);right:1rem;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:0.8rem 1rem;z-index:40;min-width:200px;box-shadow:0 8px 30px #0008}\n.prefs-panel.open{display:block}\n.prefs-label{font-size:0.6rem;text-transform:uppercase;letter-spacing:0.05em;color:var(--dim);font-weight:600;margin-bottom:0.3rem}\n.prefs-label:not(:first-child){margin-top:0.7rem}\n.prefs-row{display:flex;gap:0.3rem}\n.prefs-opt{background:var(--card-alt);border:1px solid var(--border);color:var(--muted);font-size:0.68rem;padding:0.25rem 0.6rem;border-radius:6px;cursor:pointer;font-family:inherit;transition:all 0.1s}\n.prefs-opt:hover{color:var(--text);border-color:var(--dim)}\n.prefs-opt.active{background:var(--accent);color:#fff;border-color:var(--accent)}\n[data-theme=\"light\"] .prefs-panel{box-shadow:0 8px 30px #0002}\n\n/* \u2500\u2500 Actions page \u2500\u2500 */\n.act-summary{display:flex;gap:1.5rem;margin-bottom:2rem}\n.act-stat{text-align:center}\n.act-stat-n{display:block;font-size:1.8rem;font-weight:700;line-height:1.2}\n.act-stat-l{font-size:0.72rem;color:var(--muted)}\n.act-section{margin-bottom:2.5rem}\n.act-section h3{display:flex;align-items:center;gap:0.5rem;font-size:1rem;margin-bottom:0.3rem}\n.act-icon{display:inline-flex;align-items:center;justify-content:center;width:1.6rem;height:1.6rem;border-radius:8px;font-size:0.9rem}\n.act-count{font-size:0.72rem;color:var(--muted);font-weight:400}\n.act-desc{font-size:0.78rem;color:var(--muted);margin-bottom:0.8rem}\n.act-cmd{margin-bottom:1rem}\n.act-cmd code{display:inline-block;background:var(--card);border:1px solid var(--border);padding:0.3rem 0.8rem;border-radius:6px;font-size:0.78rem;color:var(--accent)}\n.act-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:0.8rem}\n.act-card{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:0.8rem 1rem}\n.act-card-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:0.4rem}\n.act-check{font-size:0.78rem;font-weight:600}\n.act-fix{font-size:0.78rem;color:var(--pass);margin-bottom:0.5rem;padding:0.3rem 0.5rem;background:var(--pass)10;border-radius:4px}\n.act-rec{font-size:0.75rem;color:var(--muted);margin-bottom:0.5rem;line-height:1.4}\n.act-item{font-size:0.72rem;color:var(--dim);padding:0.15rem 0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.act-details{margin-top:0.5rem}\n.act-details summary{font-size:0.75rem;color:var(--muted);cursor:pointer}\n.act-table{width:100%;font-size:0.72rem;margin-top:0.3rem}\n.act-table td{padding:0.2rem 0.4rem;border-bottom:1px solid var(--border)}\n.act-table .act-check{color:var(--muted);white-space:nowrap}\n@media(max-width:600px){.act-summary{flex-direction:column;gap:0.8rem;align-items:center}.act-grid{grid-template-columns:1fr}}\n\n/* \u2500\u2500 Delta banner \u2500\u2500 */\n.delta-banner{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:1.2rem 1.5rem;margin-bottom:2rem}\n.delta-head{display:flex;align-items:center;gap:1rem;margin-bottom:0.5rem}\n.delta-title{font-weight:700;font-size:0.9rem}\n.delta-score{font-size:0.85rem;color:var(--muted)}\n.delta-arrow{font-weight:800;font-size:1rem}\n.delta-stats{display:flex;gap:1.2rem;font-size:0.78rem;margin-bottom:0.5rem}\n.delta-checks{display:flex;flex-wrap:wrap;gap:0.4rem;margin-bottom:0.5rem}\n.delta-chip{font-size:0.7rem;padding:0.15rem 0.5rem;background:var(--card-alt);border:1px solid var(--border);border-radius:4px;white-space:nowrap}\n.delta-fixed,.delta-new{font-size:0.75rem;color:var(--muted);margin-top:0.3rem}\n";
|
package/dist/report/styles.js
CHANGED
|
@@ -294,4 +294,41 @@ h3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:
|
|
|
294
294
|
.prefs-opt:hover{color:var(--text);border-color:var(--dim)}
|
|
295
295
|
.prefs-opt.active{background:var(--accent);color:#fff;border-color:var(--accent)}
|
|
296
296
|
[data-theme="light"] .prefs-panel{box-shadow:0 8px 30px #0002}
|
|
297
|
+
|
|
298
|
+
/* โโ Actions page โโ */
|
|
299
|
+
.act-summary{display:flex;gap:1.5rem;margin-bottom:2rem}
|
|
300
|
+
.act-stat{text-align:center}
|
|
301
|
+
.act-stat-n{display:block;font-size:1.8rem;font-weight:700;line-height:1.2}
|
|
302
|
+
.act-stat-l{font-size:0.72rem;color:var(--muted)}
|
|
303
|
+
.act-section{margin-bottom:2.5rem}
|
|
304
|
+
.act-section h3{display:flex;align-items:center;gap:0.5rem;font-size:1rem;margin-bottom:0.3rem}
|
|
305
|
+
.act-icon{display:inline-flex;align-items:center;justify-content:center;width:1.6rem;height:1.6rem;border-radius:8px;font-size:0.9rem}
|
|
306
|
+
.act-count{font-size:0.72rem;color:var(--muted);font-weight:400}
|
|
307
|
+
.act-desc{font-size:0.78rem;color:var(--muted);margin-bottom:0.8rem}
|
|
308
|
+
.act-cmd{margin-bottom:1rem}
|
|
309
|
+
.act-cmd code{display:inline-block;background:var(--card);border:1px solid var(--border);padding:0.3rem 0.8rem;border-radius:6px;font-size:0.78rem;color:var(--accent)}
|
|
310
|
+
.act-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:0.8rem}
|
|
311
|
+
.act-card{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:0.8rem 1rem}
|
|
312
|
+
.act-card-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:0.4rem}
|
|
313
|
+
.act-check{font-size:0.78rem;font-weight:600}
|
|
314
|
+
.act-fix{font-size:0.78rem;color:var(--pass);margin-bottom:0.5rem;padding:0.3rem 0.5rem;background:var(--pass)10;border-radius:4px}
|
|
315
|
+
.act-rec{font-size:0.75rem;color:var(--muted);margin-bottom:0.5rem;line-height:1.4}
|
|
316
|
+
.act-item{font-size:0.72rem;color:var(--dim);padding:0.15rem 0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
317
|
+
.act-details{margin-top:0.5rem}
|
|
318
|
+
.act-details summary{font-size:0.75rem;color:var(--muted);cursor:pointer}
|
|
319
|
+
.act-table{width:100%;font-size:0.72rem;margin-top:0.3rem}
|
|
320
|
+
.act-table td{padding:0.2rem 0.4rem;border-bottom:1px solid var(--border)}
|
|
321
|
+
.act-table .act-check{color:var(--muted);white-space:nowrap}
|
|
322
|
+
@media(max-width:600px){.act-summary{flex-direction:column;gap:0.8rem;align-items:center}.act-grid{grid-template-columns:1fr}}
|
|
323
|
+
|
|
324
|
+
/* โโ Delta banner โโ */
|
|
325
|
+
.delta-banner{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:1.2rem 1.5rem;margin-bottom:2rem}
|
|
326
|
+
.delta-head{display:flex;align-items:center;gap:1rem;margin-bottom:0.5rem}
|
|
327
|
+
.delta-title{font-weight:700;font-size:0.9rem}
|
|
328
|
+
.delta-score{font-size:0.85rem;color:var(--muted)}
|
|
329
|
+
.delta-arrow{font-weight:800;font-size:1rem}
|
|
330
|
+
.delta-stats{display:flex;gap:1.2rem;font-size:0.78rem;margin-bottom:0.5rem}
|
|
331
|
+
.delta-checks{display:flex;flex-wrap:wrap;gap:0.4rem;margin-bottom:0.5rem}
|
|
332
|
+
.delta-chip{font-size:0.7rem;padding:0.15rem 0.5rem;background:var(--card-alt);border:1px solid var(--border);border-radius:4px;white-space:nowrap}
|
|
333
|
+
.delta-fixed,.delta-new{font-size:0.75rem;color:var(--muted);margin-top:0.3rem}
|
|
297
334
|
`;
|
|
@@ -173,7 +173,7 @@ function checkSupplyChain(has, read) {
|
|
|
173
173
|
let followed = 0;
|
|
174
174
|
// Lockfile committed
|
|
175
175
|
practices++;
|
|
176
|
-
const hasLockfile = has("pnpm-lock.yaml") || has("package-lock.json") || has("yarn.lock") || has("bun.lockb") || has("pubspec.lock");
|
|
176
|
+
const hasLockfile = has("pnpm-lock.yaml") || has("package-lock.json") || has("yarn.lock") || has("bun.lockb") || has("bun.lock") || has("pubspec.lock");
|
|
177
177
|
if (hasLockfile) {
|
|
178
178
|
followed++;
|
|
179
179
|
}
|
|
@@ -117,12 +117,9 @@ export function runConfusion(cwd) {
|
|
|
117
117
|
const dirGroups = new Map();
|
|
118
118
|
for (const f of files) {
|
|
119
119
|
const dir = f.path.includes("/") ? f.path.replace(/\/[^/]+$/, "") : ".";
|
|
120
|
-
|
|
121
|
-
const pkg = f.path.match(/^(packages|apps|libs|modules|internal)\/[^/]+/)?.[0];
|
|
122
|
-
const groupKey = pkg || dir;
|
|
123
|
-
const group = dirGroups.get(groupKey) || [];
|
|
120
|
+
const group = dirGroups.get(dir) || [];
|
|
124
121
|
group.push(f);
|
|
125
|
-
dirGroups.set(
|
|
122
|
+
dirGroups.set(dir, group);
|
|
126
123
|
}
|
|
127
124
|
// Also do a global comparison but only for files in the same top-level group
|
|
128
125
|
for (const group of dirGroups.values()) {
|
|
@@ -130,11 +127,12 @@ export function runConfusion(cwd) {
|
|
|
130
127
|
for (let j = i + 1; j < group.length; j++) {
|
|
131
128
|
const a = group[i].base;
|
|
132
129
|
const b = group[j].base;
|
|
133
|
-
// Near-identical (Levenshtein โค 2, but
|
|
130
|
+
// Near-identical (Levenshtein โค 2, but require longer names for distance 2)
|
|
134
131
|
// Early exit: length diff > 2 means edit distance > 2
|
|
135
132
|
if (a !== b && a.length >= 3 && b.length >= 3 && Math.abs(a.length - b.length) <= 2) {
|
|
136
133
|
const dist = levenshtein(a, b);
|
|
137
|
-
|
|
134
|
+
const minLen = Math.min(a.length, b.length);
|
|
135
|
+
if (dist === 1 || (dist === 2 && minLen >= 5)) {
|
|
138
136
|
fileConfusability++;
|
|
139
137
|
issues.push({
|
|
140
138
|
severity: "warning",
|
|
@@ -179,8 +177,8 @@ export function runConfusion(cwd) {
|
|
|
179
177
|
rule: "generic-name",
|
|
180
178
|
});
|
|
181
179
|
}
|
|
182
|
-
// Match
|
|
183
|
-
const varMatch = line.match(/^
|
|
180
|
+
// Match exported variable assignments with generic names (local vars are fine)
|
|
181
|
+
const varMatch = line.match(/^export\s+(?:const|let)\s+(\w+)\s*=/);
|
|
184
182
|
if (varMatch && GENERIC_NAMES.has(varMatch[1].toLowerCase()) && varMatch[1].length <= 6) {
|
|
185
183
|
genericNames++;
|
|
186
184
|
issues.push({
|
|
@@ -193,7 +191,7 @@ export function runConfusion(cwd) {
|
|
|
193
191
|
}
|
|
194
192
|
}
|
|
195
193
|
}
|
|
196
|
-
// โโ 3. Export name collisions โโ
|
|
194
|
+
// โโ 3. Export name collisions (within same package scope) โโ
|
|
197
195
|
const exportMap = new Map();
|
|
198
196
|
for (const f of files) {
|
|
199
197
|
for (const exp of f.exports) {
|
|
@@ -204,13 +202,26 @@ export function runConfusion(cwd) {
|
|
|
204
202
|
}
|
|
205
203
|
for (const [name, paths] of exportMap) {
|
|
206
204
|
if (paths.length > 1) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
205
|
+
// In monorepos, only flag collisions within the same package
|
|
206
|
+
const pkgOf = (p) => p.match(/^(packages|apps|libs|modules|internal)\/[^/]+/)?.[0] || ".";
|
|
207
|
+
const pkgGroups = new Map();
|
|
208
|
+
for (const p of paths) {
|
|
209
|
+
const pkg = pkgOf(p);
|
|
210
|
+
const arr = pkgGroups.get(pkg) || [];
|
|
211
|
+
arr.push(p);
|
|
212
|
+
pkgGroups.set(pkg, arr);
|
|
213
|
+
}
|
|
214
|
+
for (const groupPaths of pkgGroups.values()) {
|
|
215
|
+
if (groupPaths.length > 1) {
|
|
216
|
+
exportCollisions++;
|
|
217
|
+
issues.push({
|
|
218
|
+
severity: "error",
|
|
219
|
+
message: `Export collision: "${name}" exported from ${groupPaths.length} files โ LLMs may reference the wrong one`,
|
|
220
|
+
file: groupPaths.join(", "),
|
|
221
|
+
rule: "export-collision",
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
214
225
|
}
|
|
215
226
|
}
|
|
216
227
|
// โโ 4. Ambiguous abbreviations in filenames and exports โโ
|
package/dist/runners/lint.js
CHANGED
|
@@ -13,7 +13,9 @@ export function runLint(cwd, stack, workspace) {
|
|
|
13
13
|
// Determine the target path for linting
|
|
14
14
|
// Monorepos with root config: lint "." (biome/eslint will find all files)
|
|
15
15
|
// Single-package: lint "src/"
|
|
16
|
-
const lintTarget = workspace?.isMonorepo ? "."
|
|
16
|
+
const lintTarget = workspace?.isMonorepo ? "."
|
|
17
|
+
: existsSync(join(cwd, "src")) ? "src/"
|
|
18
|
+
: ".";
|
|
17
19
|
if (stack.linter === "biome") {
|
|
18
20
|
const { stdout } = run(`npx biome check ${lintTarget} --reporter=json 2>/dev/null || true`, cwd);
|
|
19
21
|
try {
|
|
@@ -23,6 +25,9 @@ export function runLint(cwd, stack, workspace) {
|
|
|
23
25
|
// biome path can be string or {file: "..."} depending on version
|
|
24
26
|
const rawPath = d.location?.path;
|
|
25
27
|
const file = typeof rawPath === "string" ? rawPath : rawPath?.file || undefined;
|
|
28
|
+
// Skip generated output files
|
|
29
|
+
if (file && (file.includes(".vibe-check/") || file.includes("node_modules/")))
|
|
30
|
+
continue;
|
|
26
31
|
issues.push({
|
|
27
32
|
severity: d.severity === "error" ? "error" : d.severity === "warning" ? "warning" : "info",
|
|
28
33
|
message: d.description || d.message || "lint issue",
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
/** Code standards check โ naming conventions, anti-patterns, config hygiene. */
|
|
2
|
-
import type { CheckResult, StackInfo } from "../types.js";
|
|
3
|
-
export declare function runStandards(cwd: string, stack: StackInfo): CheckResult;
|
|
2
|
+
import type { CheckResult, StackInfo, WorkspaceInfo } from "../types.js";
|
|
3
|
+
export declare function runStandards(cwd: string, stack: StackInfo, workspace?: WorkspaceInfo): CheckResult;
|
|
@@ -42,7 +42,7 @@ const CODE_SMELLS = [
|
|
|
42
42
|
message: "Large magic number โ consider a named constant",
|
|
43
43
|
},
|
|
44
44
|
];
|
|
45
|
-
export function runStandards(cwd, stack) {
|
|
45
|
+
export function runStandards(cwd, stack, workspace) {
|
|
46
46
|
const start = Date.now();
|
|
47
47
|
const issues = [];
|
|
48
48
|
// Detect CLI projects โ console.log is intentional in CLI tools
|
|
@@ -59,15 +59,22 @@ export function runStandards(cwd, stack) {
|
|
|
59
59
|
const files = getProductionFiles(cwd);
|
|
60
60
|
// โโ File naming conventions โโ
|
|
61
61
|
let namingViolations = 0;
|
|
62
|
+
// Detect dominant convention for tsx/jsx files before flagging
|
|
63
|
+
const tsxFiles = files.filter((f) => {
|
|
64
|
+
const ext = extname(basename(f.path));
|
|
65
|
+
return ext === ".tsx" || ext === ".jsx";
|
|
66
|
+
});
|
|
67
|
+
const pascalCount = tsxFiles.filter((f) => /^[A-Z]/.test(basename(f.path).replace(extname(basename(f.path)), ""))).length;
|
|
68
|
+
const usesKebabConvention = tsxFiles.length >= 3 && pascalCount < tsxFiles.length / 2;
|
|
62
69
|
for (const f of files) {
|
|
63
70
|
const name = basename(f.path);
|
|
64
71
|
const ext = extname(name);
|
|
65
72
|
const base = name.replace(ext, "");
|
|
66
|
-
// React components should be PascalCase
|
|
73
|
+
// React components should be PascalCase โ but only flag if PascalCase is the project convention
|
|
67
74
|
if ((ext === ".tsx" || ext === ".jsx") && /^[A-Z]/.test(base)) {
|
|
68
75
|
// PascalCase component file โ correct
|
|
69
76
|
}
|
|
70
|
-
else if ((ext === ".tsx" || ext === ".jsx") && /^[a-z]/.test(base) && base !== "main" && base !== "index") {
|
|
77
|
+
else if (!usesKebabConvention && (ext === ".tsx" || ext === ".jsx") && /^[a-z]/.test(base) && base !== "main" && base !== "index") {
|
|
71
78
|
// lowercase tsx file that's not main/index โ check if it exports a component
|
|
72
79
|
if (/export (default )?(function|const) [A-Z]/.test(f.content)) {
|
|
73
80
|
namingViolations++;
|
|
@@ -140,6 +147,12 @@ export function runStandards(cwd, stack) {
|
|
|
140
147
|
// tsconfig maturity
|
|
141
148
|
if (stack.language === "typescript") {
|
|
142
149
|
const tsconfigPaths = ["tsconfig.json", "tsconfig.app.json", "tsconfig.base.json"];
|
|
150
|
+
// In monorepos, also check each workspace package's tsconfig
|
|
151
|
+
if (workspace?.isMonorepo) {
|
|
152
|
+
for (const pkg of workspace.packages) {
|
|
153
|
+
tsconfigPaths.push(join(pkg.path, "tsconfig.json"));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
143
156
|
let strictFound = false;
|
|
144
157
|
let compilerOpts = {};
|
|
145
158
|
for (const p of tsconfigPaths) {
|
|
@@ -211,8 +224,8 @@ export function runStandards(cwd, stack) {
|
|
|
211
224
|
const totalFiles = files.length || 1;
|
|
212
225
|
const errorPenalty = Math.min(40, (errors / totalFiles) * 150);
|
|
213
226
|
const warningPenalty = Math.min(30, (warnings / totalFiles) * 80);
|
|
214
|
-
// fileSizePenalty is already exponential โ normalize by file count, cap at
|
|
215
|
-
const largePenalty = Math.min(
|
|
227
|
+
// fileSizePenalty is already exponential โ normalize by file count, cap at 80
|
|
228
|
+
const largePenalty = Math.min(80, (fileSizePenalty / totalFiles) * 3);
|
|
216
229
|
const score = Math.max(0, Math.min(100, Math.round(100 - errorPenalty - warningPenalty - largePenalty)));
|
|
217
230
|
return {
|
|
218
231
|
name: "standards",
|
|
@@ -61,7 +61,7 @@ export function runStructure(cwd, stack, workspace) {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
// Check for lockfile โ in monorepos, lockfiles may be in packages
|
|
64
|
-
const lockfiles = isDart ? ["pubspec.lock"] : ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"];
|
|
64
|
+
const lockfiles = isDart ? ["pubspec.lock"] : ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb", "bun.lock"];
|
|
65
65
|
let hasLock = lockfiles.some((f) => existsSync(join(cwd, f)));
|
|
66
66
|
if (!hasLock && workspace?.isMonorepo) {
|
|
67
67
|
hasLock = workspace.packages.some((p) => lockfiles.some((f) => existsSync(join(cwd, p.path, f))));
|
package/dist/runners/styling.js
CHANGED
|
@@ -33,7 +33,7 @@ function tryStylelint(cwd) {
|
|
|
33
33
|
if (!hasStandard)
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
36
|
-
const results = runJSON("npx stylelint --formatter json \"
|
|
36
|
+
const results = runJSON("npx stylelint --formatter json \"**/*.{css,scss}\" --ignore-pattern node_modules 2>/dev/null || true", cwd, 30_000);
|
|
37
37
|
if (!results || !Array.isArray(results))
|
|
38
38
|
return null;
|
|
39
39
|
const issues = [];
|
package/dist/runners/testing.js
CHANGED
|
@@ -47,7 +47,9 @@ function countPatterns(content) {
|
|
|
47
47
|
// โโ File discovery โโ
|
|
48
48
|
function findTestFiles(cwd, srcRoots) {
|
|
49
49
|
const files = [];
|
|
50
|
-
const dirs = srcRoots ? [...srcRoots, "e2e", "playwright"]
|
|
50
|
+
const dirs = srcRoots ? [...srcRoots, "e2e", "playwright"]
|
|
51
|
+
: existsSync(join(cwd, "src")) ? ["src", "web/src", "lib", "test", "tests", "__tests__", "e2e", "playwright"]
|
|
52
|
+
: [".", "test", "tests", "__tests__", "e2e", "playwright"];
|
|
51
53
|
const seen = new Set();
|
|
52
54
|
for (const dir of dirs) {
|
|
53
55
|
const full = join(cwd, dir);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibecodeqa/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Code health scanner for the AI coding era.
|
|
3
|
+
"version": "0.44.0",
|
|
4
|
+
"description": "Code health scanner for the AI coding era. 34 checks, zero config, full report.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"vcqa": "./dist/cli.js",
|