@vibecodeqa/cli 0.21.0 → 0.23.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.
@@ -157,10 +157,12 @@ export function buildSparkline(values, opts) {
157
157
  const range = max - min || 1;
158
158
  const step = width / (values.length - 1);
159
159
  const points = values.map((v, i) => `${(i * step).toFixed(1)},${(height - ((v - min) / range) * (height - 4) - 2).toFixed(1)}`).join(" ");
160
- const dots = values.map((v, i) => {
160
+ const dots = values
161
+ .map((v, i) => {
161
162
  const x = (i * step).toFixed(1);
162
163
  const y = (height - ((v - min) / range) * (height - 4) - 2).toFixed(1);
163
164
  return `<circle cx="${x}" cy="${y}" r="1.5" fill="${color}"/>`;
164
- }).join("");
165
+ })
166
+ .join("");
165
167
  return `<svg viewBox="0 0 ${width} ${height}" width="${width}" height="${height}"><polyline points="${points}" fill="none" stroke="${color}" stroke-width="1.5" stroke-linecap="round"/>${dots}</svg>`;
166
168
  }
@@ -1,13 +1,20 @@
1
1
  /** Accessibility check — detects common a11y violations in JSX/TSX code. */
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
- import { gradeFromScore } from "../types.js";
5
4
  import { getProductionFiles } from "../fs-utils.js";
5
+ import { gradeFromScore } from "../types.js";
6
6
  export function runAccessibility(cwd) {
7
7
  const start = Date.now();
8
8
  const files = getProductionFiles(cwd).filter((f) => f.ext === ".tsx" || f.ext === ".jsx");
9
9
  if (files.length === 0) {
10
- return { name: "accessibility", score: 100, grade: "A", details: { skipped: true, reason: "no JSX/TSX files" }, issues: [], duration: Date.now() - start };
10
+ return {
11
+ name: "accessibility",
12
+ score: 100,
13
+ grade: "A",
14
+ details: { skipped: true, reason: "no JSX/TSX files" },
15
+ issues: [],
16
+ duration: Date.now() - start,
17
+ };
11
18
  }
12
19
  const issues = [];
13
20
  let missingAlt = 0;
@@ -36,7 +43,13 @@ export function runAccessibility(cwd) {
36
43
  const block = lines.slice(i, Math.min(i + 3, lines.length)).join(" ");
37
44
  if (!(/role=/.test(block) && /(?:onKeyDown|onKeyUp|onKeyPress|tabIndex)/.test(block))) {
38
45
  clickDiv++;
39
- issues.push({ severity: "warning", message: "Click handler on non-interactive element without role + keyboard handler", file: f.path, line: i + 1, rule: "click-events" });
46
+ issues.push({
47
+ severity: "warning",
48
+ message: "Click handler on non-interactive element without role + keyboard handler",
49
+ file: f.path,
50
+ line: i + 1,
51
+ rule: "click-events",
52
+ });
40
53
  }
41
54
  }
42
55
  // 3. <input>/<select>/<textarea> without associated label
@@ -44,18 +57,36 @@ export function runAccessibility(cwd) {
44
57
  const block = lines.slice(Math.max(0, i - 3), Math.min(i + 3, lines.length)).join(" ");
45
58
  if (!/aria-label=/.test(block) && !/aria-labelledby=/.test(block) && !/<label/.test(block) && !/id=/.test(trimmed)) {
46
59
  missingLabel++;
47
- issues.push({ severity: "warning", message: "Form control without label, aria-label, or aria-labelledby", file: f.path, line: i + 1, rule: "form-label" });
60
+ issues.push({
61
+ severity: "warning",
62
+ message: "Form control without label, aria-label, or aria-labelledby",
63
+ file: f.path,
64
+ line: i + 1,
65
+ rule: "form-label",
66
+ });
48
67
  }
49
68
  }
50
69
  // 4. autoFocus
51
70
  if (/\bautoFocus\b/.test(trimmed) || /\bautofocus\b/.test(trimmed)) {
52
71
  autofocus++;
53
- issues.push({ severity: "warning", message: "autoFocus can disorient screen reader users", file: f.path, line: i + 1, rule: "no-autofocus" });
72
+ issues.push({
73
+ severity: "warning",
74
+ message: "autoFocus can disorient screen reader users",
75
+ file: f.path,
76
+ line: i + 1,
77
+ rule: "no-autofocus",
78
+ });
54
79
  }
55
80
  // 5. Positive tabIndex
56
81
  if (/tabIndex=\{[1-9]/.test(trimmed) || /tabindex=["'][1-9]/.test(trimmed)) {
57
82
  positiveTabindex++;
58
- issues.push({ severity: "warning", message: "Positive tabIndex disrupts natural tab order — use 0 or -1", file: f.path, line: i + 1, rule: "tabindex" });
83
+ issues.push({
84
+ severity: "warning",
85
+ message: "Positive tabIndex disrupts natural tab order — use 0 or -1",
86
+ file: f.path,
87
+ line: i + 1,
88
+ rule: "tabindex",
89
+ });
59
90
  }
60
91
  }
61
92
  }
@@ -27,4 +27,6 @@ export declare function runArchitecture(cwd: string): CheckResult;
27
27
  export declare function generateArchSVG(details: Record<string, unknown>): string;
28
28
  export declare function generateDSM(details: Record<string, unknown>): string;
29
29
  export declare function generatePackageDiagram(details: Record<string, unknown>): string;
30
+ export declare function generateSequenceDiagram(details: Record<string, unknown>): string;
31
+ export declare function generateContainerDiagram(cwd: string): string;
30
32
  export {};
@@ -9,7 +9,8 @@
9
9
  * 6. Layer violations (optional: detect cross-layer imports)
10
10
  * 7. SVG architecture diagram
11
11
  */
12
- import { basename, dirname, extname } from "node:path";
12
+ import { existsSync } from "node:fs";
13
+ import { basename, dirname, extname, join } from "node:path";
13
14
  import { getProductionFiles } from "../fs-utils.js";
14
15
  import { gradeFromScore } from "../types.js";
15
16
  export function runArchitecture(cwd) {
@@ -105,6 +106,7 @@ export function runArchitecture(cwd) {
105
106
  highFanOut,
106
107
  connectors,
107
108
  graph: graphData,
109
+ containerSvg: generateContainerDiagram(cwd),
108
110
  },
109
111
  issues,
110
112
  duration: Date.now() - start,
@@ -504,3 +506,155 @@ export function generatePackageDiagram(details) {
504
506
  const H = maxH + gap;
505
507
  return `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:${W}px">${svg}</svg>`;
506
508
  }
509
+ // ── Sequence Diagram ─────────────────────────────────────────────────
510
+ // Traces the longest import chains from entry points, showing how a request
511
+ // flows through the system. UML-style lifelines with arrows.
512
+ export function generateSequenceDiagram(details) {
513
+ const graph = details.graph;
514
+ if (!graph || Object.keys(graph).length < 3)
515
+ return "";
516
+ // Find entry points (files with 0 importers that aren't utility files)
517
+ const entries = Object.entries(graph);
518
+ const entryPoints = entries
519
+ .filter(([path, info]) => {
520
+ const name = basename(path, extname(path));
521
+ return info.importedBy.length === 0 && ["index", "main", "cli", "App", "app", "server"].includes(name);
522
+ })
523
+ .map(([p]) => p);
524
+ if (entryPoints.length === 0)
525
+ return "";
526
+ // BFS from first entry point to find the longest chain (max 8 deep)
527
+ const entry = entryPoints[0];
528
+ const chain = findLongestChain(entry, graph, 8);
529
+ if (chain.length < 3)
530
+ return "";
531
+ // Draw sequence diagram
532
+ const participants = chain.map((p) => basename(p, extname(p)));
533
+ const lifelineSpacing = 120;
534
+ const W = participants.length * lifelineSpacing + 40;
535
+ const messageH = 36;
536
+ const headerH = 50;
537
+ const H = headerH + (chain.length - 1) * messageH + 40;
538
+ let svg = "";
539
+ // Participant boxes (lifeline headers)
540
+ for (let i = 0; i < participants.length; i++) {
541
+ const x = 20 + i * lifelineSpacing + lifelineSpacing / 2;
542
+ const name = participants[i];
543
+ const boxW = Math.max(60, name.length * 7 + 16);
544
+ svg += `<rect x="${x - boxW / 2}" y="8" width="${boxW}" height="22" rx="4" fill="#ffffff08" stroke="#ffffff15"/>`;
545
+ svg += `<text x="${x}" y="23" text-anchor="middle" fill="#9ca3af" font-size="9" font-weight="600">${name}</text>`;
546
+ // Lifeline (dashed vertical)
547
+ svg += `<line x1="${x}" y1="30" x2="${x}" y2="${H - 10}" stroke="#ffffff10" stroke-width="1" stroke-dasharray="4,3"/>`;
548
+ }
549
+ // Arrows between lifelines (imports = calls)
550
+ for (let i = 0; i < chain.length - 1; i++) {
551
+ const fromX = 20 + i * lifelineSpacing + lifelineSpacing / 2;
552
+ const toX = 20 + (i + 1) * lifelineSpacing + lifelineSpacing / 2;
553
+ const y = headerH + i * messageH;
554
+ // Arrow
555
+ svg += `<line x1="${fromX}" y1="${y}" x2="${toX - 6}" y2="${y}" stroke="#6d78d0" stroke-width="1.5" marker-end="url(#seq-arrow)"/>`;
556
+ // Label (the import)
557
+ const label = `import`;
558
+ svg += `<text x="${(fromX + toX) / 2}" y="${y - 6}" text-anchor="middle" fill="#6b7280" font-size="7">${label}</text>`;
559
+ }
560
+ // Arrow marker
561
+ const defs = `<defs><marker id="seq-arrow" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="7" markerHeight="5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#6d78d0"/></marker></defs>`;
562
+ return `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:${W}px">${defs}${svg}</svg>`;
563
+ }
564
+ function findLongestChain(start, graph, maxDepth) {
565
+ let longest = [start];
566
+ const visited = new Set([start]);
567
+ function dfs(node, path) {
568
+ if (path.length > longest.length)
569
+ longest = [...path];
570
+ if (path.length >= maxDepth)
571
+ return;
572
+ const info = graph[node];
573
+ if (!info)
574
+ return;
575
+ for (const imp of info.imports) {
576
+ if (!visited.has(imp) && graph[imp]) {
577
+ visited.add(imp);
578
+ dfs(imp, [...path, imp]);
579
+ visited.delete(imp);
580
+ }
581
+ }
582
+ }
583
+ dfs(start, [start]);
584
+ return longest;
585
+ }
586
+ // ── Container Diagram ────────────────────────────────────────────────
587
+ // Auto-detects high-level system containers from config files:
588
+ // frontend, backend/API, database, worker, static site, etc.
589
+ export function generateContainerDiagram(cwd) {
590
+ const has = (f) => existsSync(join(cwd, f));
591
+ const containers = [];
592
+ // Detect containers from config files
593
+ if (has("src/App.tsx") || has("src/App.vue") || has("src/App.svelte") || has("web/src/App.tsx")) {
594
+ const tech = has("src/App.tsx") ? "React" : has("src/App.vue") ? "Vue" : "Svelte";
595
+ containers.push({ name: "Frontend", type: "webapp", tech });
596
+ }
597
+ if (has("wrangler.toml") || has("wrangler.json")) {
598
+ containers.push({ name: "Worker", type: "worker", tech: "Cloudflare Workers" });
599
+ }
600
+ if (has("Dockerfile") || has("server.ts") || has("src/server.ts") || has("src/index.ts")) {
601
+ if (!containers.some((c) => c.name === "Frontend")) {
602
+ containers.push({ name: "API Server", type: "api", tech: "Node.js" });
603
+ }
604
+ }
605
+ if (has("prisma/schema.prisma") || has("drizzle.config.ts")) {
606
+ const tech = has("prisma/schema.prisma") ? "Prisma" : "Drizzle";
607
+ containers.push({ name: "Database", type: "db", tech });
608
+ }
609
+ if (has("firebase.json") || has(".firebaserc")) {
610
+ containers.push({ name: "Firebase", type: "baas", tech: "Firebase" });
611
+ }
612
+ if (has("supabase/config.toml") || has(".supabase")) {
613
+ containers.push({ name: "Supabase", type: "baas", tech: "Supabase" });
614
+ }
615
+ if (has("pubspec.yaml")) {
616
+ containers.push({ name: "Mobile App", type: "mobile", tech: "Flutter" });
617
+ }
618
+ if (has("package.json") && !containers.length) {
619
+ containers.push({ name: "Application", type: "app", tech: "Node.js" });
620
+ }
621
+ if (containers.length < 2)
622
+ return ""; // Only interesting with 2+ containers
623
+ // Layout: horizontal boxes with connecting lines
624
+ const boxW = 140;
625
+ const boxH = 60;
626
+ const gap = 30;
627
+ const W = containers.length * (boxW + gap) + gap;
628
+ const H = 120;
629
+ const typeColors = {
630
+ webapp: "#6d78d0",
631
+ worker: "#d97706",
632
+ api: "#22c55e",
633
+ db: "#8b5cf6",
634
+ baas: "#ec4899",
635
+ mobile: "#06b6d4",
636
+ app: "#6d78d0",
637
+ };
638
+ let svg = "";
639
+ for (let i = 0; i < containers.length; i++) {
640
+ const c = containers[i];
641
+ const x = gap + i * (boxW + gap);
642
+ const y = (H - boxH) / 2;
643
+ const color = typeColors[c.type] || "#6d78d0";
644
+ // Box
645
+ svg += `<rect x="${x}" y="${y}" width="${boxW}" height="${boxH}" rx="8" fill="${color}15" stroke="${color}50"/>`;
646
+ // Name
647
+ svg += `<text x="${x + boxW / 2}" y="${y + 24}" text-anchor="middle" fill="#e5e5e5" font-size="10" font-weight="700">${c.name}</text>`;
648
+ // Tech
649
+ svg += `<text x="${x + boxW / 2}" y="${y + 40}" text-anchor="middle" fill="#6b7280" font-size="8">[${c.tech}]</text>`;
650
+ // Connection to next
651
+ if (i < containers.length - 1) {
652
+ const ax = x + boxW;
653
+ const bx = ax + gap;
654
+ const ay = H / 2;
655
+ svg += `<line x1="${ax}" y1="${ay}" x2="${bx}" y2="${ay}" stroke="#ffffff20" stroke-width="1.5" marker-end="url(#cont-arrow)"/>`;
656
+ }
657
+ }
658
+ const defs = `<defs><marker id="cont-arrow" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="7" markerHeight="5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#ffffff40"/></marker></defs>`;
659
+ return `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:${W}px">${defs}${svg}</svg>`;
660
+ }
@@ -13,32 +13,23 @@ 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
- export function runBestPractices(cwd) {
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) => { try {
23
- return readFileSync(join(cwd, f), "utf-8");
24
- }
25
- catch {
26
- return "";
27
- } };
28
- // ── 1. CI/CD Best Practices ──
29
20
  // Check for GitHub Actions workflows
30
21
  const hasWorkflows = has(".github/workflows");
31
22
  practices++;
32
23
  if (hasWorkflows) {
33
24
  followed++;
34
- const workflows = readdirSync(join(cwd, ".github/workflows")).filter(f => f.endsWith(".yml") || f.endsWith(".yaml"));
25
+ const workflows = readdirSync(join(cwd, ".github/workflows")).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
35
26
  for (const wf of workflows) {
36
27
  const content = read(`.github/workflows/${wf}`);
37
28
  // Check: actions pinned to SHA (not @v4, @main)
38
29
  const actionUses = content.match(/uses:\s*([^\n]+)/g) || [];
39
- const unpinned = actionUses.filter(u => !u.includes("@") || (!u.match(/@[a-f0-9]{40}/) && !u.includes("@sha")));
30
+ const unpinned = actionUses.filter((u) => !u.includes("@") || (!u.match(/@[a-f0-9]{40}/) && !u.includes("@sha")));
40
31
  // Only flag third-party actions (not actions/*)
41
- const unpinnedThirdParty = unpinned.filter(u => !u.includes("actions/") && !u.includes("pnpm/"));
32
+ const unpinnedThirdParty = unpinned.filter((u) => !u.includes("actions/") && !u.includes("pnpm/"));
42
33
  if (unpinnedThirdParty.length > 0) {
43
34
  issues.push({
44
35
  severity: "info",
@@ -97,7 +88,12 @@ export function runBestPractices(cwd) {
97
88
  rule: "no-ci",
98
89
  });
99
90
  }
100
- // ── 2. Supply Chain ──
91
+ return { practices, followed, issues };
92
+ }
93
+ function checkSupplyChain(has, read) {
94
+ const issues = [];
95
+ let practices = 0;
96
+ let followed = 0;
101
97
  // Lockfile committed
102
98
  practices++;
103
99
  const hasLockfile = has("pnpm-lock.yaml") || has("package-lock.json") || has("yarn.lock") || has("bun.lockb") || has("pubspec.lock");
@@ -114,7 +110,11 @@ export function runBestPractices(cwd) {
114
110
  followed++;
115
111
  }
116
112
  else {
117
- issues.push({ severity: "info", message: "No engine constraints (engines in package.json or .nvmrc) — Node version not pinned", rule: "pin-node-version" });
113
+ issues.push({
114
+ severity: "info",
115
+ message: "No engine constraints (engines in package.json or .nvmrc) — Node version not pinned",
116
+ rule: "pin-node-version",
117
+ });
118
118
  }
119
119
  // npm provenance / package.json has repository field
120
120
  if (pkg) {
@@ -123,10 +123,19 @@ export function runBestPractices(cwd) {
123
123
  followed++;
124
124
  }
125
125
  else {
126
- issues.push({ severity: "info", message: "package.json missing repository field — provenance attestation won't link to source", rule: "repository-field" });
126
+ issues.push({
127
+ severity: "info",
128
+ message: "package.json missing repository field — provenance attestation won't link to source",
129
+ rule: "repository-field",
130
+ });
127
131
  }
128
132
  }
129
- // ── 3. Repo Hygiene ──
133
+ return { practices, followed, issues };
134
+ }
135
+ function checkRepoHygiene(has) {
136
+ const issues = [];
137
+ let practices = 0;
138
+ let followed = 0;
130
139
  // SECURITY.md or security policy
131
140
  practices++;
132
141
  if (has("SECURITY.md") || has(".github/SECURITY.md")) {
@@ -149,14 +158,27 @@ export function runBestPractices(cwd) {
149
158
  followed++;
150
159
  }
151
160
  else {
152
- issues.push({ severity: "info", message: "No CONTRIBUTING.md — onboarding is harder for new contributors", rule: "contributing-guide" });
161
+ issues.push({
162
+ severity: "info",
163
+ message: "No CONTRIBUTING.md — onboarding is harder for new contributors",
164
+ rule: "contributing-guide",
165
+ });
153
166
  }
154
- // ── 4. Developer Experience ──
167
+ return { practices, followed, issues };
168
+ }
169
+ function checkDevExperience(cwd, has) {
170
+ const issues = [];
171
+ let practices = 0;
172
+ let followed = 0;
155
173
  // .env.example
156
174
  practices++;
157
175
  const hasEnvFiles = has(".env") || has(".env.local") || has(".env.development");
158
176
  if (hasEnvFiles && !has(".env.example")) {
159
- issues.push({ severity: "info", message: "Has .env files but no .env.example — new developers won't know what vars are needed", rule: "env-example" });
177
+ issues.push({
178
+ severity: "info",
179
+ message: "Has .env files but no .env.example — new developers won't know what vars are needed",
180
+ rule: "env-example",
181
+ });
160
182
  }
161
183
  else {
162
184
  followed++;
@@ -168,7 +190,11 @@ export function runBestPractices(cwd) {
168
190
  followed++;
169
191
  }
170
192
  else {
171
- issues.push({ severity: "info", message: "No pre-commit hooks (husky/lefthook) — lint/format not enforced before commit", rule: "pre-commit-hooks" });
193
+ issues.push({
194
+ severity: "info",
195
+ message: "No pre-commit hooks (husky/lefthook) — lint/format not enforced before commit",
196
+ rule: "pre-commit-hooks",
197
+ });
172
198
  }
173
199
  // Renovate/Dependabot for automated dependency updates
174
200
  practices++;
@@ -176,16 +202,34 @@ export function runBestPractices(cwd) {
176
202
  followed++;
177
203
  }
178
204
  else {
179
- issues.push({ severity: "info", message: "No Dependabot/Renovate — dependency updates are manual and often forgotten", rule: "automated-deps" });
205
+ issues.push({
206
+ severity: "info",
207
+ message: "No Dependabot/Renovate — dependency updates are manual and often forgotten",
208
+ rule: "automated-deps",
209
+ });
180
210
  }
181
- // ── 5. Code Quality Tooling ──
211
+ return { practices, followed, issues };
212
+ }
213
+ function checkCodeQualityTooling(has, read) {
214
+ const issues = [];
215
+ let practices = 0;
216
+ let followed = 0;
182
217
  // Linter configured
183
218
  practices++;
184
- if (has("biome.json") || has(".eslintrc.json") || has(".eslintrc.js") || has("eslint.config.js") || has("eslint.config.ts") || has("analysis_options.yaml")) {
219
+ if (has("biome.json") ||
220
+ has(".eslintrc.json") ||
221
+ has(".eslintrc.js") ||
222
+ has("eslint.config.js") ||
223
+ has("eslint.config.ts") ||
224
+ has("analysis_options.yaml")) {
185
225
  followed++;
186
226
  }
187
227
  else {
188
- issues.push({ severity: "warning", message: "No linter config (ESLint/Biome/dart analyze) — code style not enforced", rule: "linter-config" });
228
+ issues.push({
229
+ severity: "warning",
230
+ message: "No linter config (ESLint/Biome/dart analyze) — code style not enforced",
231
+ rule: "linter-config",
232
+ });
189
233
  }
190
234
  // Formatter configured
191
235
  practices++;
@@ -193,7 +237,11 @@ export function runBestPractices(cwd) {
193
237
  followed++;
194
238
  }
195
239
  else {
196
- issues.push({ severity: "info", message: "No formatter config (Prettier/Biome/.editorconfig) — inconsistent code formatting", rule: "formatter-config" });
240
+ issues.push({
241
+ severity: "info",
242
+ message: "No formatter config (Prettier/Biome/.editorconfig) — inconsistent code formatting",
243
+ rule: "formatter-config",
244
+ });
197
245
  }
198
246
  // TypeScript strict mode
199
247
  practices++;
@@ -202,9 +250,19 @@ export function runBestPractices(cwd) {
202
250
  followed++;
203
251
  }
204
252
  else {
205
- issues.push({ severity: "info", message: "TypeScript strict mode not enabled — allows implicit any and null errors", rule: "ts-strict-mode" });
253
+ issues.push({
254
+ severity: "info",
255
+ message: "TypeScript strict mode not enabled — allows implicit any and null errors",
256
+ rule: "ts-strict-mode",
257
+ });
206
258
  }
207
- // ── 6. Testing Best Practices ──
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");
208
266
  // Test script exists
209
267
  practices++;
210
268
  if (pkg.includes('"test"') || has("pubspec.yaml")) {
@@ -219,9 +277,18 @@ export function runBestPractices(cwd) {
219
277
  followed++;
220
278
  }
221
279
  else {
222
- issues.push({ severity: "info", message: "No test coverage configuration — coverage thresholds not enforced", rule: "coverage-config" });
280
+ issues.push({
281
+ severity: "info",
282
+ message: "No test coverage configuration — coverage thresholds not enforced",
283
+ rule: "coverage-config",
284
+ });
223
285
  }
224
- // ── 7. Docker / Deployment ──
286
+ return { practices, followed, issues };
287
+ }
288
+ function checkDocker(has, read) {
289
+ const issues = [];
290
+ let practices = 0;
291
+ let followed = 0;
225
292
  // Dockerfile best practices (if Docker is used)
226
293
  if (has("Dockerfile") || has("docker-compose.yml") || has("docker-compose.yaml")) {
227
294
  practices++;
@@ -230,7 +297,11 @@ export function runBestPractices(cwd) {
230
297
  followed++;
231
298
  }
232
299
  else if (dockerfile.includes(":latest")) {
233
- issues.push({ severity: "warning", message: "Dockerfile uses :latest tag — pin to a specific version for reproducible builds", rule: "docker-pin-version" });
300
+ issues.push({
301
+ severity: "warning",
302
+ message: "Dockerfile uses :latest tag — pin to a specific version for reproducible builds",
303
+ rule: "docker-pin-version",
304
+ });
234
305
  }
235
306
  else {
236
307
  followed++;
@@ -242,7 +313,11 @@ export function runBestPractices(cwd) {
242
313
  followed++;
243
314
  }
244
315
  else if (dockerfile.length > 100) {
245
- issues.push({ severity: "info", message: "Dockerfile is single-stage — consider multi-stage to reduce image size", rule: "docker-multi-stage" });
316
+ issues.push({
317
+ severity: "info",
318
+ message: "Dockerfile is single-stage — consider multi-stage to reduce image size",
319
+ rule: "docker-multi-stage",
320
+ });
246
321
  }
247
322
  else {
248
323
  followed++;
@@ -253,10 +328,20 @@ export function runBestPractices(cwd) {
253
328
  followed++;
254
329
  }
255
330
  else {
256
- issues.push({ severity: "info", message: "No .dockerignore — node_modules and build artifacts will bloat Docker image", rule: "dockerignore" });
331
+ issues.push({
332
+ severity: "info",
333
+ message: "No .dockerignore — node_modules and build artifacts will bloat Docker image",
334
+ rule: "dockerignore",
335
+ });
257
336
  }
258
337
  }
259
- // ── 8. Git Practices ──
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);
260
345
  // .gitignore is comprehensive
261
346
  practices++;
262
347
  const gitignore = read(".gitignore");
@@ -264,7 +349,11 @@ export function runBestPractices(cwd) {
264
349
  followed++;
265
350
  }
266
351
  else if (gitignore) {
267
- issues.push({ severity: "info", message: ".gitignore exists but may be incomplete — ensure build artifacts are excluded", rule: "gitignore-complete" });
352
+ issues.push({
353
+ severity: "info",
354
+ message: ".gitignore exists but may be incomplete — ensure build artifacts are excluded",
355
+ rule: "gitignore-complete",
356
+ });
268
357
  }
269
358
  else {
270
359
  followed++; // no gitignore = handled by structure check
@@ -275,9 +364,19 @@ export function runBestPractices(cwd) {
275
364
  followed++;
276
365
  }
277
366
  else {
278
- issues.push({ severity: "info", message: "No commit convention enforcement (commitlint/changesets) — changelog generation is manual", rule: "conventional-commits" });
367
+ issues.push({
368
+ severity: "info",
369
+ message: "No commit convention enforcement (commitlint/changesets) — changelog generation is manual",
370
+ rule: "conventional-commits",
371
+ });
279
372
  }
280
- // ── 9. Monitoring & Observability ──
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);
281
380
  // Error tracking (Sentry, Bugsnag, etc.) — only for apps/servers, not CLI tools
282
381
  const isApp = deps.react || deps.vue || deps.svelte || deps.express || deps.fastify || deps.hono || deps.next || deps.nuxt;
283
382
  if (isApp) {
@@ -286,10 +385,21 @@ export function runBestPractices(cwd) {
286
385
  followed++;
287
386
  }
288
387
  else {
289
- issues.push({ severity: "info", message: "No error tracking (Sentry/Bugsnag) — production errors may go unnoticed", rule: "error-tracking" });
388
+ issues.push({
389
+ severity: "info",
390
+ message: "No error tracking (Sentry/Bugsnag) — production errors may go unnoticed",
391
+ rule: "error-tracking",
392
+ });
290
393
  }
291
394
  }
292
- // ── 10. API & Configuration ──
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");
293
403
  // Environment validation (zod, joi, envalid)
294
404
  practices++;
295
405
  if (deps.zod || deps.joi || deps.envalid || deps["@t3-oss/env-core"] || deps["@t3-oss/env-nextjs"]) {
@@ -298,12 +408,49 @@ export function runBestPractices(cwd) {
298
408
  else {
299
409
  const hasEnvUsage = pkg.includes("process.env") || read("src/index.ts").includes("process.env") || read("src/main.ts").includes("process.env");
300
410
  if (hasEnvUsage) {
301
- issues.push({ severity: "info", message: "Uses env vars but no validation library (zod/envalid) — missing vars crash at runtime", rule: "env-validation" });
411
+ issues.push({
412
+ severity: "info",
413
+ message: "Uses env vars but no validation library (zod/envalid) — missing vars crash at runtime",
414
+ rule: "env-validation",
415
+ });
302
416
  }
303
417
  else {
304
418
  followed++;
305
419
  }
306
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
+ }
307
454
  // ── Score ──
308
455
  const pct = practices > 0 ? Math.round((followed / practices) * 100) : 100;
309
456
  const score = pct;
@@ -20,7 +20,9 @@ export function runDependencies(cwd, stack) {
20
20
  }
21
21
  }
22
22
  }
23
- catch { /* parse failed */ }
23
+ catch {
24
+ /* parse failed */
25
+ }
24
26
  if (majorOutdated > 0)
25
27
  issues.push({ severity: "warning", message: `${majorOutdated} packages behind by a major version` });
26
28
  const score = Math.max(0, Math.min(100, 100 - majorOutdated));