@kb-labs/quality-entry 2.93.0 → 2.96.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 (98) hide show
  1. package/dist/cli/commands/build-order.d.ts +5 -12
  2. package/dist/cli/commands/build-order.js +45 -97
  3. package/dist/cli/commands/build-order.js.map +1 -1
  4. package/dist/cli/commands/check-layers.d.ts +9 -0
  5. package/dist/cli/commands/check-layers.js +48 -0
  6. package/dist/cli/commands/check-layers.js.map +1 -0
  7. package/dist/cli/commands/context.d.ts +9 -0
  8. package/dist/cli/commands/context.js +94 -0
  9. package/dist/cli/commands/context.js.map +1 -0
  10. package/dist/cli/commands/coupling.d.ts +9 -0
  11. package/dist/cli/commands/coupling.js +35 -0
  12. package/dist/cli/commands/coupling.js.map +1 -0
  13. package/dist/cli/commands/dead-code.d.ts +4 -5
  14. package/dist/cli/commands/dead-code.js +41 -197
  15. package/dist/cli/commands/dead-code.js.map +1 -1
  16. package/dist/cli/commands/fix-deps.d.ts +2 -5
  17. package/dist/cli/commands/fix-deps.js +74 -83
  18. package/dist/cli/commands/fix-deps.js.map +1 -1
  19. package/dist/cli/commands/flags.d.ts +115 -1
  20. package/dist/cli/commands/flags.js +46 -1
  21. package/dist/cli/commands/flags.js.map +1 -1
  22. package/dist/cli/commands/gate.d.ts +9 -0
  23. package/dist/cli/commands/gate.js +59 -0
  24. package/dist/cli/commands/gate.js.map +1 -0
  25. package/dist/cli/commands/health.d.ts +4 -5
  26. package/dist/cli/commands/health.js +42 -191
  27. package/dist/cli/commands/health.js.map +1 -1
  28. package/dist/cli/commands/history.d.ts +9 -0
  29. package/dist/cli/commands/history.js +55 -0
  30. package/dist/cli/commands/history.js.map +1 -0
  31. package/dist/cli/commands/index.d.ts +6 -2
  32. package/dist/cli/commands/index.js +411 -828
  33. package/dist/cli/commands/index.js.map +1 -1
  34. package/dist/cli/commands/snapshot.d.ts +9 -0
  35. package/dist/cli/commands/snapshot.js +91 -0
  36. package/dist/cli/commands/snapshot.js.map +1 -0
  37. package/dist/cli/commands/stats.d.ts +4 -5
  38. package/dist/cli/commands/stats.js +17 -265
  39. package/dist/cli/commands/stats.js.map +1 -1
  40. package/dist/index.d.ts +1 -1
  41. package/dist/index.js +198 -306
  42. package/dist/index.js.map +1 -1
  43. package/dist/manifest.d.ts +95 -82
  44. package/dist/manifest.js +198 -306
  45. package/dist/manifest.js.map +1 -1
  46. package/dist/rest/handlers/build-order-handler.d.ts +5 -13
  47. package/dist/rest/handlers/build-order-handler.js +14 -17
  48. package/dist/rest/handlers/build-order-handler.js.map +1 -1
  49. package/dist/rest/handlers/builds-handler.js +10 -6
  50. package/dist/rest/handlers/builds-handler.js.map +1 -1
  51. package/dist/rest/handlers/coupling-handler.d.ts +8 -0
  52. package/dist/rest/handlers/coupling-handler.js +28 -0
  53. package/dist/rest/handlers/coupling-handler.js.map +1 -0
  54. package/dist/rest/handlers/cycles-handler.js +16 -12
  55. package/dist/rest/handlers/cycles-handler.js.map +1 -1
  56. package/dist/rest/handlers/dependencies-handler.js +12 -8
  57. package/dist/rest/handlers/dependencies-handler.js.map +1 -1
  58. package/dist/rest/handlers/graph-handler.js +77 -73
  59. package/dist/rest/handlers/graph-handler.js.map +1 -1
  60. package/dist/rest/handlers/health-handler.d.ts +4 -11
  61. package/dist/rest/handlers/health-handler.js +35 -9
  62. package/dist/rest/handlers/health-handler.js.map +1 -1
  63. package/dist/rest/handlers/history-handler.d.ts +7 -0
  64. package/dist/rest/handlers/history-handler.js +21 -0
  65. package/dist/rest/handlers/history-handler.js.map +1 -0
  66. package/dist/rest/handlers/knip-handler.d.ts +8 -0
  67. package/dist/rest/handlers/knip-handler.js +31 -0
  68. package/dist/rest/handlers/knip-handler.js.map +1 -0
  69. package/dist/rest/handlers/layers-handler.d.ts +8 -0
  70. package/dist/rest/handlers/layers-handler.js +28 -0
  71. package/dist/rest/handlers/layers-handler.js.map +1 -0
  72. package/dist/rest/handlers/snapshot-handler.d.ts +7 -0
  73. package/dist/rest/handlers/snapshot-handler.js +67 -0
  74. package/dist/rest/handlers/snapshot-handler.js.map +1 -0
  75. package/dist/rest/handlers/stale-handler.js +13 -9
  76. package/dist/rest/handlers/stale-handler.js.map +1 -1
  77. package/dist/rest/handlers/stats-handler.d.ts +4 -9
  78. package/dist/rest/handlers/stats-handler.js +22 -18
  79. package/dist/rest/handlers/stats-handler.js.map +1 -1
  80. package/dist/rest/handlers/tests-handler.js +12 -8
  81. package/dist/rest/handlers/tests-handler.js.map +1 -1
  82. package/dist/rest/handlers/types-handler.js +10 -6
  83. package/dist/rest/handlers/types-handler.js.map +1 -1
  84. package/dist/widgets/117.js +24 -1
  85. package/dist/widgets/117.js.map +1 -1
  86. package/dist/widgets/370.js +1 -1
  87. package/dist/widgets/370.js.map +1 -1
  88. package/dist/widgets/62.js +97 -0
  89. package/dist/widgets/62.js.map +1 -0
  90. package/dist/widgets/__federation_expose_QualityOverview.js +1 -1
  91. package/dist/widgets/__federation_expose_QualityOverview.js.map +1 -1
  92. package/dist/widgets/mf-manifest.json +27 -27
  93. package/dist/widgets/mf-stats.json +27 -27
  94. package/dist/widgets/remoteEntry.js +2 -2
  95. package/dist/widgets/remoteEntry.js.map +1 -1
  96. package/package.json +9 -8
  97. package/dist/widgets/418.js +0 -39
  98. package/dist/widgets/418.js.map +0 -1
@@ -1,12 +1,12 @@
1
- import { defineCommand, useLoader } from '@kb-labs/sdk';
2
- import { buildDependencyGraph, getBuildOrderForPackage, topologicalSort, findCircularDependencies, getReverseDependencies, getImpactAnalysis } from '@kb-labs/quality-core/graph';
3
- import { CACHE_KEYS } from '@kb-labs/quality-contracts';
1
+ import { defineCommand, useConfig, validationError, useLoader } from '@kb-labs/sdk';
2
+ import { analyzeBuildOrder, analyzeLayering, analyzeCoupling, runKnip, analyzeTypes as analyzeTypes$1, calculateHealth, QualitySnapshotStore, calculateStats } from '@kb-labs/quality-core';
3
+ import { defaultQualityConfig, CACHE_KEYS, CACHE_TTLS } from '@kb-labs/quality-contracts';
4
4
  import { checkBuilds } from '@kb-labs/quality-core/builds';
5
5
  import { runTests } from '@kb-labs/quality-core/tests';
6
6
  import { analyzeTypes } from '@kb-labs/quality-core/types';
7
- import { listBackups, restoreFromBackup, scanDeadFiles, removeDeadFiles } from '@kb-labs/quality-core/dead-code';
7
+ import { buildDependencyGraph, findCircularDependencies, getReverseDependencies, getImpactAnalysis } from '@kb-labs/quality-core/graph';
8
8
  import fs from 'fs';
9
- import path3 from 'path';
9
+ import path from 'path';
10
10
 
11
11
  // src/cli/commands/build-order.ts
12
12
  var build_order_default = defineCommand({
@@ -14,106 +14,53 @@ var build_order_default = defineCommand({
14
14
  description: "Calculate build order using topological sort",
15
15
  handler: {
16
16
  async execute(ctx, input) {
17
- const { ui } = ctx;
18
- const flags = "flags" in input && typeof input.flags === "object" && input.flags !== null ? input.flags : input;
19
- const graph = buildDependencyGraph(ctx.cwd);
20
- let result;
21
- if (flags.package) {
22
- result = getBuildOrderForPackage(graph, flags.package);
23
- } else {
24
- result = topologicalSort(graph);
25
- }
26
- if (result.circular.length > 0) {
27
- ui?.error?.(
28
- `Found ${result.circular.length} circular dependencies. Build order cannot be determined.`
29
- );
30
- outputCircularDependencies(result.circular, ui);
31
- return { exitCode: 1, result };
32
- }
33
- outputBuildOrder(result, flags, ui);
34
- return { exitCode: 0, result };
35
- }
36
- }
37
- });
38
- function outputBuildOrder(result, flags, ui) {
39
- if (flags.json) {
40
- ui?.json?.(result);
41
- return;
42
- }
43
- if (flags.script) {
44
- ui?.write?.("#!/bin/bash");
45
- ui?.write?.("# Generated build script");
46
- ui?.write?.("set -e");
47
- ui?.write?.("");
48
- for (let i = 0; i < result.layers.length; i++) {
49
- const layer = result.layers[i];
50
- if (!layer) {
51
- continue;
52
- }
53
- ui?.write?.(`# Layer ${i + 1} (${layer.length} packages)`);
54
- for (const pkg of layer) {
55
- ui?.write?.(`pnpm --filter "${pkg}" run build`);
17
+ const { flags } = input;
18
+ const config = await useConfig();
19
+ const cfg = config ?? defaultQualityConfig;
20
+ const cwd = ctx.cwd ?? process.cwd();
21
+ const result = analyzeBuildOrder({
22
+ rootDir: cwd,
23
+ layerMap: cfg.layers,
24
+ filterPackage: flags.package
25
+ });
26
+ if (result.hasCircular) {
27
+ if (flags.json) {
28
+ ctx.ui?.json?.({ ...result, ok: false });
29
+ } else {
30
+ ctx.ui?.error?.(`Circular dependencies detected \u2014 build order cannot be fully determined`, {
31
+ sections: result.circular.map((cycle, i) => ({
32
+ header: `Cycle ${i + 1}`,
33
+ items: cycle
34
+ }))
35
+ });
36
+ }
37
+ return { exitCode: 1 };
56
38
  }
57
- ui?.write?.("");
58
- }
59
- return;
60
- }
61
- const sections = [];
62
- if (flags.layers) {
63
- const layerItems = [];
64
- for (let i = 0; i < result.layers.length; i++) {
65
- const layer = result.layers[i];
66
- if (!layer) {
67
- continue;
39
+ if (flags.json) {
40
+ ctx.ui?.json?.(result);
41
+ return { exitCode: 0 };
68
42
  }
69
- layerItems.push(`Layer ${i + 1}: ${layer.length} packages (can build in parallel)`);
70
- for (const pkg of layer) {
71
- layerItems.push(` \u2022 ${pkg}`);
43
+ if (flags.script) {
44
+ for (let i = 0; i < result.layers.length; i++) {
45
+ const layer = result.layers[i];
46
+ if (!layer) continue;
47
+ ctx.ui?.success?.(`# Layer ${i + 1}`, { sections: [{ header: "", items: layer.map((p) => `pnpm --filter "${p}" run build`) }] });
48
+ }
49
+ return { exitCode: 0 };
72
50
  }
73
- layerItems.push("");
74
- }
75
- sections.push({ header: "Build Layers", items: layerItems });
76
- } else {
77
- const orderItems = result.sorted.map((pkg, idx) => `${idx + 1}. ${pkg}`);
78
- sections.push({ header: "Build Order", items: orderItems });
79
- }
80
- sections.push({
81
- header: "Summary",
82
- items: [
83
- `Total packages: ${result.sorted.length}`,
84
- `Build layers: ${result.layers.length}`,
85
- `Circular dependencies: ${result.circular.length}`
86
- ]
87
- });
88
- const title = flags.package ? `\u{1F4E6} Build Order for ${flags.package}` : "\u{1F4E6} Monorepo Build Order";
89
- ui?.success?.("Build order calculated successfully", {
90
- title,
91
- sections
92
- });
93
- }
94
- function outputCircularDependencies(cycles, ui) {
95
- const sections = [];
96
- for (let i = 0; i < cycles.length; i++) {
97
- const cycle = cycles[i];
98
- if (!cycle) {
99
- continue;
51
+ const sections = flags.layers ? result.layers.map((layer, i) => ({
52
+ header: `Layer ${i + 1} \u2014 ${layer.length} package(s) (parallel)`,
53
+ items: layer
54
+ })) : [{ header: "Build order", items: result.sorted.map((p, i) => `${i + 1}. ${p}`) }];
55
+ sections.push({
56
+ header: "Summary",
57
+ items: [`${result.packageCount} packages in ${result.layerCount} layers`]
58
+ });
59
+ ctx.ui?.success?.(flags.package ? `Build order for ${flags.package}` : "Monorepo build order", { sections });
60
+ return { exitCode: 0 };
100
61
  }
101
- sections.push({
102
- header: `Cycle ${i + 1}`,
103
- items: cycle.map((pkg, idx) => {
104
- if (idx === cycle.length - 1) {
105
- const firstPkg = cycle[0];
106
- return ` ${pkg} \u2192 ${firstPkg ?? "?"} (circular!)`;
107
- }
108
- return ` ${pkg} \u2192`;
109
- })
110
- });
111
62
  }
112
- ui?.error?.("Circular dependencies detected", {
113
- title: "\u26A0\uFE0F Circular Dependencies",
114
- sections
115
- });
116
- }
63
+ });
117
64
  var check_builds_default = defineCommand({
118
65
  id: "quality:check-builds",
119
66
  description: "Check build status across monorepo",
@@ -402,6 +349,73 @@ function outputTypeAnalysis(result, flags, ui) {
402
349
  sections
403
350
  });
404
351
  }
352
+ var check_layers_default = defineCommand({
353
+ id: "quality:check-layers",
354
+ description: "Detect layering violations (lower layer importing higher layer)",
355
+ handler: {
356
+ async execute(ctx, input) {
357
+ const { flags } = input;
358
+ const config = await useConfig();
359
+ const cwd = ctx.cwd ?? process.cwd();
360
+ const report = await analyzeLayering({
361
+ rootDir: cwd,
362
+ layerMap: config?.layers
363
+ });
364
+ const filtered = flags.package ? {
365
+ ...report,
366
+ violations: report.violations.filter((v) => v.fromPackage.includes(flags.package)),
367
+ totalViolations: report.violations.filter((v) => v.fromPackage.includes(flags.package)).length
368
+ } : report;
369
+ if (flags.json) {
370
+ ctx.ui?.json?.(filtered);
371
+ return { exitCode: filtered.totalViolations > 0 ? 1 : 0 };
372
+ }
373
+ if (filtered.totalViolations === 0) {
374
+ ctx.ui?.success?.("No layering violations found");
375
+ return { exitCode: 0 };
376
+ }
377
+ const byPackage = /* @__PURE__ */ new Map();
378
+ for (const v of filtered.violations) {
379
+ const list = byPackage.get(v.fromPackage) ?? [];
380
+ list.push(v);
381
+ byPackage.set(v.fromPackage, list);
382
+ }
383
+ const sections = [...byPackage.entries()].map(([pkg, vs]) => ({
384
+ header: `${pkg} (Layer ${vs[0]?.fromLayer})`,
385
+ items: vs.map((v) => `\u2192 ${v.toPackage} (Layer ${v.toLayer}) ${v.importSpecifier}`)
386
+ }));
387
+ ctx.ui?.error?.(`${filtered.totalViolations} layering violation(s) in ${filtered.affectedPackages.length} package(s)`, { sections });
388
+ return { exitCode: 1 };
389
+ }
390
+ }
391
+ });
392
+ var coupling_default = defineCommand({
393
+ id: "quality:coupling",
394
+ description: "Show coupling metrics per package (afferent/efferent/instability)",
395
+ handler: {
396
+ async execute(ctx, input) {
397
+ const { flags } = input;
398
+ const config = await useConfig();
399
+ const cwd = ctx.cwd ?? process.cwd();
400
+ const report = analyzeCoupling({ rootDir: cwd, layerMap: config?.layers, topN: flags.top });
401
+ if (flags.json) {
402
+ ctx.ui?.json?.(report);
403
+ return { exitCode: 0 };
404
+ }
405
+ const sorted = flags.sort === "coupled" ? [...report.packages].sort((a, b) => b.afferent + b.efferent - (a.afferent + a.efferent)) : [...report.packages].sort((a, b) => b.instability - a.instability);
406
+ const top = sorted.slice(0, flags.top ?? 10);
407
+ const threshold = config?.thresholds?.instability ?? 0.85;
408
+ const items = top.map((p) => {
409
+ const warn = p.instability > threshold ? " \u26A0 unstable" : "";
410
+ return `${p.name} Ca: ${p.afferent} Ce: ${p.efferent} I: ${p.instability.toFixed(2)}${warn}`;
411
+ });
412
+ ctx.ui?.success?.(`Coupling \u2014 avg instability: ${report.avgInstability.toFixed(2)}`, {
413
+ sections: [{ header: `Top ${top.length} by instability`, items }]
414
+ });
415
+ return { exitCode: 0 };
416
+ }
417
+ }
418
+ });
405
419
  var cycles_default = defineCommand({
406
420
  id: "quality:cycles",
407
421
  description: "Detect circular dependencies in monorepo",
@@ -481,220 +495,93 @@ function outputCycles(cycles, flags, ui) {
481
495
  }
482
496
  var dead_code_default = defineCommand({
483
497
  id: "quality:dead-code",
484
- description: "Detect and optionally remove dead (unreachable) source files",
498
+ description: "Detect unused files, exports, and deps via knip",
485
499
  handler: {
486
500
  async execute(ctx, input) {
487
- const { ui, platform } = ctx;
488
- const flags = "flags" in input && typeof input.flags === "object" && input.flags !== null ? input.flags : input;
489
- if (flags["list-backups"]) {
490
- const backups = listBackups(ctx.cwd);
491
- if (flags.json) {
492
- ui?.json?.(backups);
493
- } else {
494
- outputBackupList(backups, ui);
495
- }
496
- return { exitCode: 0, result: backups };
501
+ const { flags } = input;
502
+ const config = await useConfig();
503
+ const cwd = ctx.cwd ?? process.cwd();
504
+ const cfg = config ?? defaultQualityConfig;
505
+ if (!cfg.knip.enabled) {
506
+ ctx.ui?.success?.("knip is disabled in config (knip.enabled = false)");
507
+ return { exitCode: 0 };
497
508
  }
498
- if (flags.restore) {
499
- try {
500
- const result2 = await restoreFromBackup(ctx.cwd, flags.restore);
501
- if (flags.json) {
502
- ui?.json?.(result2);
503
- } else {
504
- ui?.success?.(`Restored ${result2.restoredFiles} file(s) from backup ${flags.restore}`, {});
505
- if (result2.restoredExports > 0) {
506
- ui?.info?.(`Note: ${result2.restoredExports} package.json export(s) were cleaned. You may need to rebuild.`);
507
- }
508
- }
509
- return { exitCode: 0 };
510
- } catch (err) {
511
- if (flags.json) {
512
- ui?.json?.({ error: err instanceof Error ? err.message : String(err) });
513
- } else {
514
- ui?.error?.(err instanceof Error ? err.message : String(err));
515
- }
516
- return { exitCode: 1 };
517
- }
509
+ const report = await runKnip({ shell: ctx.api.shell, rootDir: cwd });
510
+ if (flags.json) {
511
+ ctx.ui?.json?.(report);
512
+ return { exitCode: report.totalIssues > 0 ? 1 : 0 };
518
513
  }
519
- const cacheKey = `${CACHE_KEYS.DEAD_CODE}:${flags.package || "all"}`;
520
- if (!flags.refresh && !flags["auto-remove"]) {
521
- const cached = await platform.cache.get(cacheKey);
522
- if (cached) {
523
- outputDeadCodeReport({ ...cached, cached: true }, flags, ui);
524
- return { exitCode: cached.summary.totalDead > 0 ? 1 : 0, result: cached };
525
- }
514
+ if (report.totalIssues === 0) {
515
+ ctx.ui?.success?.("No dead code detected");
516
+ return { exitCode: 0 };
526
517
  }
527
- const result = await scanDeadFiles(ctx.cwd, {
528
- packageFilter: flags.package,
529
- verbose: flags.verbose
530
- });
531
- await platform.cache.set(cacheKey, result, 5 * 60 * 1e3);
532
- await platform.analytics.track("quality:dead-code", {
533
- totalPackages: result.summary.totalPackages,
534
- totalFiles: result.summary.totalFiles,
535
- totalDead: result.summary.totalDead,
536
- totalDeadBytes: result.summary.totalDeadBytes,
537
- duration: result.duration,
538
- autoRemove: !!flags["auto-remove"],
539
- dryRun: !!flags["dry-run"]
540
- });
541
- if (flags["auto-remove"]) {
542
- if (result.summary.totalDead === 0) {
543
- if (flags.json) {
544
- ui?.json?.({ message: "No dead files found", ...result });
545
- } else {
546
- ui?.success?.("No dead files found. Nothing to remove.", {});
547
- }
548
- return { exitCode: 0, result };
549
- }
550
- const removalResult = await removeDeadFiles(ctx.cwd, result, {
551
- dryRun: flags["dry-run"]
518
+ const sections = [];
519
+ if (report.unusedFiles.length > 0) {
520
+ sections.push({
521
+ header: `Unused files (${report.unusedFiles.length})`,
522
+ items: report.unusedFiles
552
523
  });
553
- if (flags.json) {
554
- ui?.json?.(removalResult);
555
- } else {
556
- outputRemovalReport(removalResult, flags, ui);
557
- }
558
- return { exitCode: 0, result: removalResult };
559
524
  }
560
- outputDeadCodeReport({ ...result, cached: false }, flags, ui);
561
- return {
562
- exitCode: result.summary.totalDead > 0 ? 1 : 0,
563
- result
564
- };
565
- }
566
- }
567
- });
568
- function outputDeadCodeReport(result, flags, ui) {
569
- if (flags.json) {
570
- ui?.json?.(result);
571
- return;
572
- }
573
- const sections = [];
574
- const packagesWithDead = result.packages.filter((p) => p.deadFiles.length > 0);
575
- if (packagesWithDead.length > 0) {
576
- const items = [];
577
- for (const pkg of packagesWithDead) {
578
- items.push(`${pkg.packageName} (${pkg.deadFiles.length} dead file${pkg.deadFiles.length === 1 ? "" : "s"})`);
579
- for (const dead of pkg.deadFiles.slice(0, 5)) {
580
- const sizeKb = (dead.sizeBytes / 1024).toFixed(1);
581
- items.push(` ${dead.relativePath} (${sizeKb} KB)`);
525
+ if (report.unusedExports.length > 0) {
526
+ sections.push({
527
+ header: `Unused exports (${report.unusedExports.length})`,
528
+ items: report.unusedExports.map((e) => `${e.file} ${e.symbol}`)
529
+ });
582
530
  }
583
- if (pkg.deadFiles.length > 5) {
584
- items.push(` ... and ${pkg.deadFiles.length - 5} more`);
531
+ if (report.unusedDependencies.length > 0) {
532
+ sections.push({
533
+ header: `Unused dependencies (${report.unusedDependencies.length})`,
534
+ items: report.unusedDependencies.map((d) => `${d.package} in ${d.workspace}`)
535
+ });
585
536
  }
586
- }
587
- sections.push({ header: "Dead Files", items });
588
- }
589
- if (flags.verbose) {
590
- for (const pkg of result.packages) {
591
- if (pkg.entryPoints.length > 0) {
592
- const items = pkg.entryPoints.slice(0, 10).map((ep) => ` ${ep}`);
593
- if (pkg.entryPoints.length > 10) {
594
- items.push(` ... and ${pkg.entryPoints.length - 10} more`);
595
- }
596
- sections.push({ header: `Entry Points: ${pkg.packageName}`, items });
537
+ if (report.unlistedDependencies.length > 0) {
538
+ sections.push({
539
+ header: `Unlisted dependencies (${report.unlistedDependencies.length})`,
540
+ items: report.unlistedDependencies.map((d) => `${d.package} in ${d.workspace}`)
541
+ });
597
542
  }
543
+ ctx.ui?.error?.(`${report.totalIssues} dead code issue(s) found`, { sections });
544
+ return { exitCode: 1 };
598
545
  }
599
546
  }
600
- const allWarnings = result.packages.flatMap((p) => p.warnings);
601
- if (allWarnings.length > 0) {
602
- sections.push({
603
- header: "Warnings",
604
- items: allWarnings.slice(0, 10)
605
- });
606
- }
607
- const totalDeadKb = (result.summary.totalDeadBytes / 1024).toFixed(1);
608
- const summaryItems = [
609
- `Packages scanned: ${result.summary.totalPackages}`,
610
- `Total source files: ${result.summary.totalFiles}`,
611
- `Alive files: ${result.summary.totalAlive}`,
612
- `Dead files: ${result.summary.totalDead}`,
613
- result.summary.totalDead > 0 ? `Dead code size: ${totalDeadKb} KB` : null,
614
- result.summary.emptyDirectories.length > 0 ? `Empty directories: ${result.summary.emptyDirectories.length}` : null,
615
- `Duration: ${(result.duration / 1e3).toFixed(1)}s`,
616
- result.cached ? "(cached \u2014 use --refresh to rescan)" : null
617
- ].filter(Boolean);
618
- sections.push({ header: "Summary", items: summaryItems });
619
- if (result.summary.totalDead > 0) {
620
- sections.push({
621
- header: "Next Steps",
622
- items: [
623
- "Run with --auto-remove --dry-run to preview removal",
624
- "Run with --auto-remove to remove and create backup",
625
- "Run with --verbose to see entry points"
626
- ]
627
- });
628
- }
629
- const title = result.summary.totalDead === 0 ? "No dead files found" : `${result.summary.totalDead} dead file(s) found`;
630
- ui?.success?.("Dead code analysis completed", { title, sections });
631
- }
632
- function outputRemovalReport(result, flags, ui) {
633
- const isDryRun = flags["dry-run"];
634
- const prefix = isDryRun ? "[DRY RUN] Would" : "Successfully";
635
- const sections = [];
636
- const items = [
637
- `${prefix} remove ${result.filesRemoved} file(s)`,
638
- `${prefix} free ${(result.bytesRemoved / 1024).toFixed(1)} KB`
639
- ];
640
- if (result.emptyDirsRemoved > 0) {
641
- items.push(`${prefix} remove ${result.emptyDirsRemoved} empty director${result.emptyDirsRemoved === 1 ? "y" : "ies"}`);
642
- }
643
- if (result.exportsCleanedUp > 0) {
644
- items.push(`${prefix} clean ${result.exportsCleanedUp} package.json export(s)`);
645
- }
646
- sections.push({ header: isDryRun ? "Preview" : "Removal Complete", items });
647
- if (!isDryRun) {
648
- sections.push({
649
- header: "Backup",
650
- items: [
651
- `Backup ID: ${result.backupId}`,
652
- `To restore: pnpm kb quality:dead-code --restore ${result.backupId}`
653
- ]
654
- });
655
- }
656
- const fileItems = [];
657
- for (const file of result.manifest.removedFiles.slice(0, 10)) {
658
- const sizeKb = (file.sizeBytes / 1024).toFixed(1);
659
- fileItems.push(`${file.backupPath} (${sizeKb} KB)`);
660
- }
661
- if (result.manifest.removedFiles.length > 10) {
662
- fileItems.push(`... and ${result.manifest.removedFiles.length - 10} more`);
663
- }
664
- sections.push({ header: "Files", items: fileItems });
665
- const title = isDryRun ? `[DRY RUN] Would remove ${result.filesRemoved} file(s)` : `Removed ${result.filesRemoved} file(s)`;
666
- ui?.success?.(title, { title, sections });
667
- }
668
- function outputBackupList(backups, ui) {
669
- if (backups.length === 0) {
670
- ui?.info?.("No backups found.");
671
- return;
672
- }
673
- const sections = [];
674
- for (const backup of backups) {
675
- const date = new Date(backup.createdAt).toLocaleString();
676
- const items = [
677
- `Date: ${date}`,
678
- `Git: ${backup.gitBranch} @ ${backup.gitSha.slice(0, 8)}`,
679
- `Files: ${backup.totalFilesRemoved}`,
680
- `Size: ${(backup.totalBytesRemoved / 1024).toFixed(1)} KB`,
681
- `Restore: pnpm kb quality:dead-code --restore ${backup.id}`
682
- ];
683
- sections.push({ header: `Backup: ${backup.id}`, items });
684
- }
685
- ui?.success?.(`${backups.length} backup(s) found`, {
686
- title: "Dead Code Backups",
687
- sections
688
- });
689
- }
547
+ });
690
548
  var fix_deps_default = defineCommand({
691
549
  id: "quality:fix-deps",
692
550
  description: "Auto-fix dependency issues",
693
551
  handler: {
552
+ async intent(_ctx, input) {
553
+ const flags = input.flags;
554
+ const removeUnused = flags["remove-unused"] || flags.all;
555
+ const addMissing = flags["add-missing"] || flags.all;
556
+ const alignVersions = flags["align-versions"] || flags.all;
557
+ const packages = findPackages(_ctx.cwd);
558
+ const preview = await runAnalysis(packages, { removeUnused, addMissing, alignVersions, dryRun: true });
559
+ const operations = [
560
+ ...preview.alignedDeps.map((a) => ({
561
+ type: "update",
562
+ resource: "dependency",
563
+ details: { dep: a.dep, from: a.from, to: a.to, packages: a.packages }
564
+ })),
565
+ ...preview.addedDeps.map((a) => ({
566
+ type: "create",
567
+ resource: "dependency",
568
+ details: { package: a.package, dep: a.dep, version: a.version }
569
+ })),
570
+ ...preview.removedDeps.map((r) => ({
571
+ type: "delete",
572
+ resource: "dependency",
573
+ details: { package: r.package, dep: r.dep }
574
+ }))
575
+ ];
576
+ const total = operations.length;
577
+ return {
578
+ summary: total > 0 ? `Fix ${total} dependency issue(s) across ${preview.packagesScanned} packages` : `No dependency issues found in ${preview.packagesScanned} packages`,
579
+ operations
580
+ };
581
+ },
694
582
  async execute(ctx, input) {
695
583
  const { ui, platform } = ctx;
696
- const flags = "flags" in input && typeof input.flags === "object" && input.flags !== null ? input.flags : input;
697
- const dryRun = flags["dry-run"] ?? false;
584
+ const flags = input.flags;
698
585
  const removeUnused = flags["remove-unused"] || flags.all;
699
586
  const addMissing = flags["add-missing"] || flags.all;
700
587
  const alignVersions = flags["align-versions"] || flags.all;
@@ -705,52 +592,45 @@ var fix_deps_default = defineCommand({
705
592
  return { exitCode: 0 };
706
593
  }
707
594
  if (!removeUnused && !addMissing && !alignVersions) {
708
- ui?.error?.("No fix options specified. Use --remove-unused, --add-missing, --align-versions, or --all");
595
+ validationError(ctx, "No fix options specified", "Use --remove-unused, --add-missing, --align-versions, or --all", flags.json);
709
596
  return { exitCode: 1 };
710
597
  }
711
598
  const loader = useLoader("Scanning packages...");
712
599
  loader.start();
713
- const result = {
714
- packagesScanned: 0,
715
- removedDeps: [],
716
- addedDeps: [],
717
- alignedDeps: [],
718
- dryRun
719
- };
720
600
  const packages = findPackages(ctx.cwd);
721
- result.packagesScanned = packages.length;
722
601
  loader.update({ text: `Scanned ${packages.length} packages` });
723
- if (alignVersions) {
724
- loader.update({ text: "Analyzing duplicate dependencies..." });
725
- const aligned = await alignDuplicateVersions(packages, dryRun);
726
- result.alignedDeps = aligned;
727
- }
728
- if (addMissing) {
729
- loader.update({ text: "Checking for missing workspace dependencies..." });
730
- const added = await addMissingWorkspaceDeps(packages, dryRun);
731
- result.addedDeps = added;
732
- }
733
- if (removeUnused) {
734
- loader.update({ text: "Checking for unused dependencies..." });
735
- const removed = await removeUnusedDeps(packages, dryRun);
736
- result.removedDeps = removed;
737
- }
602
+ const result = await runAnalysis(packages, { removeUnused, addMissing, alignVersions, dryRun: false });
738
603
  loader.succeed("Dependency analysis completed");
739
604
  await platform.analytics.track("quality:fix-deps", {
740
- dryRun,
605
+ dryRun: false,
741
606
  packagesScanned: result.packagesScanned,
742
607
  removedCount: result.removedDeps.length,
743
608
  addedCount: result.addedDeps.length,
744
609
  alignedCount: result.alignedDeps.length
745
610
  });
746
- outputResults(result, flags, ui);
747
- return {
748
- exitCode: 0,
749
- result
750
- };
611
+ outputResults({ ...result, dryRun: false }, flags, ui);
612
+ return { exitCode: 0, result };
751
613
  }
752
614
  }
753
615
  });
616
+ async function runAnalysis(packages, opts) {
617
+ const result = {
618
+ packagesScanned: packages.length,
619
+ removedDeps: [],
620
+ addedDeps: [],
621
+ alignedDeps: []
622
+ };
623
+ if (opts.alignVersions) {
624
+ result.alignedDeps = await alignDuplicateVersions(packages, opts.dryRun);
625
+ }
626
+ if (opts.addMissing) {
627
+ result.addedDeps = await addMissingWorkspaceDeps(packages, opts.dryRun);
628
+ }
629
+ if (opts.removeUnused) {
630
+ result.removedDeps = await removeUnusedDeps(packages, opts.dryRun);
631
+ }
632
+ return result;
633
+ }
754
634
  function findPackages(rootDir) {
755
635
  const packages = [];
756
636
  if (!fs.existsSync(rootDir)) {
@@ -761,8 +641,8 @@ function findPackages(rootDir) {
761
641
  if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
762
642
  continue;
763
643
  }
764
- const repoPath = path3.join(rootDir, entry.name);
765
- const packagesDir = path3.join(repoPath, "packages");
644
+ const repoPath = path.join(rootDir, entry.name);
645
+ const packagesDir = path.join(repoPath, "packages");
766
646
  if (!fs.existsSync(packagesDir)) {
767
647
  continue;
768
648
  }
@@ -771,7 +651,7 @@ function findPackages(rootDir) {
771
651
  if (!pkgDir.isDirectory()) {
772
652
  continue;
773
653
  }
774
- const packageJsonPath = path3.join(packagesDir, pkgDir.name, "package.json");
654
+ const packageJsonPath = path.join(packagesDir, pkgDir.name, "package.json");
775
655
  if (fs.existsSync(packageJsonPath)) {
776
656
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
777
657
  packages.push({
@@ -845,12 +725,7 @@ async function alignDuplicateVersions(packages, dryRun) {
845
725
  continue;
846
726
  }
847
727
  for (const { version, packages: pkgNames } of versionCounts.slice(1)) {
848
- aligned.push({
849
- dep,
850
- from: version,
851
- to: targetVersion,
852
- packages: pkgNames
853
- });
728
+ aligned.push({ dep, from: version, to: targetVersion, packages: pkgNames });
854
729
  if (!dryRun) {
855
730
  for (const pkgName of pkgNames) {
856
731
  const pkg = packages.find((p) => p.name === pkgName);
@@ -879,8 +754,8 @@ async function addMissingWorkspaceDeps(packages, dryRun) {
879
754
  }
880
755
  }
881
756
  for (const pkg of packages) {
882
- const packageDir = path3.dirname(pkg.path);
883
- const srcDir = path3.join(packageDir, "src");
757
+ const packageDir = path.dirname(pkg.path);
758
+ const srcDir = path.join(packageDir, "src");
884
759
  if (!fs.existsSync(srcDir)) {
885
760
  continue;
886
761
  }
@@ -891,11 +766,7 @@ async function addMissingWorkspaceDeps(packages, dryRun) {
891
766
  };
892
767
  for (const imp of imports) {
893
768
  if (imp.startsWith("@kb-labs/") && workspacePackages.has(imp) && !currentDeps[imp]) {
894
- added.push({
895
- package: pkg.name,
896
- dep: imp,
897
- version: "workspace:*"
898
- });
769
+ added.push({ package: pkg.name, dep: imp, version: "workspace:*" });
899
770
  if (!dryRun) {
900
771
  if (!pkg.json.dependencies) {
901
772
  pkg.json.dependencies = {};
@@ -911,8 +782,8 @@ async function addMissingWorkspaceDeps(packages, dryRun) {
911
782
  async function removeUnusedDeps(packages, dryRun) {
912
783
  const removed = [];
913
784
  for (const pkg of packages) {
914
- const packageDir = path3.dirname(pkg.path);
915
- const srcDir = path3.join(packageDir, "src");
785
+ const packageDir = path.dirname(pkg.path);
786
+ const srcDir = path.join(packageDir, "src");
916
787
  if (!fs.existsSync(srcDir)) {
917
788
  continue;
918
789
  }
@@ -926,10 +797,7 @@ async function removeUnusedDeps(packages, dryRun) {
926
797
  continue;
927
798
  }
928
799
  if (!imports.has(dep)) {
929
- removed.push({
930
- package: pkg.name,
931
- dep
932
- });
800
+ removed.push({ package: pkg.name, dep });
933
801
  if (!dryRun) {
934
802
  delete pkg.json.dependencies?.[dep];
935
803
  delete pkg.json.devDependencies?.[dep];
@@ -945,7 +813,7 @@ function scanImports(dir) {
945
813
  function walk(currentDir) {
946
814
  const entries = fs.readdirSync(currentDir, { withFileTypes: true });
947
815
  for (const entry of entries) {
948
- const fullPath = path3.join(currentDir, entry.name);
816
+ const fullPath = path.join(currentDir, entry.name);
949
817
  if (entry.isDirectory()) {
950
818
  walk(fullPath);
951
819
  } else if (entry.isFile() && /\.(ts|tsx|js|jsx)$/.test(entry.name)) {
@@ -1005,7 +873,7 @@ function outputStats(stats, flags, ui) {
1005
873
  });
1006
874
  }
1007
875
  ui?.success?.("Dependency statistics", {
1008
- title: "\u{1F4CA} Dependency Statistics",
876
+ title: "Dependency Statistics",
1009
877
  sections
1010
878
  });
1011
879
  }
@@ -1015,530 +883,245 @@ function outputResults(result, flags, ui) {
1015
883
  return;
1016
884
  }
1017
885
  const sections = [];
1018
- const summaryItems = [
1019
- `Packages Scanned: ${result.packagesScanned}`,
1020
- `Mode: ${result.dryRun ? "\u{1F50D} Dry Run (no changes applied)" : "\u2705 Changes Applied"}`
1021
- ];
1022
- sections.push({ header: "Summary", items: summaryItems });
886
+ sections.push({
887
+ header: "Summary",
888
+ items: [`Packages Scanned: ${result.packagesScanned}`]
889
+ });
1023
890
  if (result.alignedDeps.length > 0) {
1024
- const alignedItems = result.alignedDeps.map(
1025
- (a) => `${a.dep}: ${a.from} \u2192 ${a.to} (${a.packages.length} packages)`
1026
- );
1027
- sections.push({ header: "Aligned Versions", items: alignedItems.slice(0, 10) });
891
+ const items = result.alignedDeps.slice(0, 10).map((a) => `${a.dep}: ${a.from} \u2192 ${a.to} (${a.packages.length} packages)`);
1028
892
  if (result.alignedDeps.length > 10) {
1029
- const lastSection = sections[sections.length - 1];
1030
- if (lastSection) {
1031
- lastSection.items.push(`... and ${result.alignedDeps.length - 10} more`);
1032
- }
893
+ items.push(`... and ${result.alignedDeps.length - 10} more`);
1033
894
  }
895
+ sections.push({ header: "Aligned Versions", items });
1034
896
  }
1035
897
  if (result.addedDeps.length > 0) {
1036
- const addedItems = result.addedDeps.map((a) => `${a.package}: +${a.dep}`);
1037
- sections.push({ header: "Added Dependencies", items: addedItems.slice(0, 10) });
898
+ const items = result.addedDeps.slice(0, 10).map((a) => `${a.package}: +${a.dep}`);
1038
899
  if (result.addedDeps.length > 10) {
1039
- const lastSection = sections[sections.length - 1];
1040
- if (lastSection) {
1041
- lastSection.items.push(`... and ${result.addedDeps.length - 10} more`);
1042
- }
900
+ items.push(`... and ${result.addedDeps.length - 10} more`);
1043
901
  }
902
+ sections.push({ header: "Added Dependencies", items });
1044
903
  }
1045
904
  if (result.removedDeps.length > 0) {
1046
- const removedItems = result.removedDeps.map((r) => `${r.package}: -${r.dep}`);
1047
- sections.push({ header: "Removed Dependencies", items: removedItems.slice(0, 10) });
905
+ const items = result.removedDeps.slice(0, 10).map((r) => `${r.package}: -${r.dep}`);
1048
906
  if (result.removedDeps.length > 10) {
1049
- const lastSection = sections[sections.length - 1];
1050
- if (lastSection) {
1051
- lastSection.items.push(`... and ${result.removedDeps.length - 10} more`);
1052
- }
907
+ items.push(`... and ${result.removedDeps.length - 10} more`);
1053
908
  }
909
+ sections.push({ header: "Removed Dependencies", items });
1054
910
  }
1055
911
  if (result.alignedDeps.length === 0 && result.addedDeps.length === 0 && result.removedDeps.length === 0) {
1056
- sections.push({ header: "Result", items: ["\u2705 No issues found!"] });
912
+ sections.push({ header: "Result", items: ["No issues found!"] });
1057
913
  }
1058
- if (result.dryRun && (result.alignedDeps.length > 0 || result.addedDeps.length > 0 || result.removedDeps.length > 0)) {
1059
- sections.push({
1060
- header: "Next Steps",
1061
- items: ["Remove --dry-run flag to apply changes", "Run `pnpm install` after applying changes"]
1062
- });
1063
- }
1064
- const title = result.dryRun ? "\u{1F50D} Dependency Fix Preview" : "\u2705 Dependency Fix Complete";
1065
914
  ui?.success?.("Dependency fix completed", {
1066
- title,
915
+ title: "Dependency Fix Complete",
1067
916
  sections
1068
917
  });
1069
918
  }
1070
919
  var health_default = defineCommand({
1071
920
  id: "quality:health",
1072
- description: "Check monorepo health score",
921
+ description: "Multidimensional health score: architecture, TypeScript, dead code, dependencies",
1073
922
  handler: {
1074
923
  async execute(ctx, input) {
1075
- const { ui, platform } = ctx;
1076
- const flags = "flags" in input && typeof input.flags === "object" && input.flags !== null ? input.flags : input;
1077
- const health = await calculateHealth(ctx.cwd, flags.package);
1078
- await platform.cache.set(CACHE_KEYS.HEALTH, health, 5 * 60 * 1e3);
1079
- await platform.analytics.track("quality:health", {
1080
- score: health.score,
1081
- grade: health.grade,
1082
- issueCount: health.issues.length,
1083
- packageSpecific: !!flags.package
1084
- });
1085
- outputHealth(health, flags, ui);
1086
- return {
1087
- exitCode: health.score < 60 ? 1 : 0,
1088
- result: health
1089
- };
1090
- }
1091
- }
1092
- });
1093
- function findPackages2(rootDir, specificPackage) {
1094
- const packages = [];
1095
- if (!fs.existsSync(rootDir)) {
1096
- return packages;
1097
- }
1098
- const entries = fs.readdirSync(rootDir, { withFileTypes: true });
1099
- for (const entry of entries) {
1100
- if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
1101
- continue;
1102
- }
1103
- const repoPath = path3.join(rootDir, entry.name);
1104
- const packagesDir = path3.join(repoPath, "packages");
1105
- if (!fs.existsSync(packagesDir)) {
1106
- continue;
1107
- }
1108
- const packageDirs = fs.readdirSync(packagesDir, { withFileTypes: true });
1109
- for (const pkgDir of packageDirs) {
1110
- if (!pkgDir.isDirectory()) {
1111
- continue;
924
+ const { flags } = input;
925
+ const config = await useConfig();
926
+ const cwd = ctx.cwd ?? process.cwd();
927
+ const cfg = config ?? defaultQualityConfig;
928
+ const cached = await ctx.platform.cache.get(CACHE_KEYS.HEALTH);
929
+ if (cached && !flags.refresh) {
930
+ const health2 = cached;
931
+ outputHealth(health2, flags, ctx);
932
+ return { exitCode: health2.score < cfg.thresholds.health ? 1 : 0 };
1112
933
  }
1113
- const packageJsonPath = path3.join(packagesDir, pkgDir.name, "package.json");
1114
- if (fs.existsSync(packageJsonPath)) {
1115
- if (specificPackage) {
1116
- const pkgJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
1117
- if (pkgJson.name === specificPackage || pkgDir.name === specificPackage) {
1118
- packages.push(packageJsonPath);
1119
- }
1120
- } else {
1121
- packages.push(packageJsonPath);
1122
- }
934
+ const [layering, types] = await Promise.all([
935
+ analyzeLayering({ rootDir: cwd, layerMap: cfg.layers }),
936
+ analyzeTypes$1(cwd)
937
+ ]);
938
+ const coupling = analyzeCoupling({ rootDir: cwd, layerMap: cfg.layers });
939
+ let knip = { unusedFiles: [], unusedExports: [], unusedDependencies: [], unlistedDependencies: [], totalIssues: 0 };
940
+ if (cfg.knip.enabled) {
941
+ knip = await runKnip({ shell: ctx.api.shell, rootDir: cwd });
1123
942
  }
943
+ const health = calculateHealth({
944
+ layering,
945
+ coupling,
946
+ types,
947
+ knip,
948
+ totalPackages: coupling.packages.length,
949
+ avgTestCoverage: null,
950
+ thresholds: cfg.thresholds
951
+ });
952
+ await ctx.platform.cache.set(CACHE_KEYS.HEALTH, health, CACHE_TTLS.FAST);
953
+ outputHealth(health, flags, ctx);
954
+ return { exitCode: health.score < cfg.thresholds.health ? 1 : 0 };
1124
955
  }
1125
956
  }
1126
- return packages;
1127
- }
1128
- async function calculateHealth(rootDir, specificPackage) {
1129
- const packages = findPackages2(rootDir, specificPackage);
1130
- let score = 100;
1131
- const issues = [];
1132
- const allDeps = /* @__PURE__ */ new Map();
1133
- for (const packagePath of packages) {
1134
- const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
1135
- const deps = {
1136
- ...packageJson.dependencies,
1137
- ...packageJson.devDependencies
1138
- };
1139
- for (const [dep, version] of Object.entries(deps)) {
1140
- if (!allDeps.has(dep)) {
1141
- allDeps.set(dep, /* @__PURE__ */ new Set());
1142
- }
1143
- allDeps.get(dep).add(version);
1144
- }
1145
- }
1146
- const duplicates = Array.from(allDeps.entries()).filter(([_, versions]) => versions.size > 1).filter(([dep]) => !dep.startsWith("@kb-labs/"));
1147
- if (duplicates.length > 0) {
1148
- const penalty = Math.min(20, duplicates.length * 2);
1149
- score -= penalty;
1150
- issues.push({
1151
- type: "duplicates",
1152
- severity: "warning",
1153
- message: `${duplicates.length} duplicate dependencies (-${penalty})`
1154
- });
1155
- }
1156
- let missingReadmes = 0;
1157
- for (const packagePath of packages) {
1158
- const packageDir = path3.dirname(packagePath);
1159
- const readmePath = path3.join(packageDir, "README.md");
1160
- if (!fs.existsSync(readmePath)) {
1161
- missingReadmes++;
1162
- }
1163
- }
1164
- if (missingReadmes > 0) {
1165
- const penalty = Math.min(15, missingReadmes);
1166
- score -= penalty;
1167
- issues.push({
1168
- type: "missing-docs",
1169
- severity: "warning",
1170
- message: `${missingReadmes} packages missing README (-${penalty})`
1171
- });
1172
- }
1173
- let namingIssues = 0;
1174
- for (const packagePath of packages) {
1175
- const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
1176
- const packageName = packageJson.name;
1177
- if (!packageName || !packageName.startsWith("@kb-labs/")) {
1178
- namingIssues++;
1179
- }
1180
- }
1181
- if (namingIssues > 0) {
1182
- const penalty = Math.min(10, namingIssues * 2);
1183
- score -= penalty;
1184
- issues.push({
1185
- type: "naming",
1186
- severity: "critical",
1187
- message: `${namingIssues} packages with naming issues (-${penalty})`
1188
- });
1189
- }
1190
- let missingTypes = 0;
1191
- for (const packagePath of packages) {
1192
- const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
1193
- if (!packageJson.types && !packageJson.typings) {
1194
- missingTypes++;
1195
- }
1196
- }
1197
- if (missingTypes > 0) {
1198
- const penalty = Math.min(15, missingTypes);
1199
- score -= penalty;
1200
- issues.push({
1201
- type: "missing-types",
1202
- severity: "warning",
1203
- message: `${missingTypes} packages missing types field (-${penalty})`
1204
- });
1205
- }
1206
- let grade;
1207
- if (score >= 90) {
1208
- grade = "A";
1209
- } else if (score >= 80) {
1210
- grade = "B";
1211
- } else if (score >= 70) {
1212
- grade = "C";
1213
- } else if (score >= 60) {
1214
- grade = "D";
1215
- } else {
1216
- grade = "F";
1217
- }
1218
- return {
1219
- score: Math.max(0, score),
1220
- grade,
1221
- issues
1222
- };
1223
- }
1224
- function outputHealth(health, flags, ui) {
957
+ });
958
+ function outputHealth(health, flags, ctx) {
1225
959
  if (flags.json) {
1226
- ui?.json?.(health);
960
+ ctx.ui?.json?.(health);
1227
961
  return;
1228
962
  }
1229
- const sections = [];
1230
- const scoreItems = [
1231
- `Score: ${health.score}/100`,
1232
- `Grade: ${health.grade}`,
1233
- `Status: ${health.score >= 80 ? "\u2705 Healthy" : health.score >= 60 ? "\u26A0\uFE0F Needs Attention" : "\u274C Critical"}`
1234
- ];
1235
- sections.push({ header: "Health Score", items: scoreItems });
1236
- if (health.issues.length > 0) {
1237
- const issueItems = health.issues.map((issue) => {
1238
- const icon = issue.severity === "critical" ? "\u274C" : issue.severity === "warning" ? "\u26A0\uFE0F" : "\u2139\uFE0F";
1239
- return `${icon} ${issue.message}`;
1240
- });
1241
- if (flags.detailed) {
1242
- sections.push({ header: "Issues (Detailed)", items: issueItems });
1243
- } else {
1244
- sections.push({ header: "Issues", items: issueItems });
1245
- }
1246
- } else {
1247
- sections.push({ header: "Issues", items: ["\u2705 No issues found!"] });
1248
- }
1249
- if (health.score < 90) {
1250
- const recommendations = [];
1251
- if (health.issues.some((i) => i.type === "duplicates")) {
1252
- recommendations.push("Run `kb quality:fix-deps --align-versions` to fix duplicates");
1253
- }
1254
- if (health.issues.some((i) => i.type === "missing-docs")) {
1255
- recommendations.push("Add README.md files to packages");
1256
- }
1257
- if (health.issues.some((i) => i.type === "missing-types")) {
1258
- recommendations.push('Add "types" field to package.json or enable dts generation');
1259
- }
1260
- if (recommendations.length > 0) {
1261
- sections.push({ header: "Recommendations", items: recommendations });
1262
- }
1263
- }
1264
- const title = health.score >= 80 ? "\u{1F49A} Monorepo Health Check - Healthy" : health.score >= 60 ? "\u{1F49B} Monorepo Health Check - Needs Attention" : "\u2764\uFE0F Monorepo Health Check - Critical";
1265
- ui?.success?.("Health check completed", {
1266
- title,
1267
- sections
963
+ const dimItems = Object.entries(health.dimensions).map(([key, dim]) => {
964
+ const details = dim.details.length > 0 ? `: ${dim.details.join(", ")}` : "";
965
+ return `${key} ${dim.score}/100 (${dim.grade})${details}`;
966
+ });
967
+ ctx.ui?.success?.(`Health score: ${health.score}/100 (${health.grade})`, {
968
+ sections: [{ header: "Dimensions", items: dimItems }]
1268
969
  });
1269
970
  }
1270
- var stats_default = defineCommand({
1271
- id: "quality:stats",
1272
- description: "Show monorepo statistics and health score",
971
+ var history_default = defineCommand({
972
+ id: "quality:history",
973
+ description: "Show quality snapshot history and trends",
1273
974
  handler: {
1274
975
  async execute(ctx, input) {
1275
- const { ui, platform } = ctx;
1276
- const flags = "flags" in input && typeof input.flags === "object" && input.flags !== null ? input.flags : input;
1277
- if (!flags.refresh) {
1278
- const cached = await platform.cache.get(CACHE_KEYS.STATS);
1279
- if (cached) {
1280
- outputStats2(cached, flags, ui);
1281
- return { exitCode: 0 };
1282
- }
976
+ const { flags } = input;
977
+ const config = await useConfig();
978
+ const cwd = ctx.cwd ?? process.cwd();
979
+ const store = new QualitySnapshotStore(cwd, config?.maxSnapshots ?? defaultQualityConfig.maxSnapshots);
980
+ const { snapshots, delta, latest } = store.history();
981
+ if (snapshots.length === 0) {
982
+ ctx.ui?.success?.("No snapshots yet. Run `kb quality snapshot` first.");
983
+ return { exitCode: 0 };
1283
984
  }
1284
- const stats = await collectStats(ctx.cwd);
1285
- if (flags.health) {
1286
- stats.health = calculateHealthScore(stats);
985
+ const limit = flags.limit ?? 10;
986
+ const recent = snapshots.slice(-limit).reverse();
987
+ if (flags.json) {
988
+ ctx.ui?.json?.({ snapshots: recent, delta, latest });
989
+ return { exitCode: 0 };
1287
990
  }
1288
- await platform.cache.set(CACHE_KEYS.STATS, stats, 5 * 60 * 1e3);
1289
- await platform.analytics.track("quality:stats", {
1290
- packages: stats.overview.totalPackages,
1291
- repositories: stats.overview.totalRepositories,
1292
- withHealth: flags.health ?? false
991
+ const items = recent.map((s) => {
992
+ const date = new Date(s.timestamp).toLocaleString();
993
+ const branch = s.git.branch ? ` [${s.git.branch}]` : "";
994
+ return `${date}${branch} \u2014 ${s.score}/100 (${s.grade}) violations: ${s.counters.layeringViolations} any: ${s.counters.anyCount}`;
1293
995
  });
1294
- outputStats2(stats, flags, ui);
996
+ const sections = [
997
+ { header: `Last ${recent.length} snapshots`, items }
998
+ ];
999
+ if (delta) {
1000
+ const sign = (n) => n > 0 ? `+${n}` : `${n}`;
1001
+ sections.push({
1002
+ header: "Delta vs previous snapshot",
1003
+ items: [
1004
+ `Score: ${sign(delta.score)}`,
1005
+ `Layering violations: ${sign(delta.layeringViolations)}`,
1006
+ `Any count: ${sign(delta.anyCount)}`,
1007
+ `Unused files: ${sign(delta.unusedFiles)}`,
1008
+ `Avg instability: ${sign(delta.avgInstability)}`
1009
+ ]
1010
+ });
1011
+ }
1012
+ ctx.ui?.success?.(`Quality history (${recent.length} snapshots)`, { sections });
1295
1013
  return { exitCode: 0 };
1296
1014
  }
1297
1015
  }
1298
1016
  });
1299
- function findPackages3(rootDir) {
1300
- const packages = [];
1301
- if (!fs.existsSync(rootDir)) {
1302
- return packages;
1303
- }
1304
- const entries = fs.readdirSync(rootDir, { withFileTypes: true });
1305
- for (const entry of entries) {
1306
- if (!entry.isDirectory() || !entry.name.startsWith("kb-labs-")) {
1307
- continue;
1308
- }
1309
- const repoPath = path3.join(rootDir, entry.name);
1310
- const packagesDir = path3.join(repoPath, "packages");
1311
- if (!fs.existsSync(packagesDir)) {
1312
- continue;
1313
- }
1314
- const packageDirs = fs.readdirSync(packagesDir, { withFileTypes: true });
1315
- for (const pkgDir of packageDirs) {
1316
- if (!pkgDir.isDirectory()) {
1317
- continue;
1318
- }
1319
- const packageJsonPath = path3.join(packagesDir, pkgDir.name, "package.json");
1320
- if (fs.existsSync(packageJsonPath)) {
1321
- packages.push(packageJsonPath);
1322
- }
1323
- }
1324
- }
1325
- return packages;
1326
- }
1327
- function calculateSize(packageDir) {
1328
- const srcDir = path3.join(packageDir, "src");
1329
- if (!fs.existsSync(srcDir)) {
1330
- return { files: 0, lines: 0, bytes: 0 };
1017
+ async function captureGit(shell, cwd) {
1018
+ try {
1019
+ const [c, b, m] = await Promise.all([
1020
+ shell.exec("git", ["rev-parse", "--short", "HEAD"], { cwd, throwOnError: false }),
1021
+ shell.exec("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd, throwOnError: false }),
1022
+ shell.exec("git", ["log", "-1", "--format=%s"], { cwd, throwOnError: false })
1023
+ ]);
1024
+ if (!c.ok) return void 0;
1025
+ return { commit: c.stdout.trim(), branch: b.stdout.trim() || "HEAD", message: m.stdout.trim() };
1026
+ } catch {
1027
+ return void 0;
1331
1028
  }
1332
- let files = 0;
1333
- let lines = 0;
1334
- let bytes = 0;
1335
- function walk(dir) {
1336
- const entries = fs.readdirSync(dir, { withFileTypes: true });
1337
- for (const entry of entries) {
1338
- const fullPath = path3.join(dir, entry.name);
1339
- if (entry.isDirectory()) {
1340
- walk(fullPath);
1341
- } else if (entry.isFile() && /\.(ts|tsx|js|jsx)$/.test(entry.name)) {
1342
- files++;
1343
- const content = fs.readFileSync(fullPath, "utf-8");
1344
- lines += content.split("\n").length;
1345
- bytes += Buffer.byteLength(content, "utf-8");
1346
- }
1347
- }
1348
- }
1349
- walk(srcDir);
1350
- return { files, lines, bytes };
1351
1029
  }
1352
- async function collectStats(rootDir) {
1353
- const packages = findPackages3(rootDir);
1354
- const stats = {
1355
- overview: {
1356
- totalPackages: 0,
1357
- totalRepositories: 0,
1358
- totalFiles: 0,
1359
- totalLines: 0,
1360
- totalBytes: 0
1361
- },
1362
- byRepository: {},
1363
- dependencies: {
1364
- total: 0,
1365
- workspace: 0,
1366
- external: 0,
1367
- duplicates: 0,
1368
- topUsed: []
1369
- },
1370
- health: {
1371
- score: 0,
1372
- grade: "F",
1373
- issues: []
1374
- },
1375
- largestPackages: []
1376
- };
1377
- const repos = /* @__PURE__ */ new Set();
1378
- const allDeps = /* @__PURE__ */ new Map();
1379
- for (const packagePath of packages) {
1380
- const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
1381
- const packageName = packageJson.name;
1382
- if (!packageName || !packageName.startsWith("@kb-labs/")) {
1383
- continue;
1384
- }
1385
- stats.overview.totalPackages++;
1386
- const packageDir = path3.dirname(packagePath);
1387
- const repoName = path3.basename(path3.dirname(path3.dirname(packageDir)));
1388
- repos.add(repoName);
1389
- const size = calculateSize(packageDir);
1390
- stats.overview.totalFiles += size.files;
1391
- stats.overview.totalLines += size.lines;
1392
- stats.overview.totalBytes += size.bytes;
1393
- if (!stats.byRepository[repoName]) {
1394
- stats.byRepository[repoName] = {
1395
- name: repoName,
1396
- packages: 0,
1397
- files: 0,
1398
- lines: 0,
1399
- bytes: 0
1030
+ var snapshot_default = defineCommand({
1031
+ id: "quality:snapshot",
1032
+ description: "Collect all quality metrics and save a snapshot for trend tracking",
1033
+ handler: {
1034
+ async intent(_ctx, _input) {
1035
+ return {
1036
+ summary: "Collect all quality metrics and save snapshot for trend tracking",
1037
+ operations: [
1038
+ { type: "create", resource: "file", details: { path: ".kb/quality/snapshots/<timestamp>.json" } }
1039
+ ]
1400
1040
  };
1401
- }
1402
- stats.byRepository[repoName].packages++;
1403
- stats.byRepository[repoName].files += size.files;
1404
- stats.byRepository[repoName].lines += size.lines;
1405
- stats.byRepository[repoName].bytes += size.bytes;
1406
- const deps = {
1407
- ...packageJson.dependencies,
1408
- ...packageJson.devDependencies
1409
- };
1410
- for (const dep of Object.keys(deps)) {
1411
- stats.dependencies.total++;
1412
- if (dep.startsWith("@kb-labs/")) {
1413
- stats.dependencies.workspace++;
1414
- } else {
1415
- stats.dependencies.external++;
1041
+ },
1042
+ async execute(ctx, input) {
1043
+ const { flags } = input;
1044
+ const config = await useConfig();
1045
+ const cwd = ctx.cwd ?? process.cwd();
1046
+ const cfg = config ?? defaultQualityConfig;
1047
+ ctx.ui?.success?.("Collecting quality metrics\u2026");
1048
+ const [layering, coupling, types, git] = await Promise.all([
1049
+ analyzeLayering({ rootDir: cwd, layerMap: cfg.layers }),
1050
+ Promise.resolve(analyzeCoupling({ rootDir: cwd, layerMap: cfg.layers })),
1051
+ analyzeTypes$1(cwd),
1052
+ captureGit(ctx.api.shell, cwd)
1053
+ ]);
1054
+ let knip = { unusedFiles: [], unusedExports: [], unusedDependencies: [], unlistedDependencies: [], totalIssues: 0 };
1055
+ if (cfg.knip.enabled) {
1056
+ knip = await runKnip({ shell: ctx.api.shell, rootDir: cwd });
1416
1057
  }
1417
- allDeps.set(dep, (allDeps.get(dep) || 0) + 1);
1418
- }
1419
- stats.largestPackages.push({
1420
- name: packageName,
1421
- repository: repoName,
1422
- files: size.files,
1423
- lines: size.lines,
1424
- bytes: size.bytes
1425
- });
1426
- }
1427
- stats.overview.totalRepositories = repos.size;
1428
- stats.dependencies.duplicates = Array.from(allDeps.values()).filter((count) => count > 1).length;
1429
- stats.dependencies.topUsed = Array.from(allDeps.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([name, count]) => ({ name, count }));
1430
- stats.largestPackages.sort((a, b) => b.lines - a.lines);
1431
- stats.largestPackages = stats.largestPackages.slice(0, 10);
1432
- return stats;
1433
- }
1434
- function calculateHealthScore(stats) {
1435
- let score = 100;
1436
- const issues = [];
1437
- if (stats.dependencies.duplicates > 0) {
1438
- const penalty = Math.min(20, stats.dependencies.duplicates * 2);
1439
- score -= penalty;
1440
- issues.push({
1441
- type: "duplicates",
1442
- severity: "warning",
1443
- message: `${stats.dependencies.duplicates} duplicate dependencies (-${penalty})`
1444
- });
1445
- }
1446
- let grade;
1447
- if (score >= 90) {
1448
- grade = "A";
1449
- } else if (score >= 80) {
1450
- grade = "B";
1451
- } else if (score >= 70) {
1452
- grade = "C";
1453
- } else if (score >= 60) {
1454
- grade = "D";
1455
- } else {
1456
- grade = "F";
1457
- }
1458
- return {
1459
- score,
1460
- grade,
1461
- issues
1462
- };
1463
- }
1464
- function outputStats2(stats, flags, ui) {
1465
- if (flags.json) {
1466
- ui?.json?.(stats);
1467
- return;
1468
- }
1469
- if (flags.md) {
1470
- outputMarkdown(stats);
1471
- return;
1472
- }
1473
- const overviewItems = [
1474
- `Packages: ${stats.overview.totalPackages}`,
1475
- `Repositories: ${stats.overview.totalRepositories}`,
1476
- `Lines of Code: ${stats.overview.totalLines.toLocaleString()}`,
1477
- `Total Size: ${formatBytes(stats.overview.totalBytes)}`
1478
- ];
1479
- const sections = [
1480
- { header: "Overview", items: overviewItems }
1481
- ];
1482
- if (flags.health && stats.health) {
1483
- const healthItems = [
1484
- `Score: ${stats.health.score}/100 (Grade ${stats.health.grade})`
1485
- ];
1486
- if (stats.health.issues.length > 0) {
1487
- healthItems.push("");
1488
- healthItems.push("Issues:");
1489
- stats.health.issues.forEach((issue) => {
1490
- healthItems.push(` ${issue.message}`);
1058
+ const totalPackages = coupling.packages.length;
1059
+ const health = calculateHealth({
1060
+ layering,
1061
+ coupling,
1062
+ types,
1063
+ knip,
1064
+ totalPackages,
1065
+ avgTestCoverage: null,
1066
+ thresholds: cfg.thresholds
1491
1067
  });
1068
+ const store = new QualitySnapshotStore(cwd, cfg.maxSnapshots);
1069
+ const snap = store.save({
1070
+ score: health.score,
1071
+ grade: health.grade,
1072
+ dimensions: health.dimensions,
1073
+ counters: {
1074
+ layeringViolations: layering.totalViolations,
1075
+ avgInstability: coupling.avgInstability,
1076
+ anyCount: types.packages.reduce((s, p) => s + p.anyCount, 0),
1077
+ tsIgnoreCount: types.packages.reduce((s, p) => s + p.tsIgnoreCount, 0),
1078
+ unusedFiles: knip.unusedFiles.length,
1079
+ unusedDeps: knip.unusedDependencies.length
1080
+ },
1081
+ git: git ?? { commit: "", branch: "", message: "" }
1082
+ });
1083
+ if (flags.json) {
1084
+ ctx.ui?.json?.(snap);
1085
+ } else {
1086
+ ctx.ui?.success?.(`Snapshot saved: score ${snap.score}/100 (${snap.grade})`, {
1087
+ sections: [{
1088
+ header: "Dimensions",
1089
+ items: Object.entries(snap.dimensions).map(
1090
+ ([k, d]) => `${k} ${d.score}/100 (${d.grade})`
1091
+ )
1092
+ }]
1093
+ });
1094
+ }
1095
+ return { exitCode: 0 };
1492
1096
  }
1493
- sections.push({ header: "Health Score", items: healthItems });
1494
- }
1495
- const topRepos = Object.values(stats.byRepository).sort((a, b) => b.lines - a.lines).slice(0, 5).map((repo) => `${repo.name}: ${repo.lines.toLocaleString()} lines, ${repo.packages} pkg(s)`);
1496
- if (topRepos.length > 0) {
1497
- sections.push({ header: "Top Repositories", items: topRepos });
1498
1097
  }
1499
- ui?.success?.("Statistics collected successfully", {
1500
- title: "\u{1F4CA} KB Labs Monorepo Statistics",
1501
- sections
1502
- });
1503
- }
1504
- function outputMarkdown(stats) {
1505
- console.log("# KB Labs Monorepo Statistics\n");
1506
- console.log("## Overview\n");
1507
- console.log("| Metric | Value |");
1508
- console.log("|--------|-------|");
1509
- console.log(`| Packages | ${stats.overview.totalPackages} |`);
1510
- console.log(`| Repositories | ${stats.overview.totalRepositories} |`);
1511
- console.log(`| Lines of Code | ${stats.overview.totalLines.toLocaleString()} |`);
1512
- console.log(`| Total Size | ${formatBytes(stats.overview.totalBytes)} |
1513
- `);
1514
- if (stats.health) {
1515
- console.log("## Health Score\n");
1516
- console.log(`**${stats.health.score}/100** (Grade ${stats.health.grade})
1517
- `);
1518
- if (stats.health.issues.length > 0) {
1519
- console.log("### Issues\n");
1520
- for (const issue of stats.health.issues) {
1521
- console.log(`- ${issue.message}`);
1098
+ });
1099
+ var stats_default = defineCommand({
1100
+ id: "quality:stats",
1101
+ description: "Monorepo statistics: packages, lines of code, size",
1102
+ handler: {
1103
+ async execute(ctx, input) {
1104
+ const { flags } = input;
1105
+ const cwd = ctx.cwd ?? process.cwd();
1106
+ const stats = await calculateStats(cwd);
1107
+ if (flags.json) {
1108
+ ctx.ui?.json?.(stats);
1109
+ return { exitCode: 0 };
1522
1110
  }
1523
- console.log("");
1111
+ ctx.ui?.success?.("Monorepo statistics", {
1112
+ sections: [{
1113
+ header: "Overview",
1114
+ items: [
1115
+ `Packages: ${stats.packages}`,
1116
+ `Lines of code: ${stats.loc.toLocaleString()}`,
1117
+ `Size: ${stats.sizeFormatted}`
1118
+ ]
1119
+ }]
1120
+ });
1121
+ return { exitCode: 0 };
1524
1122
  }
1525
1123
  }
1526
- console.log("## By Repository\n");
1527
- console.log("| Repository | Packages | Lines | Size |");
1528
- console.log("|------------|----------|-------|------|");
1529
- for (const [name, repo] of Object.entries(stats.byRepository)) {
1530
- console.log(`| ${name} | ${repo.packages} | ${repo.lines.toLocaleString()} | ${formatBytes(repo.bytes)} |`);
1531
- }
1532
- }
1533
- function formatBytes(bytes) {
1534
- if (bytes === 0) {
1535
- return "0 B";
1536
- }
1537
- const k = 1024;
1538
- const sizes = ["B", "KB", "MB", "GB"];
1539
- const i = Math.floor(Math.log(bytes) / Math.log(k));
1540
- return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
1541
- }
1124
+ });
1542
1125
  var visualize_default = defineCommand({
1543
1126
  id: "quality:visualize",
1544
1127
  description: "Visualize dependency graph",
@@ -1556,9 +1139,9 @@ var visualize_default = defineCommand({
1556
1139
  } else if (flags.impact && flags.package) {
1557
1140
  outputImpact(graph, flags.package, flags, ui);
1558
1141
  } else if (flags.stats) {
1559
- outputStats3(graph, flags, ui);
1142
+ outputStats2(graph, flags, ui);
1560
1143
  } else {
1561
- outputStats3(graph, flags, ui);
1144
+ outputStats2(graph, flags, ui);
1562
1145
  }
1563
1146
  return { exitCode: 0 };
1564
1147
  }
@@ -1664,7 +1247,7 @@ function outputImpact(graph, packageName, flags, ui) {
1664
1247
  sections
1665
1248
  });
1666
1249
  }
1667
- function outputStats3(graph, flags, ui) {
1250
+ function outputStats2(graph, flags, ui) {
1668
1251
  const { nodes } = graph;
1669
1252
  let totalDeps = 0;
1670
1253
  let maxDeps = 0;
@@ -1742,6 +1325,6 @@ function outputDot(graph, flags, ui) {
1742
1325
  }
1743
1326
  }
1744
1327
 
1745
- export { build_order_default as buildOrder, check_builds_default as checkBuilds, check_tests_default as checkTests, check_types_default as checkTypes, cycles_default as cycles, dead_code_default as deadCode, fix_deps_default as fixDeps, health_default as health, stats_default as stats, visualize_default as visualize };
1328
+ export { build_order_default as buildOrder, check_builds_default as checkBuilds, check_layers_default as checkLayers, check_tests_default as checkTests, check_types_default as checkTypes, coupling_default as coupling, cycles_default as cycles, dead_code_default as deadCode, fix_deps_default as fixDeps, health_default as health, history_default as history, snapshot_default as snapshot, stats_default as stats, visualize_default as visualize };
1746
1329
  //# sourceMappingURL=index.js.map
1747
1330
  //# sourceMappingURL=index.js.map