@vibecodeqa/cli 0.22.0 → 0.24.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 +122 -102
- package/dist/report/html.js +13 -5
- package/dist/report/pages.d.ts +1 -0
- package/dist/report/pages.js +67 -0
- package/dist/report/styles.d.ts +1 -1
- package/dist/report/styles.js +15 -0
- package/dist/runners/architecture.js +1 -2
- package/dist/runners/best-practices.js +93 -21
- package/dist/runners/confusion.js +1 -2
- package/dist/runners/context.js +1 -2
- package/dist/runners/standards.js +9 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -31,17 +31,22 @@ import { computeTrend, formatTrend } from "./trend.js";
|
|
|
31
31
|
import { gradeFromScore } from "./types.js";
|
|
32
32
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
33
33
|
const VERSION = pkg.version;
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
34
|
+
function parseFlags() {
|
|
35
|
+
const args = process.argv.slice(2);
|
|
36
|
+
const flags = new Set(args.filter((a) => a.startsWith("--")));
|
|
37
|
+
const cwd = resolve(args.find((a) => !a.startsWith("--")) || ".");
|
|
38
|
+
return {
|
|
39
|
+
cwd,
|
|
40
|
+
outputDir: join(cwd, ".vibe-check"),
|
|
41
|
+
jsonOnly: flags.has("--json"),
|
|
42
|
+
ciMode: flags.has("--ci"),
|
|
43
|
+
skipTests: flags.has("--skip-tests"),
|
|
44
|
+
watchMode: flags.has("--watch"),
|
|
45
|
+
badgeMode: flags.has("--badge"),
|
|
46
|
+
sarifMode: flags.has("--sarif"),
|
|
47
|
+
uploadMode: flags.has("--upload"),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
45
50
|
function color(grade) {
|
|
46
51
|
if (grade === "A")
|
|
47
52
|
return "\x1b[32m";
|
|
@@ -49,23 +54,7 @@ function color(grade) {
|
|
|
49
54
|
return "\x1b[33m";
|
|
50
55
|
return "\x1b[31m";
|
|
51
56
|
}
|
|
52
|
-
|
|
53
|
-
const start = Date.now();
|
|
54
|
-
if (!jsonOnly) {
|
|
55
|
-
console.log("");
|
|
56
|
-
console.log(` \x1b[1m\x1b[38;5;141mvcqa\x1b[0m v${VERSION}`);
|
|
57
|
-
console.log(` \x1b[2m${cwd}\x1b[0m`);
|
|
58
|
-
console.log("");
|
|
59
|
-
}
|
|
60
|
-
const stack = detectStack(cwd);
|
|
61
|
-
if (!jsonOnly) {
|
|
62
|
-
const parts = [stack.language, stack.framework, stack.bundler, stack.testRunner, stack.linter, stack.packageManager].filter((v) => v !== "none" && v !== "unknown");
|
|
63
|
-
console.log(` stack: ${parts.join(" + ")}`);
|
|
64
|
-
console.log("");
|
|
65
|
-
}
|
|
66
|
-
const checks = [];
|
|
67
|
-
const isDart = stack.language === "dart";
|
|
68
|
-
// All runners grouped by category
|
|
57
|
+
function runChecks(cwd, stack, skipTests, isDart, jsonOnly) {
|
|
69
58
|
const runners = [
|
|
70
59
|
// Foundations
|
|
71
60
|
{ name: "structure", fn: () => runStructure(cwd, stack) },
|
|
@@ -97,6 +86,7 @@ async function main() {
|
|
|
97
86
|
{ name: "doc-coherence", fn: () => runDocCoherence(cwd) },
|
|
98
87
|
{ name: "code-coherence", fn: () => runCodeCoherence(cwd) },
|
|
99
88
|
];
|
|
89
|
+
const checks = [];
|
|
100
90
|
for (const runner of runners) {
|
|
101
91
|
if (!jsonOnly)
|
|
102
92
|
process.stdout.write(` ${runner.name.padEnd(14)}`);
|
|
@@ -113,20 +103,9 @@ async function main() {
|
|
|
113
103
|
console.log(`${c}${label.padEnd(5)}${scoreStr}\x1b[0m \x1b[2m${result.duration}ms\x1b[0m${issueStr}`);
|
|
114
104
|
}
|
|
115
105
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const totalIssues = checks.reduce((s, c) => s + c.issues.length, 0);
|
|
120
|
-
const report = {
|
|
121
|
-
version: VERSION,
|
|
122
|
-
timestamp: new Date().toISOString(),
|
|
123
|
-
score,
|
|
124
|
-
grade,
|
|
125
|
-
checks,
|
|
126
|
-
meta: { cwd, node: process.version, duration, stack, ...detectRepoUrl(cwd) },
|
|
127
|
-
};
|
|
128
|
-
// Trend comparison (read previous report before overwriting)
|
|
129
|
-
const trend = computeTrend(report, outputDir);
|
|
106
|
+
return checks;
|
|
107
|
+
}
|
|
108
|
+
async function writeOutputs(report, outputDir, flags) {
|
|
130
109
|
if (!existsSync(outputDir))
|
|
131
110
|
mkdirSync(outputDir, { recursive: true });
|
|
132
111
|
// Save to history before overwriting current report
|
|
@@ -159,58 +138,127 @@ async function main() {
|
|
|
159
138
|
writeFileSync(join(reportDir, filename), html);
|
|
160
139
|
}
|
|
161
140
|
// Badge SVG
|
|
162
|
-
if (badgeMode) {
|
|
141
|
+
if (flags.badgeMode) {
|
|
163
142
|
const { buildBadge } = await import("./report/svg.js");
|
|
164
|
-
const badgeSvg = buildBadge(score, grade);
|
|
143
|
+
const badgeSvg = buildBadge(report.score, report.grade);
|
|
165
144
|
writeFileSync(join(outputDir, "badge.svg"), badgeSvg);
|
|
166
145
|
}
|
|
167
146
|
// SARIF output for GitHub Code Scanning
|
|
168
|
-
if (sarifMode) {
|
|
147
|
+
if (flags.sarifMode) {
|
|
169
148
|
const { generateSARIF } = await import("./report/sarif.js");
|
|
170
149
|
writeFileSync(join(outputDir, "report.sarif"), generateSARIF(report));
|
|
171
150
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const res = await fetch("https://api.vibecodeqa.online/api/reports", {
|
|
178
|
-
method: "POST",
|
|
179
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
180
|
-
body: JSON.stringify({ repo, report }),
|
|
181
|
-
});
|
|
182
|
-
if (res.ok) {
|
|
183
|
-
const data = (await res.json());
|
|
184
|
-
if (!jsonOnly)
|
|
185
|
-
console.log(` \x1b[32m\u2713 Uploaded to dashboard\x1b[0m \x1b[2m(${data.totalReports || 1} reports)\x1b[0m`);
|
|
186
|
-
}
|
|
187
|
-
else if (!jsonOnly) {
|
|
188
|
-
console.log(` \x1b[33m\u26a0 Upload failed: ${res.status}\x1b[0m \x1b[2m(set VCQA_TOKEN env var)\x1b[0m`);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
catch {
|
|
192
|
-
if (!jsonOnly)
|
|
193
|
-
console.log(` \x1b[33m\u26a0 Upload failed (network error)\x1b[0m`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
if (jsonOnly) {
|
|
151
|
+
}
|
|
152
|
+
function printResults(report, trend, flags, outputDir) {
|
|
153
|
+
const { score, grade, checks } = report;
|
|
154
|
+
const totalIssues = checks.reduce((s, c) => s + c.issues.length, 0);
|
|
155
|
+
if (flags.jsonOnly) {
|
|
197
156
|
console.log(JSON.stringify(report));
|
|
198
157
|
}
|
|
199
158
|
else {
|
|
200
159
|
const gc = color(grade);
|
|
201
160
|
console.log("");
|
|
202
|
-
console.log(` ${gc}\x1b[1m${grade}\x1b[0m ${gc}${score}/100\x1b[0m \x1b[2m${checks.length} checks · ${totalIssues} issues · ${duration}ms\x1b[0m`);
|
|
161
|
+
console.log(` ${gc}\x1b[1m${grade}\x1b[0m ${gc}${score}/100\x1b[0m \x1b[2m${checks.length} checks · ${totalIssues} issues · ${report.meta.duration}ms\x1b[0m`);
|
|
203
162
|
if (trend)
|
|
204
163
|
console.log(formatTrend(trend));
|
|
205
164
|
console.log("");
|
|
206
165
|
console.log(` \x1b[2mReport: ${join(outputDir, "report/index.html")}\x1b[0m`);
|
|
207
166
|
console.log(` \x1b[2mJSON: ${join(outputDir, "report.json")}\x1b[0m`);
|
|
208
|
-
if (badgeMode)
|
|
167
|
+
if (flags.badgeMode)
|
|
209
168
|
console.log(` \x1b[2mBadge: ${join(outputDir, "badge.svg")}\x1b[0m`);
|
|
210
|
-
if (sarifMode)
|
|
169
|
+
if (flags.sarifMode)
|
|
211
170
|
console.log(` \x1b[2mSARIF: ${join(outputDir, "report.sarif")}\x1b[0m`);
|
|
212
171
|
console.log("");
|
|
213
172
|
}
|
|
173
|
+
}
|
|
174
|
+
async function handleUpload(report, cwd, jsonOnly) {
|
|
175
|
+
const repo = report.meta.repoUrl?.replace(/^https:\/\/github\.com\//, "") || cwd.split("/").pop() || "project";
|
|
176
|
+
const token = process.env.VCQA_TOKEN || "";
|
|
177
|
+
try {
|
|
178
|
+
const res = await fetch("https://api.vibecodeqa.online/api/reports", {
|
|
179
|
+
method: "POST",
|
|
180
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
181
|
+
body: JSON.stringify({ repo, report }),
|
|
182
|
+
});
|
|
183
|
+
if (res.ok) {
|
|
184
|
+
const data = (await res.json());
|
|
185
|
+
if (!jsonOnly)
|
|
186
|
+
console.log(` \x1b[32m\u2713 Uploaded to dashboard\x1b[0m \x1b[2m(${data.totalReports || 1} reports)\x1b[0m`);
|
|
187
|
+
}
|
|
188
|
+
else if (!jsonOnly) {
|
|
189
|
+
console.log(` \x1b[33m\u26a0 Upload failed: ${res.status}\x1b[0m \x1b[2m(set VCQA_TOKEN env var)\x1b[0m`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
if (!jsonOnly)
|
|
194
|
+
console.log(` \x1b[33m\u26a0 Upload failed (network error)\x1b[0m`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function startWatch(cwd) {
|
|
198
|
+
const { watch } = await import("node:fs");
|
|
199
|
+
const srcDirs = ["src", "web/src"].map((d) => join(cwd, d)).filter((d) => existsSync(d));
|
|
200
|
+
if (srcDirs.length === 0) {
|
|
201
|
+
console.log(" \x1b[31mNo src/ directory to watch\x1b[0m");
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
console.log(" \x1b[2mWatching for changes... (Ctrl+C to stop)\x1b[0m");
|
|
205
|
+
console.log("");
|
|
206
|
+
let debounce = null;
|
|
207
|
+
let running = false;
|
|
208
|
+
for (const dir of srcDirs) {
|
|
209
|
+
watch(dir, { recursive: true }, (_event, filename) => {
|
|
210
|
+
if (!filename || filename.includes("node_modules") || filename.includes(".vibe-check"))
|
|
211
|
+
return;
|
|
212
|
+
if (running)
|
|
213
|
+
return;
|
|
214
|
+
if (debounce)
|
|
215
|
+
clearTimeout(debounce);
|
|
216
|
+
debounce = setTimeout(async () => {
|
|
217
|
+
running = true;
|
|
218
|
+
console.log(` \x1b[2mChanged: ${filename} — re-scanning...\x1b[0m`);
|
|
219
|
+
await main().catch(() => { });
|
|
220
|
+
running = false;
|
|
221
|
+
}, 500);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
// Keep process alive
|
|
225
|
+
await new Promise(() => { });
|
|
226
|
+
}
|
|
227
|
+
async function main() {
|
|
228
|
+
const flags = parseFlags();
|
|
229
|
+
const { cwd, outputDir, jsonOnly, ciMode, skipTests, watchMode } = flags;
|
|
230
|
+
const start = Date.now();
|
|
231
|
+
if (!jsonOnly) {
|
|
232
|
+
console.log("");
|
|
233
|
+
console.log(` \x1b[1m\x1b[38;5;141mvcqa\x1b[0m v${VERSION}`);
|
|
234
|
+
console.log(` \x1b[2m${cwd}\x1b[0m`);
|
|
235
|
+
console.log("");
|
|
236
|
+
}
|
|
237
|
+
const stack = detectStack(cwd);
|
|
238
|
+
if (!jsonOnly) {
|
|
239
|
+
const parts = [stack.language, stack.framework, stack.bundler, stack.testRunner, stack.linter, stack.packageManager].filter((v) => v !== "none" && v !== "unknown");
|
|
240
|
+
console.log(` stack: ${parts.join(" + ")}`);
|
|
241
|
+
console.log("");
|
|
242
|
+
}
|
|
243
|
+
const isDart = stack.language === "dart";
|
|
244
|
+
const checks = runChecks(cwd, stack, skipTests, isDart, jsonOnly);
|
|
245
|
+
const score = computeScore(checks);
|
|
246
|
+
const grade = gradeFromScore(score);
|
|
247
|
+
const duration = Date.now() - start;
|
|
248
|
+
const report = {
|
|
249
|
+
version: VERSION,
|
|
250
|
+
timestamp: new Date().toISOString(),
|
|
251
|
+
score,
|
|
252
|
+
grade,
|
|
253
|
+
checks,
|
|
254
|
+
meta: { cwd, node: process.version, duration, stack, ...detectRepoUrl(cwd) },
|
|
255
|
+
};
|
|
256
|
+
const trend = computeTrend(report, outputDir);
|
|
257
|
+
await writeOutputs(report, outputDir, flags);
|
|
258
|
+
printResults(report, trend, flags, outputDir);
|
|
259
|
+
if (flags.uploadMode) {
|
|
260
|
+
await handleUpload(report, cwd, jsonOnly);
|
|
261
|
+
}
|
|
214
262
|
if (ciMode && score < 60) {
|
|
215
263
|
process.exit(1);
|
|
216
264
|
}
|
|
@@ -224,36 +272,8 @@ async function main() {
|
|
|
224
272
|
/* failed to open browser */
|
|
225
273
|
}
|
|
226
274
|
}
|
|
227
|
-
// Watch mode — re-run on file changes
|
|
228
275
|
if (watchMode) {
|
|
229
|
-
|
|
230
|
-
const srcDirs = ["src", "web/src"].map((d) => join(cwd, d)).filter((d) => existsSync(d));
|
|
231
|
-
if (srcDirs.length === 0) {
|
|
232
|
-
console.log(" \x1b[31mNo src/ directory to watch\x1b[0m");
|
|
233
|
-
process.exit(1);
|
|
234
|
-
}
|
|
235
|
-
console.log(" \x1b[2mWatching for changes... (Ctrl+C to stop)\x1b[0m");
|
|
236
|
-
console.log("");
|
|
237
|
-
let debounce = null;
|
|
238
|
-
let running = false;
|
|
239
|
-
for (const dir of srcDirs) {
|
|
240
|
-
watch(dir, { recursive: true }, (_event, filename) => {
|
|
241
|
-
if (!filename || filename.includes("node_modules") || filename.includes(".vibe-check"))
|
|
242
|
-
return;
|
|
243
|
-
if (running)
|
|
244
|
-
return; // prevent concurrent re-runs (M5)
|
|
245
|
-
if (debounce)
|
|
246
|
-
clearTimeout(debounce);
|
|
247
|
-
debounce = setTimeout(async () => {
|
|
248
|
-
running = true;
|
|
249
|
-
console.log(` \x1b[2mChanged: ${filename} — re-scanning...\x1b[0m`);
|
|
250
|
-
await main().catch(() => { });
|
|
251
|
-
running = false;
|
|
252
|
-
}, 500);
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
// Keep process alive
|
|
256
|
-
await new Promise(() => { });
|
|
276
|
+
await startWatch(cwd);
|
|
257
277
|
}
|
|
258
278
|
}
|
|
259
279
|
main().catch((err) => {
|
package/dist/report/html.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { getCheckMeta } from "../check-meta.js";
|
|
14
14
|
import { det, e, fileLink, gc } from "./components.js";
|
|
15
15
|
import { FAVICON_SVG } from "./favicon.js";
|
|
16
|
-
import { categoryPage, filesPage, issuesPage, overviewPage } from "./pages.js";
|
|
16
|
+
import { categoryPage, filesPage, issuesPage, overviewPage, trendsPage } from "./pages.js";
|
|
17
17
|
import { CSS } from "./styles.js";
|
|
18
18
|
export const GROUPS = [
|
|
19
19
|
{ id: "foundations", label: "Foundations", file: "foundations.html", checks: ["structure", "lint", "types", "type-safety", "standards"] },
|
|
@@ -93,7 +93,7 @@ export function generatePages(report, historyDir) {
|
|
|
93
93
|
const badge = premium
|
|
94
94
|
? `<span style="color:#6366f1">PRO</span>`
|
|
95
95
|
: `<span style="color:${sk ? "#555" : gc(c.grade)}">${sk ? "\u2014" : c.grade} ${sk ? "" : c.score}</span>`;
|
|
96
|
-
return `<a class="side-check" onclick="
|
|
96
|
+
return `<a class="side-check" onclick="let t=document.querySelector('[data-sub=\\'${cs.id}-${c.name}\\']');if(t)sub(t,'${cs.id}')" title="${e(meta.label)}">${badge} ${e(meta.label)}</a>`;
|
|
97
97
|
})
|
|
98
98
|
.join("") +
|
|
99
99
|
`</div>` +
|
|
@@ -121,6 +121,13 @@ export function generatePages(report, historyDir) {
|
|
|
121
121
|
`</div>` +
|
|
122
122
|
sidebarViews(totalIssues, fileIssues.size);
|
|
123
123
|
pages.set("files.html", w("files", filesSidebar, filesPage(topFiles, fileIssues, fl)));
|
|
124
|
+
// ── Trends page ──
|
|
125
|
+
const trendsSidebar = sidebarScore(report) +
|
|
126
|
+
`<div class="side-section"><div class="side-cat-title">History</div>` +
|
|
127
|
+
`<div class="side-stat"><span style="color:var(--text)">${historyDir ? "30" : "0"}</span> scans stored</div>` +
|
|
128
|
+
`</div>` +
|
|
129
|
+
sidebarViews(totalIssues, fileIssues.size);
|
|
130
|
+
pages.set("trends.html", w("trends", trendsSidebar, trendsPage(historyDir)));
|
|
124
131
|
return pages;
|
|
125
132
|
}
|
|
126
133
|
export function generateHTML(report, historyDir) {
|
|
@@ -138,6 +145,7 @@ function wrap(proj, currentId, report, totalIssues, sidebar, content) {
|
|
|
138
145
|
const navItems = [
|
|
139
146
|
{ id: "overview", label: "Overview", file: "index.html" },
|
|
140
147
|
...GROUPS.map((g) => ({ id: g.id, label: g.label, file: g.file })),
|
|
148
|
+
{ id: "trends", label: "Trends", file: "trends.html" },
|
|
141
149
|
{ id: "issues", label: `Issues (${totalIssues})`, file: "issues.html" },
|
|
142
150
|
{ id: "files", label: "Files", file: "files.html" },
|
|
143
151
|
];
|
|
@@ -178,10 +186,10 @@ function sub(el,cat){
|
|
|
178
186
|
document.querySelectorAll('.sp').forEach(s=>{s.classList.toggle('active',s.dataset.sub===id)});
|
|
179
187
|
}
|
|
180
188
|
document.addEventListener('click',function(ev){
|
|
181
|
-
|
|
189
|
+
const btn=ev.target.closest('.cp-btn');
|
|
182
190
|
if(!btn)return;
|
|
183
|
-
|
|
184
|
-
try{navigator.clipboard.writeText(text)}catch(e){
|
|
191
|
+
const text=btn.dataset.prompt||'';
|
|
192
|
+
try{navigator.clipboard.writeText(text)}catch(e){const ta=document.createElement('textarea');ta.value=text;ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta)}
|
|
185
193
|
btn.textContent='\\u2713';setTimeout(function(){btn.textContent='\\ud83d\\udccb'},1000);
|
|
186
194
|
});
|
|
187
195
|
</script>
|
package/dist/report/pages.d.ts
CHANGED
package/dist/report/pages.js
CHANGED
|
@@ -220,3 +220,70 @@ export function filesPage(topFiles, fileIssues, fl) {
|
|
|
220
220
|
}, new Set()).size} checks.</p>
|
|
221
221
|
${heatmapRows}`;
|
|
222
222
|
}
|
|
223
|
+
// ── Trends page ──────────────────────────────────────────
|
|
224
|
+
export function trendsPage(historyDir) {
|
|
225
|
+
if (!historyDir) {
|
|
226
|
+
return `<h2>Trends</h2><p class="muted">No history data yet. Run the scanner multiple times to see trends.</p>`;
|
|
227
|
+
}
|
|
228
|
+
const history = loadHistory(historyDir);
|
|
229
|
+
if (history.length < 2) {
|
|
230
|
+
return `<h2>Trends</h2><p class="muted">Need at least 2 scans to show trends. Run the scanner again.</p>`;
|
|
231
|
+
}
|
|
232
|
+
// Overall score timeline (large)
|
|
233
|
+
const overallChart = buildTimeline(history.map((h) => ({ score: h.score, timestamp: h.timestamp })), { width: 700, height: 150 });
|
|
234
|
+
// Collect all check names from latest entry
|
|
235
|
+
const latest = history[history.length - 1];
|
|
236
|
+
const checkNames = [...latest.checkScores.keys()];
|
|
237
|
+
// Per-check mini charts
|
|
238
|
+
const checkCharts = checkNames
|
|
239
|
+
.map((name) => {
|
|
240
|
+
const data = history
|
|
241
|
+
.map((h) => ({ score: h.checkScores.get(name) ?? 0, timestamp: h.timestamp }))
|
|
242
|
+
.filter((d) => d.score > 0);
|
|
243
|
+
if (data.length < 2)
|
|
244
|
+
return "";
|
|
245
|
+
const current = data[data.length - 1].score;
|
|
246
|
+
const prev = data[data.length - 2].score;
|
|
247
|
+
const delta = current - prev;
|
|
248
|
+
const deltaStr = delta > 0 ? `<span style="color:var(--pass)">+${delta}</span>` : delta < 0 ? `<span style="color:var(--fail)">${delta}</span>` : `<span class="muted">=</span>`;
|
|
249
|
+
const color = current >= 90 ? "var(--pass)" : current >= 75 ? "#84cc16" : current >= 60 ? "var(--warn)" : "var(--fail)";
|
|
250
|
+
const chart = buildTimeline(data, { width: 300, height: 60 });
|
|
251
|
+
return `<div class="trend-card">
|
|
252
|
+
<div class="trend-header"><span class="trend-name">${name}</span><span class="trend-score" style="color:${color}">${current}</span>${deltaStr}</div>
|
|
253
|
+
<div class="trend-chart">${chart}</div>
|
|
254
|
+
</div>`;
|
|
255
|
+
})
|
|
256
|
+
.filter(Boolean)
|
|
257
|
+
.join("");
|
|
258
|
+
// Score delta table
|
|
259
|
+
const deltaRows = checkNames
|
|
260
|
+
.map((name) => {
|
|
261
|
+
const scores = history.map((h) => h.checkScores.get(name) ?? 0);
|
|
262
|
+
const first = scores.find((s) => s > 0) ?? 0;
|
|
263
|
+
const last = scores[scores.length - 1];
|
|
264
|
+
const delta = last - first;
|
|
265
|
+
if (first === 0 && last === 0)
|
|
266
|
+
return "";
|
|
267
|
+
return { name, first, last, delta };
|
|
268
|
+
})
|
|
269
|
+
.filter(Boolean)
|
|
270
|
+
.sort((a, b) => b.delta - a.delta);
|
|
271
|
+
const deltaTable = deltaRows
|
|
272
|
+
.map((r) => {
|
|
273
|
+
const clr = r.delta > 0 ? "var(--pass)" : r.delta < 0 ? "var(--fail)" : "var(--muted)";
|
|
274
|
+
return `<div class="trend-row"><span class="trend-row-name">${r.name}</span><span class="trend-row-val">${r.first}</span><span class="trend-row-arrow">\u2192</span><span class="trend-row-val">${r.last}</span><span class="trend-row-delta" style="color:${clr}">${r.delta > 0 ? "+" : ""}${r.delta}</span></div>`;
|
|
275
|
+
})
|
|
276
|
+
.join("");
|
|
277
|
+
return `
|
|
278
|
+
<h2>Trends</h2>
|
|
279
|
+
<p class="muted" style="font-size:0.78rem;margin-bottom:1.5rem">${history.length} scans from ${history[0].timestamp.split("T")[0]} to ${latest.timestamp.split("T")[0]}</p>
|
|
280
|
+
|
|
281
|
+
<h3>Overall Score</h3>
|
|
282
|
+
<div class="timeline">${overallChart}</div>
|
|
283
|
+
|
|
284
|
+
<h3 style="margin-top:2rem">Score Changes (first \u2192 latest)</h3>
|
|
285
|
+
<div class="trend-table">${deltaTable}</div>
|
|
286
|
+
|
|
287
|
+
<h3 style="margin-top:2rem">Per-Check Trends</h3>
|
|
288
|
+
<div class="trend-grid">${checkCharts}</div>`;
|
|
289
|
+
}
|
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}\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:#0c0c0fdd;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:1rem;flex-shrink:0;text-decoration:none;color:var(--text)}\n.logo span{color:var(--accent)}\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:#0c0c0f;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:#444;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(--text);font-weight:700;cursor:pointer;text-decoration:none;font-size:0.72rem}\n.side-cat:hover{background:#14141a}\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 Category pages \u2500\u2500 */\n.cat-head{margin-bottom:0.3rem}\n.bar2{height:4px;background:var(--card);border-radius:2px;margin-bottom:1rem;overflow:hidden}\n.bf2{height:100%;border-radius:2px}\n.sub-nav{display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:1rem;flex-wrap:wrap}\n.sn{padding:0.5rem 0.8rem;font-size:0.75rem;color:var(--muted);cursor:pointer;border-bottom:2px solid transparent}\n.sn:hover{color:var(--text)}\n.sn.active{color:var(--text);border-bottom-color:var(--accent)}\n.sp{display:none}.sp.active{display:block}\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:#0d0d12;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:#555;font-size:0.55rem}\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:#555;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:#555;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.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.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";
|
|
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}\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:#0c0c0fdd;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:1rem;flex-shrink:0;text-decoration:none;color:var(--text)}\n.logo span{color:var(--accent)}\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:#0c0c0f;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:#444;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(--text);font-weight:700;cursor:pointer;text-decoration:none;font-size:0.72rem}\n.side-cat:hover{background:#14141a}\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 Category pages \u2500\u2500 */\n.cat-head{margin-bottom:0.3rem}\n.bar2{height:4px;background:var(--card);border-radius:2px;margin-bottom:1rem;overflow:hidden}\n.bf2{height:100%;border-radius:2px}\n.sub-nav{display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:1rem;flex-wrap:wrap}\n.sn{padding:0.5rem 0.8rem;font-size:0.75rem;color:var(--muted);cursor:pointer;border-bottom:2px solid transparent}\n.sn:hover{color:var(--text)}\n.sn.active{color:var(--text);border-bottom-color:var(--accent)}\n.sp{display:none}.sp.active{display:block}\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:#0d0d12;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:#555;font-size:0.55rem}\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:#555;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:#555;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.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";
|
package/dist/report/styles.js
CHANGED
|
@@ -152,6 +152,21 @@ h3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:
|
|
|
152
152
|
.pro-cta{color:#6366f1;font-size:0.72rem;font-weight:600;margin-top:1rem}
|
|
153
153
|
.sn-pro{opacity:0.7}
|
|
154
154
|
|
|
155
|
+
/* ── Trends page ── */
|
|
156
|
+
.trend-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem;margin-top:0.5rem}
|
|
157
|
+
.trend-card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:0.8rem}
|
|
158
|
+
.trend-header{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.3rem}
|
|
159
|
+
.trend-name{font-size:0.78rem;font-weight:700;flex:1}
|
|
160
|
+
.trend-score{font-size:1.1rem;font-weight:900}
|
|
161
|
+
.trend-chart{overflow:hidden}
|
|
162
|
+
.trend-chart svg{width:100%;height:60px}
|
|
163
|
+
.trend-table{margin-bottom:1.5rem}
|
|
164
|
+
.trend-row{display:flex;align-items:center;gap:0.5rem;padding:0.25rem 0;border-bottom:1px solid var(--border);font-size:0.75rem}
|
|
165
|
+
.trend-row-name{flex:1;font-weight:600}
|
|
166
|
+
.trend-row-val{width:2rem;text-align:center;color:var(--muted)}
|
|
167
|
+
.trend-row-arrow{color:var(--muted);font-size:0.6rem}
|
|
168
|
+
.trend-row-delta{width:2.5rem;text-align:right;font-weight:700}
|
|
169
|
+
|
|
155
170
|
.footer{text-align:center;color:var(--muted);font-size:0.58rem;margin-top:2rem;padding:0.8rem 0;border-top:1px solid var(--border)}
|
|
156
171
|
.footer a{color:var(--muted)}
|
|
157
172
|
.flink{color:var(--accent);text-decoration:none;font-family:"SF Mono",monospace}.flink:hover{text-decoration:underline}
|
|
@@ -145,8 +145,7 @@ function buildGraph(files) {
|
|
|
145
145
|
function parseImports(content) {
|
|
146
146
|
const imports = [];
|
|
147
147
|
const regex = /import\s+(?:[\s\S]*?)\s+from\s+['"]([^'"]+)['"]/g;
|
|
148
|
-
|
|
149
|
-
while ((match = regex.exec(content)) !== null) {
|
|
148
|
+
for (const match of content.matchAll(regex)) {
|
|
150
149
|
if (match[1].startsWith("."))
|
|
151
150
|
imports.push(match[1]);
|
|
152
151
|
}
|
|
@@ -13,21 +13,10 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { readDeps } from "../fs-utils.js";
|
|
15
15
|
import { gradeFromScore } from "../types.js";
|
|
16
|
-
|
|
17
|
-
const start = Date.now();
|
|
16
|
+
function checkCICD(cwd, has, read) {
|
|
18
17
|
const issues = [];
|
|
19
18
|
let practices = 0;
|
|
20
19
|
let followed = 0;
|
|
21
|
-
const has = (f) => existsSync(join(cwd, f));
|
|
22
|
-
const read = (f) => {
|
|
23
|
-
try {
|
|
24
|
-
return readFileSync(join(cwd, f), "utf-8");
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return "";
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
// ── 1. CI/CD Best Practices ──
|
|
31
20
|
// Check for GitHub Actions workflows
|
|
32
21
|
const hasWorkflows = has(".github/workflows");
|
|
33
22
|
practices++;
|
|
@@ -99,7 +88,12 @@ export function runBestPractices(cwd) {
|
|
|
99
88
|
rule: "no-ci",
|
|
100
89
|
});
|
|
101
90
|
}
|
|
102
|
-
|
|
91
|
+
return { practices, followed, issues };
|
|
92
|
+
}
|
|
93
|
+
function checkSupplyChain(has, read) {
|
|
94
|
+
const issues = [];
|
|
95
|
+
let practices = 0;
|
|
96
|
+
let followed = 0;
|
|
103
97
|
// Lockfile committed
|
|
104
98
|
practices++;
|
|
105
99
|
const hasLockfile = has("pnpm-lock.yaml") || has("package-lock.json") || has("yarn.lock") || has("bun.lockb") || has("pubspec.lock");
|
|
@@ -136,7 +130,12 @@ export function runBestPractices(cwd) {
|
|
|
136
130
|
});
|
|
137
131
|
}
|
|
138
132
|
}
|
|
139
|
-
|
|
133
|
+
return { practices, followed, issues };
|
|
134
|
+
}
|
|
135
|
+
function checkRepoHygiene(has) {
|
|
136
|
+
const issues = [];
|
|
137
|
+
let practices = 0;
|
|
138
|
+
let followed = 0;
|
|
140
139
|
// SECURITY.md or security policy
|
|
141
140
|
practices++;
|
|
142
141
|
if (has("SECURITY.md") || has(".github/SECURITY.md")) {
|
|
@@ -165,7 +164,12 @@ export function runBestPractices(cwd) {
|
|
|
165
164
|
rule: "contributing-guide",
|
|
166
165
|
});
|
|
167
166
|
}
|
|
168
|
-
|
|
167
|
+
return { practices, followed, issues };
|
|
168
|
+
}
|
|
169
|
+
function checkDevExperience(cwd, has) {
|
|
170
|
+
const issues = [];
|
|
171
|
+
let practices = 0;
|
|
172
|
+
let followed = 0;
|
|
169
173
|
// .env.example
|
|
170
174
|
practices++;
|
|
171
175
|
const hasEnvFiles = has(".env") || has(".env.local") || has(".env.development");
|
|
@@ -204,7 +208,12 @@ export function runBestPractices(cwd) {
|
|
|
204
208
|
rule: "automated-deps",
|
|
205
209
|
});
|
|
206
210
|
}
|
|
207
|
-
|
|
211
|
+
return { practices, followed, issues };
|
|
212
|
+
}
|
|
213
|
+
function checkCodeQualityTooling(has, read) {
|
|
214
|
+
const issues = [];
|
|
215
|
+
let practices = 0;
|
|
216
|
+
let followed = 0;
|
|
208
217
|
// Linter configured
|
|
209
218
|
practices++;
|
|
210
219
|
if (has("biome.json") ||
|
|
@@ -247,7 +256,13 @@ export function runBestPractices(cwd) {
|
|
|
247
256
|
rule: "ts-strict-mode",
|
|
248
257
|
});
|
|
249
258
|
}
|
|
250
|
-
|
|
259
|
+
return { practices, followed, issues };
|
|
260
|
+
}
|
|
261
|
+
function checkTesting(has, read) {
|
|
262
|
+
const issues = [];
|
|
263
|
+
let practices = 0;
|
|
264
|
+
let followed = 0;
|
|
265
|
+
const pkg = read("package.json");
|
|
251
266
|
// Test script exists
|
|
252
267
|
practices++;
|
|
253
268
|
if (pkg.includes('"test"') || has("pubspec.yaml")) {
|
|
@@ -268,7 +283,12 @@ export function runBestPractices(cwd) {
|
|
|
268
283
|
rule: "coverage-config",
|
|
269
284
|
});
|
|
270
285
|
}
|
|
271
|
-
|
|
286
|
+
return { practices, followed, issues };
|
|
287
|
+
}
|
|
288
|
+
function checkDocker(has, read) {
|
|
289
|
+
const issues = [];
|
|
290
|
+
let practices = 0;
|
|
291
|
+
let followed = 0;
|
|
272
292
|
// Dockerfile best practices (if Docker is used)
|
|
273
293
|
if (has("Dockerfile") || has("docker-compose.yml") || has("docker-compose.yaml")) {
|
|
274
294
|
practices++;
|
|
@@ -315,7 +335,13 @@ export function runBestPractices(cwd) {
|
|
|
315
335
|
});
|
|
316
336
|
}
|
|
317
337
|
}
|
|
318
|
-
|
|
338
|
+
return { practices, followed, issues };
|
|
339
|
+
}
|
|
340
|
+
function checkGitPractices(cwd, has, read) {
|
|
341
|
+
const issues = [];
|
|
342
|
+
let practices = 0;
|
|
343
|
+
let followed = 0;
|
|
344
|
+
const deps = readDeps(cwd);
|
|
319
345
|
// .gitignore is comprehensive
|
|
320
346
|
practices++;
|
|
321
347
|
const gitignore = read(".gitignore");
|
|
@@ -344,7 +370,13 @@ export function runBestPractices(cwd) {
|
|
|
344
370
|
rule: "conventional-commits",
|
|
345
371
|
});
|
|
346
372
|
}
|
|
347
|
-
|
|
373
|
+
return { practices, followed, issues };
|
|
374
|
+
}
|
|
375
|
+
function checkMonitoring(cwd) {
|
|
376
|
+
const issues = [];
|
|
377
|
+
let practices = 0;
|
|
378
|
+
let followed = 0;
|
|
379
|
+
const deps = readDeps(cwd);
|
|
348
380
|
// Error tracking (Sentry, Bugsnag, etc.) — only for apps/servers, not CLI tools
|
|
349
381
|
const isApp = deps.react || deps.vue || deps.svelte || deps.express || deps.fastify || deps.hono || deps.next || deps.nuxt;
|
|
350
382
|
if (isApp) {
|
|
@@ -360,7 +392,14 @@ export function runBestPractices(cwd) {
|
|
|
360
392
|
});
|
|
361
393
|
}
|
|
362
394
|
}
|
|
363
|
-
|
|
395
|
+
return { practices, followed, issues };
|
|
396
|
+
}
|
|
397
|
+
function checkAPIConfig(cwd, read) {
|
|
398
|
+
const issues = [];
|
|
399
|
+
let practices = 0;
|
|
400
|
+
let followed = 0;
|
|
401
|
+
const deps = readDeps(cwd);
|
|
402
|
+
const pkg = read("package.json");
|
|
364
403
|
// Environment validation (zod, joi, envalid)
|
|
365
404
|
practices++;
|
|
366
405
|
if (deps.zod || deps.joi || deps.envalid || deps["@t3-oss/env-core"] || deps["@t3-oss/env-nextjs"]) {
|
|
@@ -379,6 +418,39 @@ export function runBestPractices(cwd) {
|
|
|
379
418
|
followed++;
|
|
380
419
|
}
|
|
381
420
|
}
|
|
421
|
+
return { practices, followed, issues };
|
|
422
|
+
}
|
|
423
|
+
export function runBestPractices(cwd) {
|
|
424
|
+
const start = Date.now();
|
|
425
|
+
const has = (f) => existsSync(join(cwd, f));
|
|
426
|
+
const read = (f) => {
|
|
427
|
+
try {
|
|
428
|
+
return readFileSync(join(cwd, f), "utf-8");
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
return "";
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
const categories = [
|
|
435
|
+
checkCICD(cwd, has, read),
|
|
436
|
+
checkSupplyChain(has, read),
|
|
437
|
+
checkRepoHygiene(has),
|
|
438
|
+
checkDevExperience(cwd, has),
|
|
439
|
+
checkCodeQualityTooling(has, read),
|
|
440
|
+
checkTesting(has, read),
|
|
441
|
+
checkDocker(has, read),
|
|
442
|
+
checkGitPractices(cwd, has, read),
|
|
443
|
+
checkMonitoring(cwd),
|
|
444
|
+
checkAPIConfig(cwd, read),
|
|
445
|
+
];
|
|
446
|
+
let practices = 0;
|
|
447
|
+
let followed = 0;
|
|
448
|
+
const issues = [];
|
|
449
|
+
for (const cat of categories) {
|
|
450
|
+
practices += cat.practices;
|
|
451
|
+
followed += cat.followed;
|
|
452
|
+
issues.push(...cat.issues);
|
|
453
|
+
}
|
|
382
454
|
// ── Score ──
|
|
383
455
|
const pct = practices > 0 ? Math.round((followed / practices) * 100) : 100;
|
|
384
456
|
const score = pct;
|
|
@@ -247,8 +247,7 @@ function extractExports(content) {
|
|
|
247
247
|
/export\s+type\s+(\w+)/g,
|
|
248
248
|
];
|
|
249
249
|
for (const pat of patterns) {
|
|
250
|
-
|
|
251
|
-
while ((match = pat.exec(content)) !== null) {
|
|
250
|
+
for (const match of content.matchAll(pat)) {
|
|
252
251
|
exports.push(match[1]);
|
|
253
252
|
}
|
|
254
253
|
}
|
package/dist/runners/context.js
CHANGED
|
@@ -131,8 +131,7 @@ export function runContext(cwd) {
|
|
|
131
131
|
function parseImports(content) {
|
|
132
132
|
const imports = [];
|
|
133
133
|
const regex = /import\s+(?:[\s\S]*?)\s+from\s+['"]([^'"]+)['"]/g;
|
|
134
|
-
|
|
135
|
-
while ((match = regex.exec(content)) !== null) {
|
|
134
|
+
for (const match of content.matchAll(regex)) {
|
|
136
135
|
const path = match[1];
|
|
137
136
|
// Only count local imports (starting with . or /)
|
|
138
137
|
if (path.startsWith(".") || path.startsWith("/")) {
|
|
@@ -28,7 +28,12 @@ const CODE_SMELLS = [
|
|
|
28
28
|
message: "dangerouslySetInnerHTML bypasses React's XSS protection",
|
|
29
29
|
},
|
|
30
30
|
{ name: "document.write", pattern: /document\.write\s*\(/, severity: "error", message: "document.write blocks rendering" },
|
|
31
|
-
{
|
|
31
|
+
{
|
|
32
|
+
name: "http:// URL",
|
|
33
|
+
pattern: /['"]http:\/\/(?!localhost|127\.0\.0\.1|www\.w3\.org|schemas?\.)/,
|
|
34
|
+
severity: "warning",
|
|
35
|
+
message: "Non-HTTPS URL — use https://",
|
|
36
|
+
},
|
|
32
37
|
{ name: "TODO/FIXME", pattern: /\b(TODO|FIXME|HACK|XXX)\b/, severity: "warning", message: "Unresolved TODO/FIXME comment" },
|
|
33
38
|
{
|
|
34
39
|
name: "magic number",
|
|
@@ -96,6 +101,9 @@ export function runStandards(cwd, stack) {
|
|
|
96
101
|
continue;
|
|
97
102
|
if (/\bpattern\s*:|name:\s*["']|message:\s*["']|description:\s*["']|risk:\s*["']|recommendation:\s*["']/.test(trimmed))
|
|
98
103
|
continue;
|
|
104
|
+
// Skip string-only lines (check-meta descriptions, inline scripts)
|
|
105
|
+
if (/^\s*["'`].*["'`][,;]?\s*$/.test(line))
|
|
106
|
+
continue;
|
|
99
107
|
for (const check of CODE_SMELLS) {
|
|
100
108
|
// Skip console.log in CLI entry points (intentional output)
|
|
101
109
|
if (check.name === "console.log" && (f.path.includes("cli.") || f.path.includes("bin/")))
|