@vibecodeqa/cli 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +73 -63
  2. package/dist/check-meta.d.ts +1 -0
  3. package/dist/check-meta.js +58 -6
  4. package/dist/cli.js +48 -10
  5. package/dist/detect.js +24 -2
  6. package/dist/fs-utils.d.ts +4 -0
  7. package/dist/fs-utils.js +12 -6
  8. package/dist/report/html.d.ts +18 -9
  9. package/dist/report/html.js +108 -68
  10. package/dist/report/pages.d.ts +4 -4
  11. package/dist/report/pages.js +165 -115
  12. package/dist/report/sarif.d.ts +3 -0
  13. package/dist/report/sarif.js +67 -0
  14. package/dist/report/styles.d.ts +1 -1
  15. package/dist/report/styles.js +105 -33
  16. package/dist/report/svg.d.ts +17 -0
  17. package/dist/report/svg.js +99 -0
  18. package/dist/runners/accessibility.d.ts +3 -0
  19. package/dist/runners/accessibility.js +85 -0
  20. package/dist/runners/architecture.d.ts +2 -0
  21. package/dist/runners/architecture.js +232 -20
  22. package/dist/runners/code-coherence.d.ts +17 -0
  23. package/dist/runners/code-coherence.js +39 -0
  24. package/dist/runners/complexity.js +7 -37
  25. package/dist/runners/confusion.js +3 -31
  26. package/dist/runners/context.js +9 -40
  27. package/dist/runners/dependencies.js +28 -0
  28. package/dist/runners/doc-coherence.d.ts +14 -0
  29. package/dist/runners/doc-coherence.js +48 -0
  30. package/dist/runners/docs.js +7 -32
  31. package/dist/runners/duplication.js +9 -37
  32. package/dist/runners/lint.js +17 -0
  33. package/dist/runners/performance.d.ts +10 -0
  34. package/dist/runners/performance.js +174 -0
  35. package/dist/runners/react.d.ts +3 -0
  36. package/dist/runners/react.js +86 -0
  37. package/dist/runners/secrets.js +8 -29
  38. package/dist/runners/security.js +15 -38
  39. package/dist/runners/standards.js +3 -36
  40. package/dist/runners/structure.js +35 -55
  41. package/dist/runners/testing.js +2 -36
  42. package/dist/runners/type-safety.d.ts +1 -1
  43. package/dist/runners/type-safety.js +19 -37
  44. package/dist/runners/types-check.d.ts +1 -1
  45. package/dist/runners/types-check.js +38 -20
  46. package/dist/types.d.ts +5 -5
  47. package/package.json +11 -10
@@ -228,8 +228,22 @@ export function generateArchSVG(details) {
228
228
  return "";
229
229
  const nodes = Object.entries(graph);
230
230
  const nodeCount = nodes.length;
231
- if (nodeCount > 40)
232
- return ""; // too many nodes to render meaningfully
231
+ if (nodeCount > 50)
232
+ return `<div style="color:#6b7280;font-size:0.75rem">${nodeCount} modules too many to render. Consider splitting into smaller packages.</div>`;
233
+ // Detect cycles for highlighting
234
+ const cycleEdges = new Set();
235
+ const cycles = details.circularDeps;
236
+ if (cycles > 0) {
237
+ // Mark edges that participate in cycles (simplified: mutual imports)
238
+ for (const [path, info] of nodes) {
239
+ for (const imp of info.imports) {
240
+ if (graph[imp]?.imports.includes(path)) {
241
+ cycleEdges.add(`${path}->${imp}`);
242
+ cycleEdges.add(`${imp}->${path}`);
243
+ }
244
+ }
245
+ }
246
+ }
233
247
  // Group by directory
234
248
  const dirs = new Map();
235
249
  for (const [path, info] of nodes) {
@@ -238,23 +252,33 @@ export function generateArchSVG(details) {
238
252
  arr.push(path);
239
253
  dirs.set(dir, arr);
240
254
  }
241
- const W = 800, padding = 40;
255
+ const W = 800, padding = 50;
242
256
  const dirEntries = [...dirs.entries()];
243
257
  const dirWidth = (W - padding * 2) / Math.max(dirEntries.length, 1);
258
+ const nodeSpacing = 38;
244
259
  // Position nodes
245
260
  const positions = new Map();
246
261
  let dirIdx = 0;
247
- for (const [_d, paths] of dirEntries) {
262
+ for (const [, paths] of dirEntries) {
248
263
  const x0 = padding + dirIdx * dirWidth + dirWidth / 2;
249
264
  for (let i = 0; i < paths.length; i++) {
250
- const y = padding + 50 + i * 35;
265
+ const y = padding + 55 + i * nodeSpacing;
251
266
  positions.set(paths[i], { x: x0, y });
252
267
  }
253
268
  dirIdx++;
254
269
  }
255
- const H = Math.max(300, padding * 2 + 50 + Math.max(...[...dirs.values()].map((p) => p.length)) * 35 + 20);
256
- // Draw edges
257
- let edges = "";
270
+ const maxGroupLen = Math.max(...[...dirs.values()].map((p) => p.length));
271
+ const H = Math.max(320, padding * 2 + 55 + maxGroupLen * nodeSpacing + 50);
272
+ // ── Defs: arrowhead marker, glow filter ──
273
+ const defs = `<defs>
274
+ <marker id="ah" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="8" markerHeight="6" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#818cf850"/></marker>
275
+ <marker id="ah-cross" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="8" markerHeight="6" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#ef444460"/></marker>
276
+ <marker id="ah-cycle" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="8" markerHeight="6" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#f97316"/></marker>
277
+ </defs>`;
278
+ // ── Background — transparent, inherits page dark bg ──
279
+ const bg = `<rect width="${W}" height="${H}" rx="8" fill="none"/>`;
280
+ // ── Draw edges (curved bezier paths with arrows) ──
281
+ let edgesSvg = "";
258
282
  for (const [path, info] of nodes) {
259
283
  const from = positions.get(path);
260
284
  if (!from)
@@ -263,22 +287,36 @@ export function generateArchSVG(details) {
263
287
  const to = positions.get(imp);
264
288
  if (!to)
265
289
  continue;
266
- const color = info.dir !== graph[imp]?.dir ? "#ef444440" : "#818cf825";
267
- edges += `<line x1="${from.x}" y1="${from.y}" x2="${to.x}" y2="${to.y}" stroke="${color}" stroke-width="1.5"/>`;
290
+ const isCycle = cycleEdges.has(`${path}->${imp}`);
291
+ const isCross = info.dir !== graph[imp]?.dir;
292
+ const color = isCycle ? "#f9731680" : isCross ? "#ef444435" : "#818cf820";
293
+ const marker = isCycle ? "url(#ah-cycle)" : isCross ? "url(#ah-cross)" : "url(#ah)";
294
+ const width = isCycle ? "2" : "1.2";
295
+ const dash = isCycle ? ' stroke-dasharray="5,3"' : "";
296
+ // Bezier curve: offset control point sideways to avoid straight-line overlap
297
+ const dx = to.x - from.x;
298
+ const dy = to.y - from.y;
299
+ const cx1 = from.x + dx * 0.3 + (dy === 0 ? 0 : Math.sign(dx) * 15);
300
+ const cy1 = from.y + dy * 0.3;
301
+ const cx2 = to.x - dx * 0.3 + (dy === 0 ? 0 : Math.sign(dx) * 15);
302
+ const cy2 = to.y - dy * 0.3;
303
+ edgesSvg += `<path d="M${from.x},${from.y} C${cx1},${cy1} ${cx2},${cy2} ${to.x},${to.y}" fill="none" stroke="${color}" stroke-width="${width}" marker-end="${marker}"${dash}/>`;
268
304
  }
269
305
  }
270
- // Draw directory groups
271
- let groups = "";
306
+ // ── Draw directory groups ──
307
+ let groupsSvg = "";
272
308
  dirIdx = 0;
273
309
  for (const [dName, paths] of dirEntries) {
274
310
  const x = padding + dirIdx * dirWidth;
275
- const h = paths.length * 35 + 20;
276
- groups += `<rect x="${x + 5}" y="${padding + 30}" width="${dirWidth - 10}" height="${h}" rx="8" fill="#14141a" stroke="#23232a"/>`;
277
- groups += `<text x="${x + dirWidth / 2}" y="${padding + 22}" text-anchor="middle" fill="#6b7280" font-size="10" font-weight="600">${dName === "." ? "root" : dName.split("/").pop()}</text>`;
311
+ const h = paths.length * nodeSpacing + 24;
312
+ groupsSvg += `<rect x="${x + 5}" y="${padding + 32}" width="${dirWidth - 10}" height="${h}" rx="8" fill="#ffffff06" stroke="#ffffff10"/>`;
313
+ const label = dName === "." ? "root" : dName.split("/").pop();
314
+ groupsSvg += `<text x="${x + dirWidth / 2}" y="${padding + 24}" text-anchor="middle" fill="#6b7280" font-size="10" font-weight="700" letter-spacing="0.03em">${label}</text>`;
278
315
  dirIdx++;
279
316
  }
280
- // Draw nodes
317
+ // ── Draw nodes ──
281
318
  let nodesSvg = "";
319
+ const godThreshold = Math.max(3, Math.floor(nodeCount * 0.5));
282
320
  for (const [path] of nodes) {
283
321
  const pos = positions.get(path);
284
322
  if (!pos)
@@ -286,9 +324,183 @@ export function generateArchSVG(details) {
286
324
  const name = basename(path, extname(path));
287
325
  const info = graph[path];
288
326
  const fanIn = info.importedBy.length;
289
- const size = Math.min(8, 3 + fanIn);
290
- nodesSvg += `<circle cx="${pos.x}" cy="${pos.y}" r="${size}" fill="#818cf8"/>`;
291
- nodesSvg += `<text x="${pos.x + size + 4}" y="${pos.y + 3}" fill="#e5e5e5" font-size="9">${name}</text>`;
327
+ const fanOut = info.imports.length;
328
+ // Node color based on health
329
+ const isGod = fanIn >= godThreshold;
330
+ const isOrphan = fanIn === 0 && !["index", "main", "cli", "App"].includes(name);
331
+ const isHighFanOut = fanOut > 10;
332
+ const isInCycle = [...cycleEdges].some((e) => e.startsWith(path + "->") || e.endsWith("->" + path));
333
+ let nodeColor = "#6d78d0"; // default: softer accent
334
+ if (isInCycle)
335
+ nodeColor = "#d97706"; // amber for cycle participant
336
+ else if (isGod)
337
+ nodeColor = "#dc2626"; // red for god module
338
+ else if (isOrphan)
339
+ nodeColor = "#4b5563"; // dim for orphan
340
+ else if (isHighFanOut)
341
+ nodeColor = "#ca8a04"; // yellow for high fan-out
342
+ const size = Math.min(9, 3 + Math.floor(fanIn * 0.8));
343
+ // Node circle with subtle glow for important nodes
344
+ if (isGod || isInCycle) {
345
+ nodesSvg += `<circle cx="${pos.x}" cy="${pos.y}" r="${size + 4}" fill="${nodeColor}" opacity="0.15"/>`;
346
+ }
347
+ nodesSvg += `<circle cx="${pos.x}" cy="${pos.y}" r="${size}" fill="${nodeColor}"/>`;
348
+ // Label
349
+ const labelColor = isOrphan ? "#4b5563" : "#9ca3af";
350
+ nodesSvg += `<text x="${pos.x + size + 5}" y="${pos.y + 3}" fill="${labelColor}" font-size="9" font-weight="${isGod ? "700" : "400"}">${name}</text>`;
351
+ // Fan-in/fan-out badge (only for notable nodes)
352
+ if (fanIn > 2 || fanOut > 5) {
353
+ nodesSvg += `<text x="${pos.x + size + 5}" y="${pos.y + 13}" fill="#555" font-size="7">${fanIn}\u2190 ${fanOut}\u2192</text>`;
354
+ }
355
+ }
356
+ // ── Legend ──
357
+ const legendY = H - 30;
358
+ const legend = `<g transform="translate(${padding}, ${legendY})" font-size="8" fill="#6b7280">
359
+ <circle cx="0" cy="0" r="4" fill="#6d78d0"/><text x="8" y="3">module</text>
360
+ <circle cx="60" cy="0" r="4" fill="#dc2626"/><text x="68" y="3">god module</text>
361
+ <circle cx="140" cy="0" r="4" fill="#d97706"/><text x="148" y="3">in cycle</text>
362
+ <circle cx="200" cy="0" r="4" fill="#ca8a04"/><text x="208" y="3">high fan-out</text>
363
+ <circle cx="280" cy="0" r="4" fill="#4b5563"/><text x="288" y="3">orphan</text>
364
+ <line x1="330" y1="0" x2="350" y2="0" stroke="#ef444440" stroke-width="1.2"/><text x="354" y="3">cross-dir</text>
365
+ <line x1="410" y1="0" x2="430" y2="0" stroke="#d97706" stroke-width="1.5" stroke-dasharray="5,3"/><text x="434" y="3">circular</text>
366
+ </g>`;
367
+ return `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:${W}px">${defs}${bg}${groupsSvg}${edgesSvg}${nodesSvg}${legend}</svg>`;
368
+ }
369
+ // ── Dependency Matrix (DSM) ──────────────────────────────────────────
370
+ // Standard software architecture visualization. Rows and columns are modules,
371
+ // cells show import relationships. Clusters on the diagonal = well-structured packages.
372
+ export function generateDSM(details) {
373
+ const graph = details.graph;
374
+ if (!graph || Object.keys(graph).length === 0)
375
+ return "";
376
+ const entries = Object.entries(graph);
377
+ if (entries.length > 40)
378
+ return `<div style="color:#6b7280;font-size:0.75rem">${entries.length} modules — too many for matrix view.</div>`;
379
+ if (entries.length < 3)
380
+ return "";
381
+ // Sort by directory then name for clustering
382
+ entries.sort((a, b) => `${a[1].dir}/${a[0]}`.localeCompare(`${b[1].dir}/${b[0]}`));
383
+ const paths = entries.map(([p]) => p);
384
+ const idx = new Map(paths.map((p, i) => [p, i]));
385
+ const n = paths.length;
386
+ const cell = 14;
387
+ const labelW = 110;
388
+ const W = labelW + n * cell + 10;
389
+ const H = labelW + n * cell + 10;
390
+ // Build adjacency
391
+ const matrix = Array.from({ length: n }, () => Array(n).fill(false));
392
+ for (const [path, info] of entries) {
393
+ const from = idx.get(path);
394
+ for (const imp of info.imports) {
395
+ const to = idx.get(imp);
396
+ if (to !== undefined)
397
+ matrix[from][to] = true;
398
+ }
399
+ }
400
+ let svg = "";
401
+ const ox = labelW, oy = labelW;
402
+ // Grid
403
+ for (let i = 0; i <= n; i++) {
404
+ svg += `<line x1="${ox}" y1="${oy + i * cell}" x2="${ox + n * cell}" y2="${oy + i * cell}" stroke="#1e1e24" stroke-width="0.5"/>`;
405
+ svg += `<line x1="${ox + i * cell}" y1="${oy}" x2="${ox + i * cell}" y2="${oy + n * cell}" stroke="#1e1e24" stroke-width="0.5"/>`;
406
+ }
407
+ // Cells — row imports col
408
+ for (let r = 0; r < n; r++) {
409
+ for (let c = 0; c < n; c++) {
410
+ if (r === c) {
411
+ // Diagonal — highlight
412
+ svg += `<rect x="${ox + c * cell}" y="${oy + r * cell}" width="${cell}" height="${cell}" fill="#818cf808"/>`;
413
+ }
414
+ else if (matrix[r][c]) {
415
+ const mutual = matrix[c][r]; // circular?
416
+ const color = mutual ? "#d97706" : "#6d78d0";
417
+ svg += `<rect x="${ox + c * cell + 2}" y="${oy + r * cell + 2}" width="${cell - 4}" height="${cell - 4}" rx="2" fill="${color}" opacity="0.7"/>`;
418
+ }
419
+ }
420
+ }
421
+ // Directory bands (background stripe per dir group)
422
+ let prevDir = "";
423
+ let bandStart = 0;
424
+ const dirColors = ["#ffffff04", "#ffffff08"];
425
+ let dirIdx2 = 0;
426
+ for (let i = 0; i <= n; i++) {
427
+ const dir = i < n ? entries[i][1].dir : "__end__";
428
+ if (dir !== prevDir && i > 0) {
429
+ const fill = dirColors[dirIdx2 % 2];
430
+ svg += `<rect x="${ox}" y="${oy + bandStart * cell}" width="${n * cell}" height="${(i - bandStart) * cell}" fill="${fill}"/>`;
431
+ svg += `<rect x="${ox + bandStart * cell}" y="${oy}" width="${(i - bandStart) * cell}" height="${n * cell}" fill="${fill}"/>`;
432
+ dirIdx2++;
433
+ bandStart = i;
434
+ }
435
+ prevDir = dir;
436
+ }
437
+ // Row labels (left) and column labels (top, rotated)
438
+ for (let i = 0; i < n; i++) {
439
+ const name = basename(paths[i], extname(paths[i]));
440
+ svg += `<text x="${ox - 4}" y="${oy + i * cell + cell / 2 + 3}" text-anchor="end" fill="#9ca3af" font-size="7">${name}</text>`;
441
+ svg += `<text x="${ox + i * cell + cell / 2}" y="${oy - 4}" text-anchor="start" fill="#9ca3af" font-size="7" transform="rotate(-60 ${ox + i * cell + cell / 2} ${oy - 4})">${name}</text>`;
442
+ }
443
+ // Legend
444
+ svg += `<g transform="translate(${ox}, ${oy + n * cell + 16})" font-size="7" fill="#6b7280">`;
445
+ svg += `<rect x="0" y="-4" width="8" height="8" rx="2" fill="#6d78d0" opacity="0.7"/><text x="12" y="3">imports</text>`;
446
+ svg += `<rect x="60" y="-4" width="8" height="8" rx="2" fill="#d97706" opacity="0.7"/><text x="72" y="3">mutual (cycle)</text>`;
447
+ svg += `</g>`;
448
+ return `<svg viewBox="0 0 ${W} ${H + 30}" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:${W}px">${svg}</svg>`;
449
+ }
450
+ // ── Package Nesting Diagram ──────────────────────────────────────────
451
+ // UML-style Package diagram: directories as nested boxes, files as items inside.
452
+ export function generatePackageDiagram(details) {
453
+ const graph = details.graph;
454
+ if (!graph || Object.keys(graph).length === 0)
455
+ return "";
456
+ const entries = Object.entries(graph);
457
+ if (entries.length > 50)
458
+ return `<div style="color:#6b7280;font-size:0.75rem">${entries.length} modules — too many for package view.</div>`;
459
+ // Group by directory
460
+ const dirs = new Map();
461
+ for (const [path, info] of entries) {
462
+ const dir = info.dir || ".";
463
+ const arr = dirs.get(dir) || [];
464
+ arr.push({ path, fanIn: info.importedBy.length, fanOut: info.imports.length });
465
+ dirs.set(dir, arr);
466
+ }
467
+ const dirEntries = [...dirs.entries()].sort((a, b) => a[0].localeCompare(b[0]));
468
+ const boxW = 180;
469
+ const fileH = 18;
470
+ const headerH = 24;
471
+ const gap = 16;
472
+ const cols = Math.min(dirEntries.length, 4);
473
+ const colW = boxW + gap;
474
+ let svg = "";
475
+ let maxH = 0;
476
+ for (let i = 0; i < dirEntries.length; i++) {
477
+ const [dir, files] = dirEntries[i];
478
+ const col = i % cols;
479
+ const row = Math.floor(i / cols);
480
+ const prevRowsH = row * 300; // rough estimate, will adjust
481
+ const x = gap + col * colW;
482
+ let y = gap + prevRowsH;
483
+ const boxH = headerH + files.length * fileH + 8;
484
+ // Package box
485
+ svg += `<rect x="${x}" y="${y}" width="${boxW}" height="${boxH}" rx="6" fill="#ffffff04" stroke="#ffffff10"/>`;
486
+ // Package tab (UML-style)
487
+ svg += `<rect x="${x}" y="${y}" width="${Math.min(boxW * 0.6, 100)}" height="${headerH}" rx="4" fill="#ffffff08" stroke="#ffffff10"/>`;
488
+ const label = dir === "." ? "root" : dir.replace(/^src\//, "");
489
+ svg += `<text x="${x + 8}" y="${y + 16}" fill="#9ca3af" font-size="10" font-weight="700">${label}/</text>`;
490
+ svg += `<text x="${x + boxW - 8}" y="${y + 16}" text-anchor="end" fill="#4b5563" font-size="8">${files.length}</text>`;
491
+ y += headerH + 4;
492
+ // Files inside package
493
+ for (const f of files) {
494
+ const name = basename(f.path, extname(f.path));
495
+ const health = f.fanIn > 5 ? "#d97706" : f.fanOut > 8 ? "#ca8a04" : "#6d78d0";
496
+ svg += `<circle cx="${x + 12}" cy="${y + 7}" r="3" fill="${health}"/>`;
497
+ svg += `<text x="${x + 20}" y="${y + 10}" fill="#9ca3af" font-size="8">${name}</text>`;
498
+ svg += `<text x="${x + boxW - 8}" y="${y + 10}" text-anchor="end" fill="#4b5563" font-size="7">${f.fanIn}\u2190 ${f.fanOut}\u2192</text>`;
499
+ y += fileH;
500
+ }
501
+ maxH = Math.max(maxH, y + 8);
292
502
  }
293
- return `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:${W}px;background:#09090b;border-radius:8px;border:1px solid #1e1e24">${groups}${edges}${nodesSvg}</svg>`;
503
+ const W = gap + cols * colW;
504
+ const H = maxH + gap;
505
+ return `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:${W}px">${svg}</svg>`;
294
506
  }
@@ -0,0 +1,17 @@
1
+ /** Code Coherence — detects internal contradictions and inconsistencies in the codebase.
2
+ *
3
+ * Premium feature (powered by LLM). Analyzes the codebase for patterns where
4
+ * different parts of the code contradict each other:
5
+ * - Function A validates input X, but function B that calls A re-validates X differently
6
+ * - Type says field is required, but all usages treat it as optional
7
+ * - Error handling is strict in module A but permissive in module B for the same operations
8
+ * - Naming conventions differ across modules (camelCase vs snake_case for same concepts)
9
+ * - Two implementations of the same algorithm with different behavior
10
+ * - Config declares a feature flag but no code reads it
11
+ * - Dead branches: conditions that can never be true given the types
12
+ * - Contradictory defaults (module A defaults timeout to 5s, module B to 30s)
13
+ *
14
+ * Currently returns a "coming soon" placeholder.
15
+ */
16
+ import type { CheckResult } from "../types.js";
17
+ export declare function runCodeCoherence(cwd: string): CheckResult;
@@ -0,0 +1,39 @@
1
+ /** Code Coherence — detects internal contradictions and inconsistencies in the codebase.
2
+ *
3
+ * Premium feature (powered by LLM). Analyzes the codebase for patterns where
4
+ * different parts of the code contradict each other:
5
+ * - Function A validates input X, but function B that calls A re-validates X differently
6
+ * - Type says field is required, but all usages treat it as optional
7
+ * - Error handling is strict in module A but permissive in module B for the same operations
8
+ * - Naming conventions differ across modules (camelCase vs snake_case for same concepts)
9
+ * - Two implementations of the same algorithm with different behavior
10
+ * - Config declares a feature flag but no code reads it
11
+ * - Dead branches: conditions that can never be true given the types
12
+ * - Contradictory defaults (module A defaults timeout to 5s, module B to 30s)
13
+ *
14
+ * Currently returns a "coming soon" placeholder.
15
+ */
16
+ import { getProductionFiles } from "../fs-utils.js";
17
+ export function runCodeCoherence(cwd) {
18
+ const start = Date.now();
19
+ // Gather basic stats even in placeholder mode
20
+ const files = getProductionFiles(cwd);
21
+ const totalExports = files.reduce((s, f) => s + (f.content.match(/\bexport\s+/g) || []).length, 0);
22
+ const totalFunctions = files.reduce((s, f) => s + (f.content.match(/\bfunction\s+\w+/g) || []).length, 0);
23
+ return {
24
+ name: "code-coherence",
25
+ score: 0,
26
+ grade: "F",
27
+ details: {
28
+ premium: true,
29
+ comingSoon: true,
30
+ reason: "LLM-powered analysis — coming soon",
31
+ filesAnalyzed: files.length,
32
+ totalExports,
33
+ totalFunctions,
34
+ description: "Detects internal contradictions: inconsistent validation, conflicting defaults, naming drift, dead config flags, and behavioral mismatches across modules.",
35
+ },
36
+ issues: [],
37
+ duration: Date.now() - start,
38
+ };
39
+ }
@@ -1,6 +1,5 @@
1
1
  /** Complexity analysis — counts lines, functions, and cognitive complexity via AST-free heuristics. */
2
- import { readdirSync, readFileSync, statSync } from "node:fs";
3
- import { extname, join } from "node:path";
2
+ import { getProductionFiles } from "../fs-utils.js";
4
3
  import { gradeFromScore } from "../types.js";
5
4
  const MAX_FUNCTION_LINES = 60;
6
5
  const MAX_COMPLEXITY = 15;
@@ -8,27 +7,15 @@ export function runComplexity(cwd) {
8
7
  const start = Date.now();
9
8
  const issues = [];
10
9
  const functions = [];
11
- // Find all TS/JS source files (not tests, not node_modules)
12
- const srcDirs = ["src", "web/src"];
13
- const files = [];
14
- for (const dir of srcDirs) {
15
- try {
16
- collectFiles(join(cwd, dir), files);
17
- }
18
- catch {
19
- /* dir doesn't exist */
20
- }
21
- }
10
+ const sourceFiles = getProductionFiles(cwd);
22
11
  let totalLines = 0;
23
- const totalFiles = files.length;
12
+ const totalFiles = sourceFiles.length;
24
13
  let longFunctions = 0;
25
14
  let complexFunctions = 0;
26
- for (const file of files) {
27
- const content = readFileSync(file, "utf-8");
28
- const lines = content.split("\n");
29
- totalLines += lines.length;
15
+ for (const sf of sourceFiles) {
16
+ totalLines += sf.lines;
30
17
  // Simple heuristic: find function boundaries and measure complexity
31
- const funcs = extractFunctions(content, file.replace(`${cwd}/`, ""));
18
+ const funcs = extractFunctions(sf.content, sf.path);
32
19
  for (const f of funcs) {
33
20
  functions.push(f);
34
21
  if (f.lines > MAX_FUNCTION_LINES) {
@@ -68,23 +55,6 @@ export function runComplexity(cwd) {
68
55
  duration: Date.now() - start,
69
56
  };
70
57
  }
71
- function collectFiles(dir, out) {
72
- for (const entry of readdirSync(dir)) {
73
- if (entry === "node_modules" || entry === "dist" || entry === ".git")
74
- continue;
75
- const full = join(dir, entry);
76
- const stat = statSync(full);
77
- if (stat.isDirectory()) {
78
- collectFiles(full, out);
79
- }
80
- else {
81
- const ext = extname(entry);
82
- if ((ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx") && !entry.includes(".test.") && !entry.includes(".spec.")) {
83
- out.push(full);
84
- }
85
- }
86
- }
87
- }
88
58
  /** Simple heuristic function extraction — not a full AST parser but good enough for metrics. */
89
59
  function extractFunctions(content, filePath) {
90
60
  const funcs = [];
@@ -138,7 +108,7 @@ function measureComplexity(code) {
138
108
  for (const line of lines) {
139
109
  const trimmed = line.trim();
140
110
  // +1 for each branch/loop keyword
141
- if (/\b(if|else if|else|switch|for|while|do|catch|&&|\|\||[?]:)\b/.test(trimmed)) {
111
+ if (/\b(if|else if|else|switch|for|while|do|catch)\b/.test(trimmed) || /&&|\|\|/.test(trimmed)) {
142
112
  complexity++;
143
113
  }
144
114
  // +1 for ternary
@@ -12,8 +12,7 @@
12
12
  * 3. Export name collisions (same name exported from multiple files)
13
13
  * 4. Ambiguous abbreviations (auth, config, ctx, etc. outside conventional scope)
14
14
  */
15
- import { readdirSync, readFileSync, statSync } from "node:fs";
16
- import { basename, extname, join } from "node:path";
15
+ import { getProductionFiles } from "../fs-utils.js";
17
16
  import { gradeFromScore } from "../types.js";
18
17
  // ── Pattern dictionaries ──
19
18
  const SYNONYM_PAIRS = [
@@ -97,16 +96,8 @@ const AMBIGUOUS_ABBREVS = {
97
96
  export function runConfusion(cwd) {
98
97
  const start = Date.now();
99
98
  const issues = [];
100
- const files = [];
101
- const dirs = ["src", "web/src"];
102
- for (const dir of dirs) {
103
- try {
104
- collectFiles(join(cwd, dir), cwd, files);
105
- }
106
- catch {
107
- /* dir doesn't exist */
108
- }
109
- }
99
+ const sourceFiles = getProductionFiles(cwd);
100
+ const files = sourceFiles.map((sf) => ({ path: sf.path, base: sf.base, content: sf.content, exports: extractExports(sf.content) }));
110
101
  if (files.length === 0) {
111
102
  return {
112
103
  name: "confusion",
@@ -263,22 +254,3 @@ function extractExports(content) {
263
254
  }
264
255
  return exports;
265
256
  }
266
- function collectFiles(dir, cwd, out) {
267
- for (const entry of readdirSync(dir)) {
268
- if (entry === "node_modules" || entry === "dist" || entry === ".git")
269
- continue;
270
- const full = join(dir, entry);
271
- if (statSync(full).isDirectory()) {
272
- collectFiles(full, cwd, out);
273
- }
274
- else {
275
- const ext = extname(entry);
276
- if ([".ts", ".tsx", ".js", ".jsx"].includes(ext) && !entry.includes(".test.") && !entry.includes(".spec.")) {
277
- const content = readFileSync(full, "utf-8");
278
- const relPath = full.replace(`${cwd}/`, "");
279
- const base = basename(entry, ext);
280
- out.push({ path: relPath, base, content, exports: extractExports(content) });
281
- }
282
- }
283
- }
284
- }
@@ -11,8 +11,7 @@
11
11
  * 3. File self-containment — ratio of local symbols to imported symbols
12
12
  * 4. Circular dependencies — import cycles that confuse navigation
13
13
  */
14
- import { readdirSync, readFileSync, statSync } from "node:fs";
15
- import { extname, join } from "node:path";
14
+ import { getProductionFiles } from "../fs-utils.js";
16
15
  import { gradeFromScore } from "../types.js";
17
16
  const MAX_FILE_TOKENS = 4000; // ~400 lines; beyond this LLMs lose mid-context info
18
17
  const MAX_IMPORTS = 15; // files importing >15 modules are hard to reason about
@@ -21,16 +20,13 @@ export function runContext(cwd) {
21
20
  const start = Date.now();
22
21
  const issues = [];
23
22
  // Collect source files with imports
24
- const files = [];
25
- const dirs = ["src", "web/src"];
26
- for (const dir of dirs) {
27
- try {
28
- collectFiles(join(cwd, dir), cwd, files);
29
- }
30
- catch {
31
- /* dir doesn't exist */
32
- }
33
- }
23
+ const sourceFiles = getProductionFiles(cwd);
24
+ const files = sourceFiles.map((sf) => ({
25
+ path: sf.path,
26
+ content: sf.content,
27
+ imports: parseImports(sf.content),
28
+ tokens: Math.round(sf.content.length / CHARS_PER_TOKEN),
29
+ }));
34
30
  if (files.length === 0) {
35
31
  return {
36
32
  name: "context",
@@ -157,14 +153,8 @@ function resolveImport(fromPath, importPath) {
157
153
  parts.pop();
158
154
  resolved = [...parts, importPath.slice(3)].join("/");
159
155
  }
160
- // Strip extension and try common ones
156
+ // Strip known extension if present, then default to .ts
161
157
  resolved = resolved.replace(/\.(js|ts|tsx|jsx)$/, "");
162
- const extensions = [".ts", ".tsx", ".js", ".jsx"];
163
- for (const ext of extensions) {
164
- if (resolved.endsWith(ext.replace(".", "")))
165
- return resolved;
166
- }
167
- // Return with .ts as default assumption
168
158
  return `${resolved}.ts`;
169
159
  }
170
160
  // ── Cycle detection (DFS) ──
@@ -201,24 +191,3 @@ function findCycles(graph) {
201
191
  }
202
192
  return cycles;
203
193
  }
204
- // ── File collection ──
205
- function collectFiles(dir, cwd, out) {
206
- for (const entry of readdirSync(dir)) {
207
- if (entry === "node_modules" || entry === "dist" || entry === ".git")
208
- continue;
209
- const full = join(dir, entry);
210
- if (statSync(full).isDirectory()) {
211
- collectFiles(full, cwd, out);
212
- }
213
- else {
214
- const ext = extname(entry);
215
- if ([".ts", ".tsx", ".js", ".jsx"].includes(ext) && !entry.includes(".test.") && !entry.includes(".spec.")) {
216
- const content = readFileSync(full, "utf-8");
217
- const relPath = full.replace(`${cwd}/`, "");
218
- const imports = parseImports(content);
219
- const tokens = Math.round(content.length / CHARS_PER_TOKEN);
220
- out.push({ path: relPath, content, imports, tokens });
221
- }
222
- }
223
- }
224
- }
@@ -5,6 +5,34 @@ export function runDependencies(cwd, stack) {
5
5
  const start = Date.now();
6
6
  const issues = [];
7
7
  const pm = stack.packageManager;
8
+ // Dart/Flutter: skip npm audit, just check pubspec for outdated
9
+ if (pm === "pub") {
10
+ const outdatedResult = run("dart pub outdated --json 2>/dev/null || true", cwd);
11
+ let outdatedCount = 0;
12
+ let majorOutdated = 0;
13
+ try {
14
+ const data = JSON.parse(outdatedResult.stdout);
15
+ for (const pkg of data.packages || []) {
16
+ if (pkg.current?.version && pkg.latest?.version && pkg.current.version !== pkg.latest.version) {
17
+ outdatedCount++;
18
+ if (pkg.current.version.split(".")[0] !== pkg.latest.version.split(".")[0])
19
+ majorOutdated++;
20
+ }
21
+ }
22
+ }
23
+ catch { /* parse failed */ }
24
+ if (majorOutdated > 0)
25
+ issues.push({ severity: "warning", message: `${majorOutdated} packages behind by a major version` });
26
+ const score = Math.max(0, Math.min(100, 100 - majorOutdated));
27
+ return {
28
+ name: "dependencies",
29
+ score,
30
+ grade: gradeFromScore(score),
31
+ details: { vulnerabilities: { critical: 0, high: 0, moderate: 0, low: 0 }, outdated: outdatedCount, majorOutdated },
32
+ issues,
33
+ duration: Date.now() - start,
34
+ };
35
+ }
8
36
  // Vulnerability audit
9
37
  const auditCmd = pm === "pnpm" ? "pnpm audit --json" : pm === "yarn" ? "yarn audit --json" : "npm audit --json";
10
38
  const auditResult = run(`${auditCmd} 2>/dev/null || true`, cwd);
@@ -0,0 +1,14 @@
1
+ /** Doc Coherence — detects contradictions between documentation and code.
2
+ *
3
+ * Premium feature (powered by LLM). Scans README, CLAUDE.md, JSDoc, and inline
4
+ * comments for claims that contradict the actual code:
5
+ * - README says "supports X" but feature X was removed
6
+ * - JSDoc says "@param required" but param has a default
7
+ * - Comment says "never throws" but function has throw statements
8
+ * - CHANGELOG references files that no longer exist
9
+ * - API docs describe endpoints/functions that were renamed or deleted
10
+ *
11
+ * Currently returns a "coming soon" placeholder.
12
+ */
13
+ import type { CheckResult } from "../types.js";
14
+ export declare function runDocCoherence(cwd: string): CheckResult;