@kb-labs/devkit 1.0.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 (295) hide show
  1. package/.cursorrules +32 -0
  2. package/.github/CODEOWNERS +2 -0
  3. package/.github/actions/setup-node-pnpm/action.yml +47 -0
  4. package/.github/workflow-templates/ci.yml +13 -0
  5. package/.github/workflow-templates/drift-check.yml +10 -0
  6. package/.github/workflow-templates/profiles-validate.yml +16 -0
  7. package/.github/workflow-templates/release.yml +8 -0
  8. package/.github/workflows/ci-reusable.yml +131 -0
  9. package/.github/workflows/drift-check-reusable.yml +23 -0
  10. package/.github/workflows/fixtures.yml +74 -0
  11. package/.github/workflows/profiles-validate-reusable.yml +67 -0
  12. package/.github/workflows/release-reusable.yml +50 -0
  13. package/.vscode/settings.json +23 -0
  14. package/AGENTS.md +130 -0
  15. package/LICENSE +21 -0
  16. package/README.md +1542 -0
  17. package/agents/devkit-maintainer/context.globs +15 -0
  18. package/agents/devkit-maintainer/permissions.yml +17 -0
  19. package/agents/devkit-maintainer/prompt.md +28 -0
  20. package/agents/devkit-maintainer/runbook.md +31 -0
  21. package/agents/docs-crafter/prompt.md +24 -0
  22. package/agents/docs-crafter/runbook.md +18 -0
  23. package/agents/release-manager/context.globs +7 -0
  24. package/agents/release-manager/prompt.md +27 -0
  25. package/agents/release-manager/runbook.md +17 -0
  26. package/agents/test-generator/context.globs +7 -0
  27. package/agents/test-generator/prompt.md +27 -0
  28. package/agents/test-generator/runbook.md +18 -0
  29. package/bin/devkit-architecture.mjs +1225 -0
  30. package/bin/devkit-build-order.mjs +500 -0
  31. package/bin/devkit-check-build-readiness.mjs +222 -0
  32. package/bin/devkit-check-commands.mjs +394 -0
  33. package/bin/devkit-check-configs.mjs +461 -0
  34. package/bin/devkit-check-deprecated.mjs +532 -0
  35. package/bin/devkit-check-duplicates.mjs +431 -0
  36. package/bin/devkit-check-exports.mjs +561 -0
  37. package/bin/devkit-check-imports.mjs +712 -0
  38. package/bin/devkit-check-paths.mjs +670 -0
  39. package/bin/devkit-check-scripts.mjs +335 -0
  40. package/bin/devkit-check-structure.mjs +495 -0
  41. package/bin/devkit-check-types.mjs +450 -0
  42. package/bin/devkit-ci.mjs +261 -0
  43. package/bin/devkit-core-gate.mjs +225 -0
  44. package/bin/devkit-fix-deps.mjs +1169 -0
  45. package/bin/devkit-freshness.mjs +199 -0
  46. package/bin/devkit-health.mjs +489 -0
  47. package/bin/devkit-migrate-configs.mjs +370 -0
  48. package/bin/devkit-paths.mjs +192 -0
  49. package/bin/devkit-stats.mjs +453 -0
  50. package/bin/devkit-sync.mjs +12 -0
  51. package/bin/devkit-tsup-external.mjs +148 -0
  52. package/bin/devkit-types-audit.mjs +615 -0
  53. package/bin/devkit-types-order.mjs +637 -0
  54. package/bin/devkit-validate-naming.mjs +203 -0
  55. package/bin/devkit-visualize.mjs +452 -0
  56. package/bin/kb-devkit-qa-history.mjs +453 -0
  57. package/bin/kb-devkit-qa.mjs +915 -0
  58. package/eslint/node.js +134 -0
  59. package/eslint/react.js +260 -0
  60. package/package.json +186 -0
  61. package/prettier/index.json +10 -0
  62. package/sync/index.mjs +693 -0
  63. package/templates/configs/README.md +306 -0
  64. package/templates/configs/eslint.config.js +27 -0
  65. package/templates/configs/package.json.bin +25 -0
  66. package/templates/configs/package.json.lib +30 -0
  67. package/templates/configs/tsconfig.build.json +15 -0
  68. package/templates/configs/tsconfig.json +9 -0
  69. package/templates/configs/tsup.config.bin.ts +34 -0
  70. package/templates/configs/tsup.config.cli.ts +41 -0
  71. package/templates/configs/tsup.config.dual.ts +46 -0
  72. package/templates/configs/tsup.config.ts +36 -0
  73. package/templates/package-json/README.md +290 -0
  74. package/templates/package-json/package.json.bin.template +40 -0
  75. package/templates/package-json/package.json.template +45 -0
  76. package/tsconfig/base.json +21 -0
  77. package/tsconfig/cli.json +12 -0
  78. package/tsconfig/dist/__tests__/cache.spec.d.ts +6 -0
  79. package/tsconfig/dist/__tests__/cache.spec.d.ts.map +1 -0
  80. package/tsconfig/dist/__tests__/cache.spec.js +85 -0
  81. package/tsconfig/dist/__tests__/cache.spec.js.map +1 -0
  82. package/tsconfig/dist/__tests__/fs-atomic.spec.d.ts +6 -0
  83. package/tsconfig/dist/__tests__/fs-atomic.spec.d.ts.map +1 -0
  84. package/tsconfig/dist/__tests__/fs-atomic.spec.js +153 -0
  85. package/tsconfig/dist/__tests__/fs-atomic.spec.js.map +1 -0
  86. package/tsconfig/dist/__tests__/init-workspace.spec.d.ts +6 -0
  87. package/tsconfig/dist/__tests__/init-workspace.spec.d.ts.map +1 -0
  88. package/tsconfig/dist/__tests__/init-workspace.spec.js +99 -0
  89. package/tsconfig/dist/__tests__/init-workspace.spec.js.map +1 -0
  90. package/tsconfig/dist/__tests__/kb-error.spec.d.ts +6 -0
  91. package/tsconfig/dist/__tests__/kb-error.spec.d.ts.map +1 -0
  92. package/tsconfig/dist/__tests__/kb-error.spec.js +190 -0
  93. package/tsconfig/dist/__tests__/kb-error.spec.js.map +1 -0
  94. package/tsconfig/dist/__tests__/preset-lockfile.spec.d.ts +6 -0
  95. package/tsconfig/dist/__tests__/preset-lockfile.spec.d.ts.map +1 -0
  96. package/tsconfig/dist/__tests__/preset-lockfile.spec.js +142 -0
  97. package/tsconfig/dist/__tests__/preset-lockfile.spec.js.map +1 -0
  98. package/tsconfig/dist/__tests__/product-config-profiles.spec.d.ts +6 -0
  99. package/tsconfig/dist/__tests__/product-config-profiles.spec.d.ts.map +1 -0
  100. package/tsconfig/dist/__tests__/product-config-profiles.spec.js +100 -0
  101. package/tsconfig/dist/__tests__/product-config-profiles.spec.js.map +1 -0
  102. package/tsconfig/dist/__tests__/product-config.spec.d.ts +6 -0
  103. package/tsconfig/dist/__tests__/product-config.spec.d.ts.map +1 -0
  104. package/tsconfig/dist/__tests__/product-config.spec.js +298 -0
  105. package/tsconfig/dist/__tests__/product-config.spec.js.map +1 -0
  106. package/tsconfig/dist/__tests__/runtime.spec.d.ts +2 -0
  107. package/tsconfig/dist/__tests__/runtime.spec.d.ts.map +1 -0
  108. package/tsconfig/dist/__tests__/runtime.spec.js +127 -0
  109. package/tsconfig/dist/__tests__/runtime.spec.js.map +1 -0
  110. package/tsconfig/dist/__tests__/upsert-lockfile.spec.d.ts +6 -0
  111. package/tsconfig/dist/__tests__/upsert-lockfile.spec.d.ts.map +1 -0
  112. package/tsconfig/dist/__tests__/upsert-lockfile.spec.js +251 -0
  113. package/tsconfig/dist/__tests__/upsert-lockfile.spec.js.map +1 -0
  114. package/tsconfig/dist/__tests__/validate-config.spec.d.ts +2 -0
  115. package/tsconfig/dist/__tests__/validate-config.spec.d.ts.map +1 -0
  116. package/tsconfig/dist/__tests__/validate-config.spec.js +14 -0
  117. package/tsconfig/dist/__tests__/validate-config.spec.js.map +1 -0
  118. package/tsconfig/dist/api/init-workspace.d.ts +10 -0
  119. package/tsconfig/dist/api/init-workspace.d.ts.map +1 -0
  120. package/tsconfig/dist/api/init-workspace.js +191 -0
  121. package/tsconfig/dist/api/init-workspace.js.map +1 -0
  122. package/tsconfig/dist/api/product-config.d.ts +21 -0
  123. package/tsconfig/dist/api/product-config.d.ts.map +1 -0
  124. package/tsconfig/dist/api/product-config.js +192 -0
  125. package/tsconfig/dist/api/product-config.js.map +1 -0
  126. package/tsconfig/dist/api/read-config.d.ts +22 -0
  127. package/tsconfig/dist/api/read-config.d.ts.map +1 -0
  128. package/tsconfig/dist/api/read-config.js +105 -0
  129. package/tsconfig/dist/api/read-config.js.map +1 -0
  130. package/tsconfig/dist/api/upsert-lockfile.d.ts +10 -0
  131. package/tsconfig/dist/api/upsert-lockfile.d.ts.map +1 -0
  132. package/tsconfig/dist/api/upsert-lockfile.js +63 -0
  133. package/tsconfig/dist/api/upsert-lockfile.js.map +1 -0
  134. package/tsconfig/dist/cache/fs-cache.d.ts +38 -0
  135. package/tsconfig/dist/cache/fs-cache.d.ts.map +1 -0
  136. package/tsconfig/dist/cache/fs-cache.js +142 -0
  137. package/tsconfig/dist/cache/fs-cache.js.map +1 -0
  138. package/tsconfig/dist/errors/kb-error.d.ts +32 -0
  139. package/tsconfig/dist/errors/kb-error.d.ts.map +1 -0
  140. package/tsconfig/dist/errors/kb-error.js +54 -0
  141. package/tsconfig/dist/errors/kb-error.js.map +1 -0
  142. package/tsconfig/dist/fs/__tests__/fs.spec.d.ts +2 -0
  143. package/tsconfig/dist/fs/__tests__/fs.spec.d.ts.map +1 -0
  144. package/tsconfig/dist/fs/__tests__/fs.spec.js +22 -0
  145. package/tsconfig/dist/fs/__tests__/fs.spec.js.map +1 -0
  146. package/tsconfig/dist/fs/fs.d.ts +6 -0
  147. package/tsconfig/dist/fs/fs.d.ts.map +1 -0
  148. package/tsconfig/dist/fs/fs.js +12 -0
  149. package/tsconfig/dist/fs/fs.js.map +1 -0
  150. package/tsconfig/dist/fs/index.d.ts +2 -0
  151. package/tsconfig/dist/fs/index.d.ts.map +1 -0
  152. package/tsconfig/dist/fs/index.js +2 -0
  153. package/tsconfig/dist/fs/index.js.map +1 -0
  154. package/tsconfig/dist/hash/config-hash.d.ts +17 -0
  155. package/tsconfig/dist/hash/config-hash.d.ts.map +1 -0
  156. package/tsconfig/dist/hash/config-hash.js +55 -0
  157. package/tsconfig/dist/hash/config-hash.js.map +1 -0
  158. package/tsconfig/dist/index.d.ts +5 -0
  159. package/tsconfig/dist/index.d.ts.map +1 -0
  160. package/tsconfig/dist/index.js +5 -0
  161. package/tsconfig/dist/index.js.map +1 -0
  162. package/tsconfig/dist/lockfile/lockfile.d.ts +54 -0
  163. package/tsconfig/dist/lockfile/lockfile.d.ts.map +1 -0
  164. package/tsconfig/dist/lockfile/lockfile.js +141 -0
  165. package/tsconfig/dist/lockfile/lockfile.js.map +1 -0
  166. package/tsconfig/dist/logging/__tests__/logger.spec.d.ts +2 -0
  167. package/tsconfig/dist/logging/__tests__/logger.spec.d.ts.map +1 -0
  168. package/tsconfig/dist/logging/__tests__/logger.spec.js +65 -0
  169. package/tsconfig/dist/logging/__tests__/logger.spec.js.map +1 -0
  170. package/tsconfig/dist/logging/__tests__/redaction.spec.d.ts +2 -0
  171. package/tsconfig/dist/logging/__tests__/redaction.spec.d.ts.map +1 -0
  172. package/tsconfig/dist/logging/__tests__/redaction.spec.js +34 -0
  173. package/tsconfig/dist/logging/__tests__/redaction.spec.js.map +1 -0
  174. package/tsconfig/dist/logging/__tests__/sinks-and-env.spec.d.ts +2 -0
  175. package/tsconfig/dist/logging/__tests__/sinks-and-env.spec.d.ts.map +1 -0
  176. package/tsconfig/dist/logging/__tests__/sinks-and-env.spec.js +90 -0
  177. package/tsconfig/dist/logging/__tests__/sinks-and-env.spec.js.map +1 -0
  178. package/tsconfig/dist/logging/index.d.ts +6 -0
  179. package/tsconfig/dist/logging/index.d.ts.map +1 -0
  180. package/tsconfig/dist/logging/index.js +6 -0
  181. package/tsconfig/dist/logging/index.js.map +1 -0
  182. package/tsconfig/dist/logging/logger.d.ts +9 -0
  183. package/tsconfig/dist/logging/logger.d.ts.map +1 -0
  184. package/tsconfig/dist/logging/logger.js +101 -0
  185. package/tsconfig/dist/logging/logger.js.map +1 -0
  186. package/tsconfig/dist/logging/redaction.d.ts +7 -0
  187. package/tsconfig/dist/logging/redaction.d.ts.map +1 -0
  188. package/tsconfig/dist/logging/redaction.js +23 -0
  189. package/tsconfig/dist/logging/redaction.js.map +1 -0
  190. package/tsconfig/dist/logging/sinks/json.d.ts +4 -0
  191. package/tsconfig/dist/logging/sinks/json.d.ts.map +1 -0
  192. package/tsconfig/dist/logging/sinks/json.js +23 -0
  193. package/tsconfig/dist/logging/sinks/json.js.map +1 -0
  194. package/tsconfig/dist/logging/sinks/stdout.d.ts +3 -0
  195. package/tsconfig/dist/logging/sinks/stdout.d.ts.map +1 -0
  196. package/tsconfig/dist/logging/sinks/stdout.js +24 -0
  197. package/tsconfig/dist/logging/sinks/stdout.js.map +1 -0
  198. package/tsconfig/dist/logging/types/index.d.ts +2 -0
  199. package/tsconfig/dist/logging/types/index.d.ts.map +1 -0
  200. package/tsconfig/dist/logging/types/index.js +2 -0
  201. package/tsconfig/dist/logging/types/index.js.map +1 -0
  202. package/tsconfig/dist/logging/types/types.d.ts +37 -0
  203. package/tsconfig/dist/logging/types/types.d.ts.map +1 -0
  204. package/tsconfig/dist/logging/types/types.js +2 -0
  205. package/tsconfig/dist/logging/types/types.js.map +1 -0
  206. package/tsconfig/dist/merge/layered-merge.d.ts +16 -0
  207. package/tsconfig/dist/merge/layered-merge.d.ts.map +1 -0
  208. package/tsconfig/dist/merge/layered-merge.js +97 -0
  209. package/tsconfig/dist/merge/layered-merge.js.map +1 -0
  210. package/tsconfig/dist/preset/resolve-preset.d.ts +29 -0
  211. package/tsconfig/dist/preset/resolve-preset.d.ts.map +1 -0
  212. package/tsconfig/dist/preset/resolve-preset.js +104 -0
  213. package/tsconfig/dist/preset/resolve-preset.js.map +1 -0
  214. package/tsconfig/dist/repo/__tests__/repo.spec.d.ts +2 -0
  215. package/tsconfig/dist/repo/__tests__/repo.spec.d.ts.map +1 -0
  216. package/tsconfig/dist/repo/__tests__/repo.spec.js +25 -0
  217. package/tsconfig/dist/repo/__tests__/repo.spec.js.map +1 -0
  218. package/tsconfig/dist/repo/index.d.ts +2 -0
  219. package/tsconfig/dist/repo/index.d.ts.map +1 -0
  220. package/tsconfig/dist/repo/index.js +2 -0
  221. package/tsconfig/dist/repo/index.js.map +1 -0
  222. package/tsconfig/dist/repo/repo.d.ts +6 -0
  223. package/tsconfig/dist/repo/repo.d.ts.map +1 -0
  224. package/tsconfig/dist/repo/repo.js +25 -0
  225. package/tsconfig/dist/repo/repo.js.map +1 -0
  226. package/tsconfig/dist/runtime/index.d.ts +2 -0
  227. package/tsconfig/dist/runtime/index.d.ts.map +1 -0
  228. package/tsconfig/dist/runtime/index.js +2 -0
  229. package/tsconfig/dist/runtime/index.js.map +1 -0
  230. package/tsconfig/dist/runtime/runtime.d.ts +46 -0
  231. package/tsconfig/dist/runtime/runtime.d.ts.map +1 -0
  232. package/tsconfig/dist/runtime/runtime.js +126 -0
  233. package/tsconfig/dist/runtime/runtime.js.map +1 -0
  234. package/tsconfig/dist/tsconfig.tools.tsbuildinfo +1 -0
  235. package/tsconfig/dist/tsconfig.tsbuildinfo +1 -0
  236. package/tsconfig/dist/types/index.d.ts +2 -0
  237. package/tsconfig/dist/types/index.d.ts.map +1 -0
  238. package/tsconfig/dist/types/index.js +2 -0
  239. package/tsconfig/dist/types/index.js.map +1 -0
  240. package/tsconfig/dist/types/init.d.ts +34 -0
  241. package/tsconfig/dist/types/init.d.ts.map +1 -0
  242. package/tsconfig/dist/types/init.js +6 -0
  243. package/tsconfig/dist/types/init.js.map +1 -0
  244. package/tsconfig/dist/types/preset.d.ts +27 -0
  245. package/tsconfig/dist/types/preset.d.ts.map +1 -0
  246. package/tsconfig/dist/types/preset.js +6 -0
  247. package/tsconfig/dist/types/preset.js.map +1 -0
  248. package/tsconfig/dist/types/types.d.ts +6 -0
  249. package/tsconfig/dist/types/types.d.ts.map +1 -0
  250. package/tsconfig/dist/types/types.js +2 -0
  251. package/tsconfig/dist/types/types.js.map +1 -0
  252. package/tsconfig/dist/utils/__tests__/env.spec.d.ts +2 -0
  253. package/tsconfig/dist/utils/__tests__/env.spec.d.ts.map +1 -0
  254. package/tsconfig/dist/utils/__tests__/env.spec.js +33 -0
  255. package/tsconfig/dist/utils/__tests__/env.spec.js.map +1 -0
  256. package/tsconfig/dist/utils/env.d.ts +7 -0
  257. package/tsconfig/dist/utils/env.d.ts.map +1 -0
  258. package/tsconfig/dist/utils/env.js +25 -0
  259. package/tsconfig/dist/utils/env.js.map +1 -0
  260. package/tsconfig/dist/utils/fs-atomic.d.ts +15 -0
  261. package/tsconfig/dist/utils/fs-atomic.d.ts.map +1 -0
  262. package/tsconfig/dist/utils/fs-atomic.js +45 -0
  263. package/tsconfig/dist/utils/fs-atomic.js.map +1 -0
  264. package/tsconfig/dist/utils/index.d.ts +3 -0
  265. package/tsconfig/dist/utils/index.d.ts.map +1 -0
  266. package/tsconfig/dist/utils/index.js +3 -0
  267. package/tsconfig/dist/utils/index.js.map +1 -0
  268. package/tsconfig/dist/utils/paths.d.ts +21 -0
  269. package/tsconfig/dist/utils/paths.d.ts.map +1 -0
  270. package/tsconfig/dist/utils/paths.js +32 -0
  271. package/tsconfig/dist/utils/paths.js.map +1 -0
  272. package/tsconfig/dist/utils/product-normalize.d.ts +27 -0
  273. package/tsconfig/dist/utils/product-normalize.d.ts.map +1 -0
  274. package/tsconfig/dist/utils/product-normalize.js +45 -0
  275. package/tsconfig/dist/utils/product-normalize.js.map +1 -0
  276. package/tsconfig/dist/validation/validate-config.d.ts +7 -0
  277. package/tsconfig/dist/validation/validate-config.d.ts.map +1 -0
  278. package/tsconfig/dist/validation/validate-config.js +22 -0
  279. package/tsconfig/dist/validation/validate-config.js.map +1 -0
  280. package/tsconfig/lib.json +13 -0
  281. package/tsconfig/node.json +12 -0
  282. package/tsconfig/react-app.json +8 -0
  283. package/tsconfig/react-lib.json +8 -0
  284. package/tsconfig/test.json +15 -0
  285. package/tsup/bin.js +172 -0
  286. package/tsup/dual.js +155 -0
  287. package/tsup/external-sync.mjs +41 -0
  288. package/tsup/external.mjs +109 -0
  289. package/tsup/node.js +65 -0
  290. package/tsup/react-lib.js +18 -0
  291. package/tsup/sdk.js +118 -0
  292. package/vite/react-app.js +50 -0
  293. package/vitest/node.js +37 -0
  294. package/vitest/react.js +39 -0
  295. package/vitest/vitest-setup.ts +9 -0
@@ -0,0 +1,1225 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @kb-labs/devkit - Architecture Audit Tool (Tier 1+)
5
+ *
6
+ * Analyzes monorepo architecture and generates:
7
+ * 1. AI-first JSON output (complete structured data for LLM consumption)
8
+ * 2. Human-first visual graph (interactive HTML with Cytoscape.js)
9
+ * 3. Human-first markdown report (executive summary with recommendations)
10
+ *
11
+ * Features:
12
+ * - Automated anomaly detection (10 types):
13
+ * • Circular dependencies (score: 100)
14
+ * • Layer violations (score: 90) - infrastructure depends on plugin/feature
15
+ * • God packages (score: 80) - >15 dependents
16
+ * • Unstable core (score: 75) - core/infra with instability >0.7
17
+ * • Bidirectional dependencies (score: 70) - A→B && B→A (not circular)
18
+ * • Code smell: Large packages (score: 60) - >10K LOC
19
+ * • Orphan packages (score: 60) - 0 dependents
20
+ * • Code smell: Many dependencies (score: 50) - >10 deps
21
+ * • Deep chains (score: 50) - depth >7
22
+ * • Code smell: No docs (score: 40) - missing README
23
+ * - Heuristic scoring (prioritize top 10 issues by impact)
24
+ * - Metrics calculation (coupling, instability, centrality, depth)
25
+ * - Layer inference (Infrastructure → Core → Plugin → Feature → UI)
26
+ * - Trend analysis (compare with previous runs, track improvements)
27
+ * - Dual output (AI-readable JSON + Human-readable graph/report)
28
+ *
29
+ * Usage:
30
+ * kb-devkit-architecture # Generate all outputs + trends
31
+ * kb-devkit-architecture --ai # Generate only JSON (stdout)
32
+ * kb-devkit-architecture --human # Generate only graph + report
33
+ * kb-devkit-architecture --format=json # JSON to stdout
34
+ * kb-devkit-architecture --format=md # Markdown report
35
+ * kb-devkit-architecture --format=html # HTML graph (TODO)
36
+ * kb-devkit-architecture --open # Open graph in browser (TODO)
37
+ * kb-devkit-architecture --layer=core # Filter by layer
38
+ * kb-devkit-architecture --threshold=70 # Show anomalies >= 70 score
39
+ */
40
+
41
+ import fs from 'node:fs';
42
+ import path from 'node:path';
43
+ import { fileURLToPath } from 'node:url';
44
+
45
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
46
+
47
+ // ANSI colors
48
+ const colors = {
49
+ reset: '\x1b[0m',
50
+ red: '\x1b[31m',
51
+ green: '\x1b[32m',
52
+ yellow: '\x1b[33m',
53
+ blue: '\x1b[34m',
54
+ magenta: '\x1b[35m',
55
+ cyan: '\x1b[36m',
56
+ gray: '\x1b[90m',
57
+ bold: '\x1b[1m',
58
+ };
59
+
60
+ function log(message, color = 'reset') {
61
+ if (!options.json && options.format !== 'json') {
62
+ console.log(`${colors[color]}${message}${colors.reset}`);
63
+ }
64
+ }
65
+
66
+ // Parse CLI arguments
67
+ const args = process.argv.slice(2);
68
+ const options = {
69
+ ai: args.includes('--ai'),
70
+ human: args.includes('--human'),
71
+ open: args.includes('--open'),
72
+ layer: args.find((arg) => arg.startsWith('--layer='))?.split('=')[1],
73
+ threshold: parseInt(args.find((arg) => arg.startsWith('--threshold='))?.split('=')[1] || '0'),
74
+ format: args.find((arg) => arg.startsWith('--format='))?.split('=')[1] || 'all',
75
+ json: args.includes('--json'),
76
+ };
77
+
78
+ // ============================
79
+ // Phase 1: Data Collection
80
+ // ============================
81
+
82
+ /**
83
+ * Find all packages in monorepo
84
+ */
85
+ function findPackages(rootDir) {
86
+ const packages = [];
87
+ const entries = fs.readdirSync(rootDir, { withFileTypes: true });
88
+
89
+ for (const entry of entries) {
90
+ if (!entry.isDirectory() || !entry.name.startsWith('kb-labs-')) {continue;}
91
+
92
+ const repoPath = path.join(rootDir, entry.name);
93
+ const packagesDir = path.join(repoPath, 'packages');
94
+
95
+ if (!fs.existsSync(packagesDir)) {continue;}
96
+
97
+ const packageDirs = fs.readdirSync(packagesDir, { withFileTypes: true });
98
+
99
+ for (const pkgDir of packageDirs) {
100
+ if (!pkgDir.isDirectory()) {continue;}
101
+
102
+ const packageJsonPath = path.join(packagesDir, pkgDir.name, 'package.json');
103
+
104
+ if (fs.existsSync(packageJsonPath)) {
105
+ packages.push({
106
+ path: packageJsonPath,
107
+ dir: path.join(packagesDir, pkgDir.name),
108
+ repository: entry.name,
109
+ });
110
+ }
111
+ }
112
+ }
113
+
114
+ return packages;
115
+ }
116
+
117
+ /**
118
+ * Calculate package size (LOC, file count)
119
+ */
120
+ function calculatePackageSize(packageDir) {
121
+ const srcDir = path.join(packageDir, 'src');
122
+
123
+ if (!fs.existsSync(srcDir)) {
124
+ return { fileCount: 0, linesOfCode: 0 };
125
+ }
126
+
127
+ let fileCount = 0;
128
+ let linesOfCode = 0;
129
+
130
+ function walk(dir) {
131
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
132
+
133
+ for (const entry of entries) {
134
+ const fullPath = path.join(dir, entry.name);
135
+
136
+ if (entry.isDirectory()) {
137
+ walk(fullPath);
138
+ } else if (entry.isFile() && /\.(ts|tsx|js|jsx)$/.test(entry.name)) {
139
+ fileCount++;
140
+ const content = fs.readFileSync(fullPath, 'utf-8');
141
+ linesOfCode += content.split('\n').length;
142
+ }
143
+ }
144
+ }
145
+
146
+ walk(srcDir);
147
+
148
+ return { fileCount, linesOfCode };
149
+ }
150
+
151
+ /**
152
+ * Infer layer from package name
153
+ */
154
+ function inferLayer(packageName) {
155
+ if (!packageName) {return 'unknown';}
156
+
157
+ // Extract meaningful part after @kb-labs/
158
+ const name = packageName.replace('@kb-labs/', '');
159
+
160
+ // Infrastructure layer
161
+ if (name.startsWith('core-') || name.startsWith('shared-')) {
162
+ return 'infrastructure';
163
+ }
164
+
165
+ // Core/Platform layer
166
+ if (name.startsWith('plugin-') || name.startsWith('cli-') || name.startsWith('workflow-')) {
167
+ return 'core';
168
+ }
169
+
170
+ // AI/Intelligence layer (plugins)
171
+ if (name.startsWith('mind-') || name.startsWith('knowledge-') || name.startsWith('analytics-')) {
172
+ return 'plugin';
173
+ }
174
+
175
+ // Feature layer
176
+ if (name.startsWith('ai-') || name.startsWith('audit-') || name.startsWith('devlink-')) {
177
+ return 'feature';
178
+ }
179
+
180
+ // UI layer
181
+ if (name.startsWith('studio-') || name.startsWith('rest-api-')) {
182
+ return 'ui';
183
+ }
184
+
185
+ return 'unknown';
186
+ }
187
+
188
+ /**
189
+ * Build dependency graph
190
+ */
191
+ function buildDependencyGraph(packages) {
192
+ const graph = new Map(); // package -> {dependencies, dependents, metadata}
193
+ const packageData = new Map(); // package -> full data
194
+
195
+ // First pass: collect all packages
196
+ for (const pkg of packages) {
197
+ const packageJson = JSON.parse(fs.readFileSync(pkg.path, 'utf-8'));
198
+ const packageName = packageJson.name;
199
+
200
+ if (!packageName || !packageName.startsWith('@kb-labs/')) {continue;}
201
+
202
+ const size = calculatePackageSize(pkg.dir);
203
+ const layer = inferLayer(packageName);
204
+
205
+ const metadata = {
206
+ name: packageName,
207
+ version: packageJson.version || '0.0.0',
208
+ description: packageJson.description || '',
209
+ path: pkg.dir,
210
+ repository: pkg.repository,
211
+ layer,
212
+ size,
213
+ };
214
+
215
+ graph.set(packageName, {
216
+ dependencies: [],
217
+ dependents: [],
218
+ metadata,
219
+ });
220
+
221
+ packageData.set(packageName, { packageJson, metadata });
222
+ }
223
+
224
+ // Second pass: build dependency relationships
225
+ for (const [packageName, data] of packageData.entries()) {
226
+ const allDeps = {
227
+ ...data.packageJson.dependencies,
228
+ ...data.packageJson.devDependencies,
229
+ };
230
+
231
+ for (const dep of Object.keys(allDeps)) {
232
+ if (dep.startsWith('@kb-labs/') && graph.has(dep)) {
233
+ graph.get(packageName).dependencies.push(dep);
234
+ graph.get(dep).dependents.push(packageName);
235
+ }
236
+ }
237
+ }
238
+
239
+ return { graph, packageData };
240
+ }
241
+
242
+ // ============================
243
+ // Phase 2: Metrics Calculation
244
+ // ============================
245
+
246
+ /**
247
+ * Calculate coupling metrics for each package
248
+ */
249
+ function calculateMetrics(graph) {
250
+ const metrics = new Map();
251
+
252
+ for (const [packageName, data] of graph.entries()) {
253
+ const afferentCoupling = data.dependents.length; // incoming dependencies (Ca)
254
+ const efferentCoupling = data.dependencies.length; // outgoing dependencies (Ce)
255
+
256
+ // Instability: I = Ce / (Ce + Ca), where 0 = stable, 1 = unstable
257
+ const totalCoupling = efferentCoupling + afferentCoupling;
258
+ const instability = totalCoupling === 0 ? 0 : efferentCoupling / totalCoupling;
259
+
260
+ // Centrality: how important is this package (simple version: number of dependents)
261
+ const centrality = afferentCoupling / Math.max(1, graph.size);
262
+
263
+ // Depth: distance from root (packages with no dependencies)
264
+ const depth = calculateDepth(packageName, graph, new Set());
265
+
266
+ metrics.set(packageName, {
267
+ afferentCoupling,
268
+ efferentCoupling,
269
+ instability,
270
+ centrality,
271
+ depth,
272
+ });
273
+ }
274
+
275
+ return metrics;
276
+ }
277
+
278
+ /**
279
+ * Calculate depth of package in dependency tree
280
+ */
281
+ function calculateDepth(packageName, graph, visited) {
282
+ if (visited.has(packageName)) {return 0;} // circular dependency
283
+ visited.add(packageName);
284
+
285
+ const deps = graph.get(packageName).dependencies;
286
+ if (deps.length === 0) {return 0;}
287
+
288
+ let maxDepth = 0;
289
+ for (const dep of deps) {
290
+ const depthOfDep = calculateDepth(dep, graph, new Set(visited));
291
+ maxDepth = Math.max(maxDepth, depthOfDep + 1);
292
+ }
293
+
294
+ return maxDepth;
295
+ }
296
+
297
+ // ============================
298
+ // Phase 3: Anomaly Detection
299
+ // ============================
300
+
301
+ /**
302
+ * Detect circular dependencies using DFS
303
+ */
304
+ function detectCircularDependencies(graph) {
305
+ const cycles = [];
306
+ const visited = new Set();
307
+ const recStack = new Set();
308
+ const path = [];
309
+
310
+ function dfs(node) {
311
+ visited.add(node);
312
+ recStack.add(node);
313
+ path.push(node);
314
+
315
+ const deps = graph.get(node).dependencies;
316
+
317
+ for (const dep of deps) {
318
+ if (!visited.has(dep)) {
319
+ if (dfs(dep)) {return true;}
320
+ } else if (recStack.has(dep)) {
321
+ // Found a cycle
322
+ const cycleStart = path.indexOf(dep);
323
+ const cycle = path.slice(cycleStart).concat([dep]);
324
+ cycles.push(cycle);
325
+ return true;
326
+ }
327
+ }
328
+
329
+ recStack.delete(node);
330
+ path.pop();
331
+ return false;
332
+ }
333
+
334
+ for (const node of graph.keys()) {
335
+ if (!visited.has(node)) {
336
+ dfs(node);
337
+ }
338
+ }
339
+
340
+ // Deduplicate cycles
341
+ const uniqueCycles = [];
342
+ const seen = new Set();
343
+
344
+ for (const cycle of cycles) {
345
+ const normalized = cycle.slice(0, -1).sort().join('→');
346
+ if (!seen.has(normalized)) {
347
+ seen.add(normalized);
348
+ uniqueCycles.push(cycle);
349
+ }
350
+ }
351
+
352
+ return uniqueCycles;
353
+ }
354
+
355
+ /**
356
+ * Detect architectural anomalies
357
+ */
358
+ function detectAnomalies(graph, metrics) {
359
+ const anomalies = [];
360
+ let anomalyId = 0;
361
+
362
+ // 1. Circular dependencies (score: 100)
363
+ const cycles = detectCircularDependencies(graph);
364
+ for (const cycle of cycles) {
365
+ anomalies.push({
366
+ id: `cycle-${++anomalyId}`,
367
+ type: 'circular-dependency',
368
+ severity: 'critical',
369
+ score: 100,
370
+ packages: cycle.slice(0, -1), // remove duplicate last element
371
+ cycle,
372
+ impact: 'Blocks build order calculation',
373
+ affectedPackages: cycle.length - 1,
374
+ recommendation: `Extract shared types/interfaces to a new contracts package`,
375
+ estimatedEffort: '2-4 hours',
376
+ });
377
+ }
378
+
379
+ // 2. God packages (score: 80, threshold: >15 dependents)
380
+ for (const [packageName, data] of graph.entries()) {
381
+ const Ca = metrics.get(packageName).afferentCoupling;
382
+ if (Ca > 15) {
383
+ anomalies.push({
384
+ id: `god-${++anomalyId}`,
385
+ type: 'god-package',
386
+ severity: 'high',
387
+ score: 80,
388
+ package: packageName,
389
+ dependents: Ca,
390
+ impact: `Changes ripple through ${Ca} packages (${((Ca / graph.size) * 100).toFixed(1)}% of codebase)`,
391
+ recommendation: `Split into smaller packages (e.g., ${packageName.split('/')[1]}-types, ${packageName.split('/')[1]}-impl)`,
392
+ estimatedEffort: '4-8 hours',
393
+ });
394
+ }
395
+ }
396
+
397
+ // 3. Orphan packages (score: 60, threshold: 0 dependents, not CLI/plugin)
398
+ for (const [packageName, data] of graph.entries()) {
399
+ const Ca = metrics.get(packageName).afferentCoupling;
400
+ const name = packageName.replace('@kb-labs/', '');
401
+
402
+ // Skip expected orphans (CLI entry points, plugins)
403
+ const isExpectedOrphan =
404
+ name.endsWith('-cli') ||
405
+ name.endsWith('-plugin') ||
406
+ name.endsWith('-bin') ||
407
+ name.startsWith('rest-api-') ||
408
+ name.startsWith('studio-') ||
409
+ name.startsWith('playbooks-');
410
+
411
+ if (Ca === 0 && !isExpectedOrphan) {
412
+ anomalies.push({
413
+ id: `orphan-${++anomalyId}`,
414
+ type: 'orphan-package',
415
+ severity: 'medium',
416
+ score: 60,
417
+ package: packageName,
418
+ impact: 'No other package depends on this (potential dead code)',
419
+ recommendation: 'Review if package is still needed or should be removed',
420
+ estimatedEffort: '1-2 hours',
421
+ });
422
+ }
423
+ }
424
+
425
+ // 4. Unstable core packages (score: 75, threshold: layer=infrastructure && I>0.7)
426
+ for (const [packageName, data] of graph.entries()) {
427
+ const layer = data.metadata.layer;
428
+ const I = metrics.get(packageName).instability;
429
+
430
+ if ((layer === 'infrastructure' || layer === 'core') && I > 0.7) {
431
+ anomalies.push({
432
+ id: `unstable-${++anomalyId}`,
433
+ type: 'unstable-core',
434
+ severity: 'high',
435
+ score: 75,
436
+ package: packageName,
437
+ instability: I.toFixed(2),
438
+ impact: 'Core package is unstable (high efferent coupling)',
439
+ recommendation: 'Reduce dependencies or extract to separate package',
440
+ estimatedEffort: '2-4 hours',
441
+ });
442
+ }
443
+ }
444
+
445
+ // 5. Deep dependency chains (score: 50, threshold: depth > 7)
446
+ for (const [packageName, data] of graph.entries()) {
447
+ const depth = metrics.get(packageName).depth;
448
+
449
+ if (depth > 7) {
450
+ anomalies.push({
451
+ id: `deep-${++anomalyId}`,
452
+ type: 'deep-chain',
453
+ severity: 'medium',
454
+ score: 50,
455
+ package: packageName,
456
+ depth,
457
+ impact: 'Deep dependency chain increases fragility',
458
+ recommendation: 'Flatten dependency tree or introduce intermediate layers',
459
+ estimatedEffort: '4-6 hours',
460
+ });
461
+ }
462
+ }
463
+
464
+ // 6. Layer violations (score: 90, threshold: lower layer depends on higher layer)
465
+ const layerHierarchy = { infrastructure: 0, core: 1, plugin: 2, feature: 3, ui: 4, unknown: 5 };
466
+ for (const [packageName, data] of graph.entries()) {
467
+ const packageLayer = data.metadata.layer;
468
+ const packageLayerLevel = layerHierarchy[packageLayer];
469
+
470
+ for (const dep of data.dependencies) {
471
+ const depLayer = graph.get(dep)?.metadata.layer;
472
+ if (!depLayer) {continue;}
473
+
474
+ const depLayerLevel = layerHierarchy[depLayer];
475
+
476
+ // Violation: lower layer depends on higher layer (e.g., infrastructure → plugin)
477
+ if (packageLayerLevel < depLayerLevel) {
478
+ anomalies.push({
479
+ id: `layer-violation-${++anomalyId}`,
480
+ type: 'layer-violation',
481
+ severity: 'critical',
482
+ score: 90,
483
+ package: packageName,
484
+ dependency: dep,
485
+ fromLayer: packageLayer,
486
+ toLayer: depLayer,
487
+ impact: `${packageLayer} layer depends on ${depLayer} layer (inverted hierarchy)`,
488
+ recommendation: `Move ${dep} to ${packageLayer} layer or refactor dependency`,
489
+ estimatedEffort: '3-6 hours',
490
+ });
491
+ }
492
+ }
493
+ }
494
+
495
+ // 7. Bidirectional dependencies (score: 70, threshold: A→B && B→A but not circular)
496
+ const bidirectional = new Set();
497
+ for (const [packageA, dataA] of graph.entries()) {
498
+ for (const packageB of dataA.dependencies) {
499
+ const dataB = graph.get(packageB);
500
+ if (!dataB) {continue;}
501
+
502
+ // Check if B also depends on A
503
+ if (dataB.dependencies.includes(packageA)) {
504
+ const pair = [packageA, packageB].sort().join('↔');
505
+ if (!bidirectional.has(pair)) {
506
+ bidirectional.add(pair);
507
+
508
+ // Verify it's not already detected as circular
509
+ const isCycle = cycles.some((cycle) => cycle.includes(packageA) && cycle.includes(packageB));
510
+
511
+ if (!isCycle) {
512
+ anomalies.push({
513
+ id: `bidirectional-${++anomalyId}`,
514
+ type: 'bidirectional-dependency',
515
+ severity: 'high',
516
+ score: 70,
517
+ packages: [packageA, packageB],
518
+ impact: 'Bidirectional coupling increases complexity and fragility',
519
+ recommendation: 'Extract shared types to a contracts package or invert one dependency',
520
+ estimatedEffort: '2-3 hours',
521
+ });
522
+ }
523
+ }
524
+ }
525
+ }
526
+ }
527
+
528
+ // 8. Code smells: Large packages (score: 60, threshold: >10K LOC)
529
+ for (const [packageName, data] of graph.entries()) {
530
+ const loc = data.metadata.size.linesOfCode;
531
+
532
+ if (loc > 10000) {
533
+ anomalies.push({
534
+ id: `large-package-${++anomalyId}`,
535
+ type: 'code-smell-large-package',
536
+ severity: 'medium',
537
+ score: 60,
538
+ package: packageName,
539
+ linesOfCode: loc,
540
+ impact: `Package is very large (${loc.toLocaleString()} LOC), hard to maintain`,
541
+ recommendation: 'Split into smaller, focused packages by feature or domain',
542
+ estimatedEffort: '8-12 hours',
543
+ });
544
+ }
545
+ }
546
+
547
+ // 9. Code smells: Too many dependencies (score: 50, threshold: >10 deps)
548
+ for (const [packageName, data] of graph.entries()) {
549
+ const depsCount = data.dependencies.length;
550
+
551
+ if (depsCount > 10) {
552
+ anomalies.push({
553
+ id: `many-deps-${++anomalyId}`,
554
+ type: 'code-smell-many-dependencies',
555
+ severity: 'medium',
556
+ score: 50,
557
+ package: packageName,
558
+ dependenciesCount: depsCount,
559
+ impact: `Package has ${depsCount} dependencies, high coupling`,
560
+ recommendation: 'Review and reduce dependencies, consider dependency injection',
561
+ estimatedEffort: '4-6 hours',
562
+ });
563
+ }
564
+ }
565
+
566
+ // 10. Code smells: No documentation (score: 40, threshold: missing README)
567
+ for (const [packageName, data] of graph.entries()) {
568
+ const packageDir = path.dirname(data.metadata.path);
569
+ const readmePath = path.join(packageDir, 'README.md');
570
+
571
+ if (!fs.existsSync(readmePath)) {
572
+ anomalies.push({
573
+ id: `no-docs-${++anomalyId}`,
574
+ type: 'code-smell-no-docs',
575
+ severity: 'low',
576
+ score: 40,
577
+ package: packageName,
578
+ impact: 'Package has no README, difficult for new developers',
579
+ recommendation: 'Create README.md with overview, usage, and examples',
580
+ estimatedEffort: '1-2 hours',
581
+ });
582
+ }
583
+ }
584
+
585
+ // Sort by score (descending)
586
+ anomalies.sort((a, b) => b.score - a.score);
587
+
588
+ return anomalies;
589
+ }
590
+
591
+ /**
592
+ * Load historical architecture data for trend analysis
593
+ */
594
+ function loadHistoricalData(archDir) {
595
+ if (!fs.existsSync(archDir)) {
596
+ return [];
597
+ }
598
+
599
+ const files = fs.readdirSync(archDir).filter((f) => f.startsWith('architecture-') && f.endsWith('.json'));
600
+
601
+ const history = [];
602
+
603
+ for (const file of files.slice(-5)) {
604
+ // Load last 5 runs
605
+ try {
606
+ const data = JSON.parse(fs.readFileSync(path.join(archDir, file), 'utf-8'));
607
+ history.push({
608
+ date: data.metadata.generatedAt,
609
+ healthScore: data.metadata.healthScore,
610
+ totalPackages: data.metadata.totalPackages,
611
+ anomaliesCount: data.anomalies.length,
612
+ anomaliesBySeverity: {
613
+ critical: data.anomalies.filter((a) => a.severity === 'critical').length,
614
+ high: data.anomalies.filter((a) => a.severity === 'high').length,
615
+ medium: data.anomalies.filter((a) => a.severity === 'medium').length,
616
+ low: data.anomalies.filter((a) => a.severity === 'low').length,
617
+ },
618
+ anomaliesByType: data.anomalies.reduce((acc, a) => {
619
+ acc[a.type] = (acc[a.type] || 0) + 1;
620
+ return acc;
621
+ }, {}),
622
+ });
623
+ } catch (err) {
624
+ // Skip corrupted files
625
+ }
626
+ }
627
+
628
+ return history.sort((a, b) => new Date(a.date) - new Date(b.date));
629
+ }
630
+
631
+ /**
632
+ * Generate trend analysis comparing current run with historical data
633
+ */
634
+ function generateTrendAnalysis(currentData, history) {
635
+ if (history.length === 0) {
636
+ return null;
637
+ }
638
+
639
+ const previous = history[history.length - 1];
640
+ const trends = {};
641
+
642
+ // Health score trend
643
+ const healthDelta = currentData.metadata.healthScore - previous.healthScore;
644
+ trends.healthScore = {
645
+ current: currentData.metadata.healthScore,
646
+ previous: previous.healthScore,
647
+ delta: healthDelta,
648
+ direction: healthDelta > 0 ? 'šŸ“ˆ' : healthDelta < 0 ? 'šŸ“‰' : 'āž”ļø',
649
+ status: healthDelta > 5 ? 'āœ…' : healthDelta < -5 ? 'āŒ' : 'āš ļø',
650
+ };
651
+
652
+ // Anomalies count trend
653
+ const anomaliesDelta = currentData.anomalies.length - previous.anomaliesCount;
654
+ trends.anomaliesCount = {
655
+ current: currentData.anomalies.length,
656
+ previous: previous.anomaliesCount,
657
+ delta: anomaliesDelta,
658
+ direction: anomaliesDelta < 0 ? 'šŸ“‰' : anomaliesDelta > 0 ? 'šŸ“ˆ' : 'āž”ļø',
659
+ status: anomaliesDelta < 0 ? 'āœ…' : anomaliesDelta > 0 ? 'āŒ' : 'āš ļø',
660
+ };
661
+
662
+ // Anomalies by severity trends
663
+ const currentBySeverity = {
664
+ critical: currentData.anomalies.filter((a) => a.severity === 'critical').length,
665
+ high: currentData.anomalies.filter((a) => a.severity === 'high').length,
666
+ medium: currentData.anomalies.filter((a) => a.severity === 'medium').length,
667
+ low: currentData.anomalies.filter((a) => a.severity === 'low').length,
668
+ };
669
+
670
+ trends.bySeverity = {};
671
+ for (const [severity, count] of Object.entries(currentBySeverity)) {
672
+ const prevCount = previous.anomaliesBySeverity[severity] || 0;
673
+ const delta = count - prevCount;
674
+ trends.bySeverity[severity] = {
675
+ current: count,
676
+ previous: prevCount,
677
+ delta,
678
+ direction: delta < 0 ? 'šŸ“‰' : delta > 0 ? 'šŸ“ˆ' : 'āž”ļø',
679
+ status: delta <= 0 ? 'āœ…' : 'āŒ',
680
+ };
681
+ }
682
+
683
+ // Anomalies by type trends
684
+ const currentByType = currentData.anomalies.reduce((acc, a) => {
685
+ acc[a.type] = (acc[a.type] || 0) + 1;
686
+ return acc;
687
+ }, {});
688
+
689
+ trends.byType = {};
690
+ const allTypes = new Set([...Object.keys(currentByType), ...Object.keys(previous.anomaliesByType || {})]);
691
+
692
+ for (const type of allTypes) {
693
+ const current = currentByType[type] || 0;
694
+ const prev = (previous.anomaliesByType || {})[type] || 0;
695
+ const delta = current - prev;
696
+
697
+ if (delta !== 0) {
698
+ // Only track changed types
699
+ trends.byType[type] = {
700
+ current,
701
+ previous: prev,
702
+ delta,
703
+ direction: delta < 0 ? 'šŸ“‰' : 'šŸ“ˆ',
704
+ status: delta < 0 ? 'āœ…' : 'āŒ',
705
+ };
706
+ }
707
+ }
708
+
709
+ // Historical comparison (last 30 days if available)
710
+ if (history.length > 1) {
711
+ const oldestDate = new Date(history[0].date);
712
+ const newestDate = new Date(currentData.metadata.generatedAt);
713
+ const daysDiff = Math.round((newestDate - oldestDate) / (1000 * 60 * 60 * 24));
714
+
715
+ trends.historical = {
716
+ period: `${daysDiff} days`,
717
+ healthScoreTrend: currentData.metadata.healthScore - history[0].healthScore,
718
+ anomaliesTrend: currentData.anomalies.length - history[0].anomaliesCount,
719
+ };
720
+ }
721
+
722
+ return trends;
723
+ }
724
+
725
+ // ============================
726
+ // Phase 4: Output Generation
727
+ // ============================
728
+
729
+ /**
730
+ * Generate AI-first JSON output
731
+ */
732
+ function generateJSON(graph, metrics, anomalies, options) {
733
+ const packages = [];
734
+ const layers = {};
735
+ const nodes = [];
736
+ const edges = [];
737
+
738
+ // Build packages array
739
+ for (const [packageName, data] of graph.entries()) {
740
+ const pkgMetrics = metrics.get(packageName);
741
+ const pkgAnomalies = anomalies.filter(
742
+ (a) => a.package === packageName || a.packages?.includes(packageName)
743
+ );
744
+
745
+ packages.push({
746
+ name: packageName,
747
+ path: data.metadata.path,
748
+ repository: data.metadata.repository,
749
+ layer: data.metadata.layer,
750
+ description: data.metadata.description,
751
+ version: data.metadata.version,
752
+ metrics: {
753
+ linesOfCode: data.metadata.size.linesOfCode,
754
+ fileCount: data.metadata.size.fileCount,
755
+ afferentCoupling: pkgMetrics.afferentCoupling,
756
+ efferentCoupling: pkgMetrics.efferentCoupling,
757
+ instability: parseFloat(pkgMetrics.instability.toFixed(3)),
758
+ centrality: parseFloat(pkgMetrics.centrality.toFixed(3)),
759
+ depth: pkgMetrics.depth,
760
+ },
761
+ dependencies: data.dependencies,
762
+ dependents: data.dependents,
763
+ anomalies: pkgAnomalies.map((a) => ({
764
+ type: a.type,
765
+ severity: a.severity,
766
+ score: a.score,
767
+ message: a.impact,
768
+ recommendation: a.recommendation,
769
+ })),
770
+ });
771
+
772
+ // Build graph nodes
773
+ nodes.push({
774
+ id: packageName,
775
+ layer: data.metadata.layer,
776
+ metrics: {
777
+ centrality: parseFloat(pkgMetrics.centrality.toFixed(3)),
778
+ instability: parseFloat(pkgMetrics.instability.toFixed(3)),
779
+ },
780
+ });
781
+
782
+ // Build graph edges
783
+ for (const dep of data.dependencies) {
784
+ edges.push({
785
+ from: packageName,
786
+ to: dep,
787
+ type: 'dependency',
788
+ });
789
+ }
790
+
791
+ // Group by layer
792
+ const layer = data.metadata.layer;
793
+ if (!layers[layer]) {
794
+ layers[layer] = {
795
+ packages: [],
796
+ count: 0,
797
+ metrics: {
798
+ averageInstability: 0,
799
+ averageCoupling: 0,
800
+ totalLOC: 0,
801
+ },
802
+ issues: {
803
+ critical: 0,
804
+ high: 0,
805
+ medium: 0,
806
+ low: 0,
807
+ },
808
+ };
809
+ }
810
+
811
+ layers[layer].packages.push(packageName);
812
+ layers[layer].count++;
813
+ layers[layer].metrics.totalLOC += data.metadata.size.linesOfCode;
814
+ }
815
+
816
+ // Calculate layer metrics
817
+ for (const [layerName, layerData] of Object.entries(layers)) {
818
+ const layerPackages = layerData.packages;
819
+ const instabilities = layerPackages.map((p) => metrics.get(p).instability);
820
+ const couplings = layerPackages.map(
821
+ (p) => metrics.get(p).afferentCoupling + metrics.get(p).efferentCoupling
822
+ );
823
+
824
+ layerData.metrics.averageInstability = parseFloat(
825
+ (instabilities.reduce((a, b) => a + b, 0) / layerPackages.length).toFixed(2)
826
+ );
827
+ layerData.metrics.averageCoupling = parseFloat(
828
+ (couplings.reduce((a, b) => a + b, 0) / layerPackages.length).toFixed(1)
829
+ );
830
+
831
+ // Count issues by severity
832
+ for (const pkg of layerPackages) {
833
+ const pkgAnomalies = anomalies.filter(
834
+ (a) => a.package === pkg || a.packages?.includes(pkg)
835
+ );
836
+ for (const anomaly of pkgAnomalies) {
837
+ layerData.issues[anomaly.severity]++;
838
+ }
839
+ }
840
+ }
841
+
842
+ // Find longest dependency chain
843
+ let longestChain = { depth: 0, path: [] };
844
+ for (const [packageName, pkgMetrics] of metrics.entries()) {
845
+ if (pkgMetrics.depth > longestChain.depth) {
846
+ // Reconstruct path (simplified - just show depth)
847
+ longestChain = {
848
+ depth: pkgMetrics.depth,
849
+ path: [packageName], // TODO: reconstruct full path
850
+ };
851
+ }
852
+ }
853
+
854
+ // Calculate health score
855
+ const totalPackages = graph.size;
856
+ let healthScore = 100;
857
+
858
+ // Deductions
859
+ const criticalCount = anomalies.filter((a) => a.severity === 'critical').length;
860
+ const highCount = anomalies.filter((a) => a.severity === 'high').length;
861
+ const mediumCount = anomalies.filter((a) => a.severity === 'medium').length;
862
+
863
+ healthScore -= criticalCount * 20;
864
+ healthScore -= highCount * 10;
865
+ healthScore -= mediumCount * 5;
866
+
867
+ healthScore = Math.max(0, Math.min(100, healthScore));
868
+
869
+ const healthGrade =
870
+ healthScore >= 90 ? 'A' : healthScore >= 80 ? 'B' : healthScore >= 70 ? 'C' : healthScore >= 60 ? 'D' : 'F';
871
+
872
+ // Build final JSON
873
+ return {
874
+ metadata: {
875
+ generatedAt: new Date().toISOString(),
876
+ version: '1.0.0',
877
+ totalPackages,
878
+ totalRepositories: new Set(packages.map((p) => p.repository)).size,
879
+ healthScore,
880
+ healthGrade,
881
+ },
882
+ packages,
883
+ layers,
884
+ anomalies,
885
+ graph: {
886
+ nodes,
887
+ edges,
888
+ },
889
+ chains: {
890
+ longest: longestChain,
891
+ },
892
+ recommendations: generateRecommendations(anomalies),
893
+ };
894
+ }
895
+
896
+ /**
897
+ * Generate prioritized recommendations
898
+ */
899
+ function generateRecommendations(anomalies) {
900
+ const recommendations = [];
901
+
902
+ const critical = anomalies.filter((a) => a.severity === 'critical');
903
+ const high = anomalies.filter((a) => a.severity === 'high');
904
+ const medium = anomalies.filter((a) => a.severity === 'medium');
905
+
906
+ if (critical.length > 0) {
907
+ recommendations.push({
908
+ priority: 'immediate',
909
+ timeframe: 'Week 1',
910
+ tasks: critical.slice(0, 3).map((a, i) => ({
911
+ id: `rec-${i + 1}`,
912
+ title: a.type.replace(/-/g, ' '),
913
+ description: a.recommendation,
914
+ effort: a.estimatedEffort,
915
+ impact: a.impact,
916
+ relatedAnomalies: [a.id],
917
+ })),
918
+ });
919
+ }
920
+
921
+ if (high.length > 0) {
922
+ recommendations.push({
923
+ priority: 'high',
924
+ timeframe: 'Month 1',
925
+ tasks: high.slice(0, 5).map((a, i) => ({
926
+ id: `rec-${critical.length + i + 1}`,
927
+ title: a.type.replace(/-/g, ' '),
928
+ description: a.recommendation,
929
+ effort: a.estimatedEffort,
930
+ impact: a.impact,
931
+ relatedAnomalies: [a.id],
932
+ })),
933
+ });
934
+ }
935
+
936
+ if (medium.length > 0) {
937
+ recommendations.push({
938
+ priority: 'medium',
939
+ timeframe: 'Quarter',
940
+ tasks: medium.slice(0, 5).map((a, i) => ({
941
+ id: `rec-${critical.length + high.length + i + 1}`,
942
+ title: a.type.replace(/-/g, ' '),
943
+ description: a.recommendation,
944
+ effort: a.estimatedEffort,
945
+ impact: a.impact,
946
+ relatedAnomalies: [a.id],
947
+ })),
948
+ });
949
+ }
950
+
951
+ return recommendations;
952
+ }
953
+
954
+ /**
955
+ * Generate Markdown report
956
+ */
957
+ function generateMarkdownReport(jsonData) {
958
+ const { metadata, anomalies, layers, chains, recommendations } = jsonData;
959
+
960
+ let report = `# KB Labs Architecture Audit Report\n\n`;
961
+ report += `**Date:** ${new Date(metadata.generatedAt).toLocaleDateString()}\n\n`;
962
+
963
+ // Executive Summary
964
+ report += `## šŸŽÆ Executive Summary\n\n`;
965
+ report += `- **Total packages:** ${metadata.totalPackages}\n`;
966
+ report += `- **Total repositories:** ${metadata.totalRepositories}\n`;
967
+ report += `- **Health score:** ${metadata.healthScore}/100 (Grade ${metadata.healthGrade})\n`;
968
+ report += `- **Critical issues:** ${anomalies.filter((a) => a.severity === 'critical').length}\n`;
969
+ report += `- **High priority issues:** ${anomalies.filter((a) => a.severity === 'high').length}\n`;
970
+ report += `- **Medium priority issues:** ${anomalies.filter((a) => a.severity === 'medium').length}\n\n`;
971
+
972
+ // Top 10 Anomalies
973
+ const top10 = anomalies.slice(0, 10);
974
+ if (top10.length > 0) {
975
+ report += `## 🚨 Top 10 Anomalies (Require Architect Attention)\n\n`;
976
+
977
+ for (let i = 0; i < top10.length; i++) {
978
+ const anomaly = top10[i];
979
+ const emoji = anomaly.severity === 'critical' ? 'šŸ”“' : anomaly.severity === 'high' ? '🟠' : '🟔';
980
+
981
+ report += `### ${i + 1}. ${emoji} ${anomaly.type.replace(/-/g, ' ')}\n`;
982
+ report += `**Severity:** ${anomaly.severity} (Score: ${anomaly.score})\n`;
983
+ report += `**Impact:** ${anomaly.impact}\n`;
984
+ report += `**Recommendation:** ${anomaly.recommendation}\n`;
985
+ report += `**Estimated Effort:** ${anomaly.estimatedEffort}\n\n`;
986
+ }
987
+ }
988
+
989
+ // Trend Analysis
990
+ if (jsonData.trends) {
991
+ const { trends } = jsonData;
992
+ report += `## šŸ“ˆ Trend Analysis\n\n`;
993
+
994
+ report += `### Health Score\n`;
995
+ report += `- **Current:** ${trends.healthScore.current}/100\n`;
996
+ report += `- **Previous:** ${trends.healthScore.previous}/100\n`;
997
+ report += `- **Change:** ${trends.healthScore.delta >= 0 ? '+' : ''}${trends.healthScore.delta} ${trends.healthScore.direction} ${trends.healthScore.status}\n\n`;
998
+
999
+ report += `### Anomalies Count\n`;
1000
+ report += `- **Current:** ${trends.anomaliesCount.current}\n`;
1001
+ report += `- **Previous:** ${trends.anomaliesCount.previous}\n`;
1002
+ report += `- **Change:** ${trends.anomaliesCount.delta >= 0 ? '+' : ''}${trends.anomaliesCount.delta} ${trends.anomaliesCount.direction} ${trends.anomaliesCount.status}\n\n`;
1003
+
1004
+ if (Object.keys(trends.bySeverity).length > 0) {
1005
+ report += `### Changes by Severity\n\n`;
1006
+ for (const [severity, data] of Object.entries(trends.bySeverity)) {
1007
+ report += `- **${severity}:** ${data.previous} → ${data.current} (${data.delta >= 0 ? '+' : ''}${data.delta}) ${data.direction} ${data.status}\n`;
1008
+ }
1009
+ report += `\n`;
1010
+ }
1011
+
1012
+ if (Object.keys(trends.byType).length > 0) {
1013
+ report += `### Notable Changes by Type\n\n`;
1014
+ const sortedTypes = Object.entries(trends.byType).sort((a, b) => Math.abs(b[1].delta) - Math.abs(a[1].delta));
1015
+ for (const [type, data] of sortedTypes.slice(0, 5)) {
1016
+ report += `- **${type.replace(/-/g, ' ')}:** ${data.previous} → ${data.current} (${data.delta >= 0 ? '+' : ''}${data.delta}) ${data.direction} ${data.status}\n`;
1017
+ }
1018
+ report += `\n`;
1019
+ }
1020
+
1021
+ if (trends.historical) {
1022
+ report += `### Historical Comparison (${trends.historical.period})\n`;
1023
+ report += `- **Health Score Change:** ${trends.historical.healthScoreTrend >= 0 ? '+' : ''}${trends.historical.healthScoreTrend}\n`;
1024
+ report += `- **Anomalies Change:** ${trends.historical.anomaliesTrend >= 0 ? '+' : ''}${trends.historical.anomaliesTrend}\n\n`;
1025
+ }
1026
+ }
1027
+
1028
+ // Metrics by Layer
1029
+ report += `## šŸ“Š Metrics by Layer\n\n`;
1030
+ for (const [layerName, layerData] of Object.entries(layers)) {
1031
+ report += `### ${layerName.charAt(0).toUpperCase() + layerName.slice(1)} Layer (${layerData.count} packages)\n`;
1032
+ report += `- **Average Instability:** ${layerData.metrics.averageInstability} ${layerData.metrics.averageInstability < 0.3 ? 'āœ…' : layerData.metrics.averageInstability < 0.7 ? 'āš ļø' : 'āŒ'}\n`;
1033
+ report += `- **Average Coupling:** ${layerData.metrics.averageCoupling} deps/pkg ${layerData.metrics.averageCoupling < 5 ? 'āœ…' : 'āš ļø'}\n`;
1034
+ report += `- **Total LOC:** ${layerData.metrics.totalLOC.toLocaleString()}\n`;
1035
+ report += `- **Issues:** ${layerData.issues.critical} critical, ${layerData.issues.high} high, ${layerData.issues.medium} medium\n\n`;
1036
+ }
1037
+
1038
+ // Recommendations
1039
+ if (recommendations.length > 0) {
1040
+ report += `## āœ… Recommendations (Prioritized)\n\n`;
1041
+
1042
+ for (const rec of recommendations) {
1043
+ report += `### ${rec.priority.charAt(0).toUpperCase() + rec.priority.slice(1)} Priority (${rec.timeframe})\n\n`;
1044
+
1045
+ for (const task of rec.tasks) {
1046
+ report += `${rec.tasks.indexOf(task) + 1}. **${task.title}**: ${task.description} (${task.effort})\n`;
1047
+ }
1048
+
1049
+ report += `\n`;
1050
+ }
1051
+ }
1052
+
1053
+ report += `---\n\n`;
1054
+ report += `šŸ¤– Generated with [KB Labs DevKit Architecture Tool](https://github.com/kb-labs/devkit)\n`;
1055
+
1056
+ return report;
1057
+ }
1058
+
1059
+ // ============================
1060
+ // Main Function
1061
+ // ============================
1062
+
1063
+ /**
1064
+ * Main entry point
1065
+ */
1066
+ async function main() {
1067
+ const rootDir = process.cwd();
1068
+
1069
+ const startTime = Date.now();
1070
+
1071
+ if (options.format !== 'json') {
1072
+ log('\nšŸ—ļø KB Labs Architecture Audit\n', 'blue');
1073
+ log(`šŸ“Š Scanning packages across monorepo...\n`, 'gray');
1074
+ }
1075
+
1076
+ // Phase 1: Data Collection
1077
+ const packages = findPackages(rootDir);
1078
+
1079
+ if (packages.length === 0) {
1080
+ log('āš ļø No KB Labs packages found', 'yellow');
1081
+ log(' Run this command from the monorepo root\n', 'gray');
1082
+ process.exit(0);
1083
+ }
1084
+
1085
+ const { graph, packageData } = buildDependencyGraph(packages);
1086
+
1087
+ if (options.format !== 'json') {
1088
+ log(`āœ… Found ${graph.size} package(s) (${((Date.now() - startTime) / 1000).toFixed(1)}s)\n`, 'green');
1089
+ log(`šŸ” Running analysis...\n`, 'gray');
1090
+ }
1091
+
1092
+ // Phase 2: Metrics Calculation
1093
+ const metrics = calculateMetrics(graph);
1094
+
1095
+ if (options.format !== 'json') {
1096
+ log(`āœ… Calculated metrics for ${metrics.size} packages (${((Date.now() - startTime) / 1000).toFixed(1)}s)\n`, 'green');
1097
+ }
1098
+
1099
+ // Phase 3: Anomaly Detection
1100
+ const anomalies = detectAnomalies(graph, metrics);
1101
+
1102
+ if (options.format !== 'json') {
1103
+ log(`āœ… Detected ${anomalies.length} anomalies (${((Date.now() - startTime) / 1000).toFixed(1)}s)\n`, 'green');
1104
+ }
1105
+
1106
+ // Phase 4: Output Generation
1107
+ const jsonData = generateJSON(graph, metrics, anomalies, options);
1108
+
1109
+ // Phase 5: Trend Analysis (load historical data and compare)
1110
+ const archDir = path.join(rootDir, '.kb', 'architecture');
1111
+ const history = loadHistoricalData(archDir);
1112
+ const trends = generateTrendAnalysis(jsonData, history);
1113
+
1114
+ if (trends) {
1115
+ jsonData.trends = trends;
1116
+
1117
+ if (options.format !== 'json') {
1118
+ log(`šŸ“Š Trend Analysis:`, 'blue');
1119
+ log(
1120
+ ` Health Score: ${trends.healthScore.previous} → ${trends.healthScore.current} (${trends.healthScore.delta >= 0 ? '+' : ''}${trends.healthScore.delta}) ${trends.healthScore.direction}`,
1121
+ trends.healthScore.status === 'āœ…' ? 'green' : trends.healthScore.status === 'āŒ' ? 'red' : 'yellow'
1122
+ );
1123
+ log(
1124
+ ` Anomalies: ${trends.anomaliesCount.previous} → ${trends.anomaliesCount.current} (${trends.anomaliesCount.delta >= 0 ? '+' : ''}${trends.anomaliesCount.delta}) ${trends.anomaliesCount.direction}`,
1125
+ trends.anomaliesCount.status === 'āœ…' ? 'green' : trends.anomaliesCount.status === 'āŒ' ? 'red' : 'yellow'
1126
+ );
1127
+
1128
+ if (trends.historical) {
1129
+ log(
1130
+ ` Over ${trends.historical.period}: Health ${trends.historical.healthScoreTrend >= 0 ? '+' : ''}${trends.historical.healthScoreTrend}, Anomalies ${trends.historical.anomaliesTrend >= 0 ? '+' : ''}${trends.historical.anomaliesTrend}`,
1131
+ 'gray'
1132
+ );
1133
+ }
1134
+
1135
+ log('');
1136
+ }
1137
+ }
1138
+
1139
+ // Filter by layer if specified
1140
+ if (options.layer) {
1141
+ jsonData.packages = jsonData.packages.filter((p) => p.layer === options.layer);
1142
+ jsonData.anomalies = jsonData.anomalies.filter((a) => {
1143
+ if (a.package) {return jsonData.packages.some((p) => p.name === a.package);}
1144
+ if (a.packages) {return a.packages.some((pkg) => jsonData.packages.some((p) => p.name === pkg));}
1145
+ return false;
1146
+ });
1147
+ }
1148
+
1149
+ // Filter by threshold if specified
1150
+ if (options.threshold > 0) {
1151
+ jsonData.anomalies = jsonData.anomalies.filter((a) => a.score >= options.threshold);
1152
+ }
1153
+
1154
+ // Output based on format
1155
+ if (options.format === 'json' || options.ai) {
1156
+ console.log(JSON.stringify(jsonData, null, 2));
1157
+ process.exit(0);
1158
+ }
1159
+
1160
+ if (options.format === 'md' || options.format === 'all') {
1161
+ const markdownReport = generateMarkdownReport(jsonData);
1162
+
1163
+ if (options.format === 'md') {
1164
+ console.log(markdownReport);
1165
+ process.exit(0);
1166
+ }
1167
+
1168
+ // Save to file
1169
+ const outputDir = path.join(rootDir, '.kb', 'architecture');
1170
+ if (!fs.existsSync(outputDir)) {
1171
+ fs.mkdirSync(outputDir, { recursive: true });
1172
+ }
1173
+
1174
+ const timestamp = new Date().toISOString().split('T')[0];
1175
+ const reportPath = path.join(outputDir, `report-${timestamp}.md`);
1176
+ const jsonPath = path.join(outputDir, `architecture-${timestamp}.json`);
1177
+
1178
+ fs.writeFileSync(reportPath, markdownReport, 'utf-8');
1179
+ fs.writeFileSync(jsonPath, JSON.stringify(jsonData, null, 2), 'utf-8');
1180
+
1181
+ log(`\nšŸ“ Generated files:`, 'blue');
1182
+ log(` - ${reportPath}`, 'cyan');
1183
+ log(` - ${jsonPath}`, 'cyan');
1184
+ log('');
1185
+ }
1186
+
1187
+ // Terminal output
1188
+ log(`\nšŸ“ˆ Health Score: ${jsonData.metadata.healthScore}/100 (Grade ${jsonData.metadata.healthGrade})\n`, 'blue');
1189
+
1190
+ // Top 10 Issues
1191
+ const top10 = jsonData.anomalies.slice(0, 10);
1192
+ if (top10.length > 0) {
1193
+ log(`🚨 Top ${top10.length} Issues Requiring Attention:\n`, 'red');
1194
+
1195
+ for (let i = 0; i < top10.length; i++) {
1196
+ const anomaly = top10[i];
1197
+ const emoji = anomaly.severity === 'critical' ? 'šŸ”“' : anomaly.severity === 'high' ? '🟠' : '🟔';
1198
+
1199
+ log(` ${i + 1}. ${emoji} ${anomaly.type.replace(/-/g, ' ')}: ${anomaly.package || anomaly.packages?.join(' ↔ ')} (Score: ${anomaly.score})`, 'yellow');
1200
+ log(` Impact: ${anomaly.impact}`, 'gray');
1201
+ log(` Fix: ${anomaly.recommendation}`, 'gray');
1202
+ log('');
1203
+ }
1204
+ } else {
1205
+ log(`āœ… No critical anomalies detected!\n`, 'green');
1206
+ }
1207
+
1208
+ log(`šŸ’” Tips:`, 'blue');
1209
+ log(` • Use --format=json to get machine-readable output`, 'gray');
1210
+ log(` • Use --format=md to get markdown report`, 'gray');
1211
+ log(` • Use --ai to pipe output to AI agent`, 'gray');
1212
+ log(` • Use --layer=core to filter by specific layer`, 'gray');
1213
+ log(` • Use --threshold=70 to show only high-impact issues`, 'gray');
1214
+ log('');
1215
+
1216
+ process.exit(0);
1217
+ }
1218
+
1219
+ main().catch((error) => {
1220
+ console.error('āŒ Error:', error.message);
1221
+ if (process.env.DEBUG) {
1222
+ console.error(error.stack);
1223
+ }
1224
+ process.exit(1);
1225
+ });