mstro-app 0.4.3 → 0.4.4

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 (306) hide show
  1. package/dist/server/cli/headless/claude-invoker-process.d.ts +11 -0
  2. package/dist/server/cli/headless/claude-invoker-process.d.ts.map +1 -0
  3. package/dist/server/cli/headless/claude-invoker-process.js +140 -0
  4. package/dist/server/cli/headless/claude-invoker-process.js.map +1 -0
  5. package/dist/server/cli/headless/claude-invoker-stall.d.ts +40 -0
  6. package/dist/server/cli/headless/claude-invoker-stall.d.ts.map +1 -0
  7. package/dist/server/cli/headless/claude-invoker-stall.js +98 -0
  8. package/dist/server/cli/headless/claude-invoker-stall.js.map +1 -0
  9. package/dist/server/cli/headless/claude-invoker-stream.d.ts +44 -0
  10. package/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -0
  11. package/dist/server/cli/headless/claude-invoker-stream.js +276 -0
  12. package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -0
  13. package/dist/server/cli/headless/claude-invoker-tools.d.ts +21 -0
  14. package/dist/server/cli/headless/claude-invoker-tools.d.ts.map +1 -0
  15. package/dist/server/cli/headless/claude-invoker-tools.js +137 -0
  16. package/dist/server/cli/headless/claude-invoker-tools.js.map +1 -0
  17. package/dist/server/cli/headless/claude-invoker.d.ts +6 -4
  18. package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
  19. package/dist/server/cli/headless/claude-invoker.js +10 -807
  20. package/dist/server/cli/headless/claude-invoker.js.map +1 -1
  21. package/dist/server/cli/headless/haiku-assessments.d.ts +62 -0
  22. package/dist/server/cli/headless/haiku-assessments.d.ts.map +1 -0
  23. package/dist/server/cli/headless/haiku-assessments.js +281 -0
  24. package/dist/server/cli/headless/haiku-assessments.js.map +1 -0
  25. package/dist/server/cli/headless/headless-logger.d.ts +3 -2
  26. package/dist/server/cli/headless/headless-logger.d.ts.map +1 -1
  27. package/dist/server/cli/headless/headless-logger.js +28 -5
  28. package/dist/server/cli/headless/headless-logger.js.map +1 -1
  29. package/dist/server/cli/headless/native-timeout-detector.d.ts +44 -0
  30. package/dist/server/cli/headless/native-timeout-detector.d.ts.map +1 -0
  31. package/dist/server/cli/headless/native-timeout-detector.js +99 -0
  32. package/dist/server/cli/headless/native-timeout-detector.js.map +1 -0
  33. package/dist/server/cli/headless/stall-assessor.d.ts +2 -110
  34. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  35. package/dist/server/cli/headless/stall-assessor.js +65 -457
  36. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  37. package/dist/server/cli/improvisation-attachments.d.ts +21 -0
  38. package/dist/server/cli/improvisation-attachments.d.ts.map +1 -0
  39. package/dist/server/cli/improvisation-attachments.js +116 -0
  40. package/dist/server/cli/improvisation-attachments.js.map +1 -0
  41. package/dist/server/cli/improvisation-retry.d.ts +52 -0
  42. package/dist/server/cli/improvisation-retry.d.ts.map +1 -0
  43. package/dist/server/cli/improvisation-retry.js +434 -0
  44. package/dist/server/cli/improvisation-retry.js.map +1 -0
  45. package/dist/server/cli/improvisation-session-manager.d.ts +10 -266
  46. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  47. package/dist/server/cli/improvisation-session-manager.js +117 -1079
  48. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  49. package/dist/server/cli/improvisation-types.d.ts +86 -0
  50. package/dist/server/cli/improvisation-types.d.ts.map +1 -0
  51. package/dist/server/cli/improvisation-types.js +10 -0
  52. package/dist/server/cli/improvisation-types.js.map +1 -0
  53. package/dist/server/cli/prompt-builders.d.ts +68 -0
  54. package/dist/server/cli/prompt-builders.d.ts.map +1 -0
  55. package/dist/server/cli/prompt-builders.js +312 -0
  56. package/dist/server/cli/prompt-builders.js.map +1 -0
  57. package/dist/server/index.js +33 -212
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/mcp/bouncer-haiku.d.ts +10 -0
  60. package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -0
  61. package/dist/server/mcp/bouncer-haiku.js +152 -0
  62. package/dist/server/mcp/bouncer-haiku.js.map +1 -0
  63. package/dist/server/mcp/bouncer-integration.d.ts +3 -4
  64. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
  65. package/dist/server/mcp/bouncer-integration.js +50 -196
  66. package/dist/server/mcp/bouncer-integration.js.map +1 -1
  67. package/dist/server/mcp/security-analysis.d.ts +38 -0
  68. package/dist/server/mcp/security-analysis.d.ts.map +1 -0
  69. package/dist/server/mcp/security-analysis.js +183 -0
  70. package/dist/server/mcp/security-analysis.js.map +1 -0
  71. package/dist/server/mcp/security-audit.d.ts +1 -1
  72. package/dist/server/mcp/security-audit.d.ts.map +1 -1
  73. package/dist/server/mcp/security-patterns.d.ts +1 -25
  74. package/dist/server/mcp/security-patterns.d.ts.map +1 -1
  75. package/dist/server/mcp/security-patterns.js +55 -260
  76. package/dist/server/mcp/security-patterns.js.map +1 -1
  77. package/dist/server/server-setup.d.ts +22 -0
  78. package/dist/server/server-setup.d.ts.map +1 -0
  79. package/dist/server/server-setup.js +101 -0
  80. package/dist/server/server-setup.js.map +1 -0
  81. package/dist/server/services/file-explorer-ops.d.ts +24 -0
  82. package/dist/server/services/file-explorer-ops.d.ts.map +1 -0
  83. package/dist/server/services/file-explorer-ops.js +211 -0
  84. package/dist/server/services/file-explorer-ops.js.map +1 -0
  85. package/dist/server/services/files.d.ts +2 -85
  86. package/dist/server/services/files.d.ts.map +1 -1
  87. package/dist/server/services/files.js +7 -427
  88. package/dist/server/services/files.js.map +1 -1
  89. package/dist/server/services/plan/composer.d.ts.map +1 -1
  90. package/dist/server/services/plan/composer.js +2 -1
  91. package/dist/server/services/plan/composer.js.map +1 -1
  92. package/dist/server/services/plan/executor.d.ts.map +1 -1
  93. package/dist/server/services/plan/executor.js +3 -1
  94. package/dist/server/services/plan/executor.js.map +1 -1
  95. package/dist/server/services/plan/parser-core.d.ts +20 -0
  96. package/dist/server/services/plan/parser-core.d.ts.map +1 -0
  97. package/dist/server/services/plan/parser-core.js +350 -0
  98. package/dist/server/services/plan/parser-core.js.map +1 -0
  99. package/dist/server/services/plan/parser-migration.d.ts +5 -0
  100. package/dist/server/services/plan/parser-migration.d.ts.map +1 -0
  101. package/dist/server/services/plan/parser-migration.js +124 -0
  102. package/dist/server/services/plan/parser-migration.js.map +1 -0
  103. package/dist/server/services/plan/parser.d.ts +0 -8
  104. package/dist/server/services/plan/parser.d.ts.map +1 -1
  105. package/dist/server/services/plan/parser.js +50 -569
  106. package/dist/server/services/plan/parser.js.map +1 -1
  107. package/dist/server/services/plan/review-gate.d.ts +2 -0
  108. package/dist/server/services/plan/review-gate.d.ts.map +1 -1
  109. package/dist/server/services/plan/review-gate.js +2 -2
  110. package/dist/server/services/plan/review-gate.js.map +1 -1
  111. package/dist/server/services/plan/types.d.ts +2 -0
  112. package/dist/server/services/plan/types.d.ts.map +1 -1
  113. package/dist/server/services/platform-credentials.d.ts +24 -0
  114. package/dist/server/services/platform-credentials.d.ts.map +1 -0
  115. package/dist/server/services/platform-credentials.js +68 -0
  116. package/dist/server/services/platform-credentials.js.map +1 -0
  117. package/dist/server/services/platform.d.ts +1 -31
  118. package/dist/server/services/platform.d.ts.map +1 -1
  119. package/dist/server/services/platform.js +10 -119
  120. package/dist/server/services/platform.js.map +1 -1
  121. package/dist/server/services/terminal/pty-manager.d.ts +7 -97
  122. package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
  123. package/dist/server/services/terminal/pty-manager.js +53 -266
  124. package/dist/server/services/terminal/pty-manager.js.map +1 -1
  125. package/dist/server/services/terminal/pty-utils.d.ts +57 -0
  126. package/dist/server/services/terminal/pty-utils.d.ts.map +1 -0
  127. package/dist/server/services/terminal/pty-utils.js +141 -0
  128. package/dist/server/services/terminal/pty-utils.js.map +1 -0
  129. package/dist/server/services/websocket/file-definition-handlers.d.ts +4 -0
  130. package/dist/server/services/websocket/file-definition-handlers.d.ts.map +1 -0
  131. package/dist/server/services/websocket/file-definition-handlers.js +153 -0
  132. package/dist/server/services/websocket/file-definition-handlers.js.map +1 -0
  133. package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -1
  134. package/dist/server/services/websocket/file-explorer-handlers.js +52 -391
  135. package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
  136. package/dist/server/services/websocket/file-search-handlers.d.ts +5 -0
  137. package/dist/server/services/websocket/file-search-handlers.d.ts.map +1 -0
  138. package/dist/server/services/websocket/file-search-handlers.js +238 -0
  139. package/dist/server/services/websocket/file-search-handlers.js.map +1 -0
  140. package/dist/server/services/websocket/file-utils.js +3 -3
  141. package/dist/server/services/websocket/file-utils.js.map +1 -1
  142. package/dist/server/services/websocket/git-branch-handlers.d.ts +7 -0
  143. package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -0
  144. package/dist/server/services/websocket/git-branch-handlers.js +110 -0
  145. package/dist/server/services/websocket/git-branch-handlers.js.map +1 -0
  146. package/dist/server/services/websocket/git-diff-handlers.d.ts +6 -0
  147. package/dist/server/services/websocket/git-diff-handlers.d.ts.map +1 -0
  148. package/dist/server/services/websocket/git-diff-handlers.js +123 -0
  149. package/dist/server/services/websocket/git-diff-handlers.js.map +1 -0
  150. package/dist/server/services/websocket/git-handlers.d.ts +2 -31
  151. package/dist/server/services/websocket/git-handlers.d.ts.map +1 -1
  152. package/dist/server/services/websocket/git-handlers.js +35 -541
  153. package/dist/server/services/websocket/git-handlers.js.map +1 -1
  154. package/dist/server/services/websocket/git-log-handlers.d.ts +6 -0
  155. package/dist/server/services/websocket/git-log-handlers.d.ts.map +1 -0
  156. package/dist/server/services/websocket/git-log-handlers.js +128 -0
  157. package/dist/server/services/websocket/git-log-handlers.js.map +1 -0
  158. package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -1
  159. package/dist/server/services/websocket/git-pr-handlers.js +13 -53
  160. package/dist/server/services/websocket/git-pr-handlers.js.map +1 -1
  161. package/dist/server/services/websocket/git-tag-handlers.d.ts +6 -0
  162. package/dist/server/services/websocket/git-tag-handlers.d.ts.map +1 -0
  163. package/dist/server/services/websocket/git-tag-handlers.js +76 -0
  164. package/dist/server/services/websocket/git-tag-handlers.js.map +1 -0
  165. package/dist/server/services/websocket/git-utils.d.ts +43 -0
  166. package/dist/server/services/websocket/git-utils.d.ts.map +1 -0
  167. package/dist/server/services/websocket/git-utils.js +201 -0
  168. package/dist/server/services/websocket/git-utils.js.map +1 -0
  169. package/dist/server/services/websocket/handler.d.ts +2 -0
  170. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  171. package/dist/server/services/websocket/handler.js +37 -126
  172. package/dist/server/services/websocket/handler.js.map +1 -1
  173. package/dist/server/services/websocket/plan-board-handlers.d.ts +11 -0
  174. package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -0
  175. package/dist/server/services/websocket/plan-board-handlers.js +218 -0
  176. package/dist/server/services/websocket/plan-board-handlers.js.map +1 -0
  177. package/dist/server/services/websocket/plan-execution-handlers.d.ts +9 -0
  178. package/dist/server/services/websocket/plan-execution-handlers.d.ts.map +1 -0
  179. package/dist/server/services/websocket/plan-execution-handlers.js +142 -0
  180. package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -0
  181. package/dist/server/services/websocket/plan-handlers.d.ts +7 -2
  182. package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -1
  183. package/dist/server/services/websocket/plan-handlers.js +6 -925
  184. package/dist/server/services/websocket/plan-handlers.js.map +1 -1
  185. package/dist/server/services/websocket/plan-helpers.d.ts +19 -0
  186. package/dist/server/services/websocket/plan-helpers.d.ts.map +1 -0
  187. package/dist/server/services/websocket/plan-helpers.js +199 -0
  188. package/dist/server/services/websocket/plan-helpers.js.map +1 -0
  189. package/dist/server/services/websocket/plan-issue-handlers.d.ts +12 -0
  190. package/dist/server/services/websocket/plan-issue-handlers.d.ts.map +1 -0
  191. package/dist/server/services/websocket/plan-issue-handlers.js +162 -0
  192. package/dist/server/services/websocket/plan-issue-handlers.js.map +1 -0
  193. package/dist/server/services/websocket/plan-sprint-handlers.d.ts +7 -0
  194. package/dist/server/services/websocket/plan-sprint-handlers.d.ts.map +1 -0
  195. package/dist/server/services/websocket/plan-sprint-handlers.js +206 -0
  196. package/dist/server/services/websocket/plan-sprint-handlers.js.map +1 -0
  197. package/dist/server/services/websocket/quality-complexity.d.ts +14 -0
  198. package/dist/server/services/websocket/quality-complexity.d.ts.map +1 -0
  199. package/dist/server/services/websocket/quality-complexity.js +262 -0
  200. package/dist/server/services/websocket/quality-complexity.js.map +1 -0
  201. package/dist/server/services/websocket/quality-fix-agent.d.ts +16 -0
  202. package/dist/server/services/websocket/quality-fix-agent.d.ts.map +1 -0
  203. package/dist/server/services/websocket/quality-fix-agent.js +140 -0
  204. package/dist/server/services/websocket/quality-fix-agent.js.map +1 -0
  205. package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
  206. package/dist/server/services/websocket/quality-handlers.js +34 -346
  207. package/dist/server/services/websocket/quality-handlers.js.map +1 -1
  208. package/dist/server/services/websocket/quality-linting.d.ts +9 -0
  209. package/dist/server/services/websocket/quality-linting.d.ts.map +1 -0
  210. package/dist/server/services/websocket/quality-linting.js +178 -0
  211. package/dist/server/services/websocket/quality-linting.js.map +1 -0
  212. package/dist/server/services/websocket/quality-review-agent.d.ts +19 -0
  213. package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -0
  214. package/dist/server/services/websocket/quality-review-agent.js +206 -0
  215. package/dist/server/services/websocket/quality-review-agent.js.map +1 -0
  216. package/dist/server/services/websocket/quality-service.d.ts +3 -51
  217. package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
  218. package/dist/server/services/websocket/quality-service.js +9 -651
  219. package/dist/server/services/websocket/quality-service.js.map +1 -1
  220. package/dist/server/services/websocket/quality-tools.d.ts +23 -0
  221. package/dist/server/services/websocket/quality-tools.d.ts.map +1 -0
  222. package/dist/server/services/websocket/quality-tools.js +208 -0
  223. package/dist/server/services/websocket/quality-tools.js.map +1 -0
  224. package/dist/server/services/websocket/quality-types.d.ts +59 -0
  225. package/dist/server/services/websocket/quality-types.d.ts.map +1 -0
  226. package/dist/server/services/websocket/quality-types.js +101 -0
  227. package/dist/server/services/websocket/quality-types.js.map +1 -0
  228. package/dist/server/services/websocket/session-handlers.d.ts +3 -4
  229. package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
  230. package/dist/server/services/websocket/session-handlers.js +3 -378
  231. package/dist/server/services/websocket/session-handlers.js.map +1 -1
  232. package/dist/server/services/websocket/session-history.d.ts +4 -0
  233. package/dist/server/services/websocket/session-history.d.ts.map +1 -0
  234. package/dist/server/services/websocket/session-history.js +208 -0
  235. package/dist/server/services/websocket/session-history.js.map +1 -0
  236. package/dist/server/services/websocket/session-initialization.d.ts +5 -0
  237. package/dist/server/services/websocket/session-initialization.d.ts.map +1 -0
  238. package/dist/server/services/websocket/session-initialization.js +163 -0
  239. package/dist/server/services/websocket/session-initialization.js.map +1 -0
  240. package/dist/server/services/websocket/types.d.ts +12 -2
  241. package/dist/server/services/websocket/types.d.ts.map +1 -1
  242. package/package.json +1 -1
  243. package/server/cli/headless/claude-invoker-process.ts +204 -0
  244. package/server/cli/headless/claude-invoker-stall.ts +164 -0
  245. package/server/cli/headless/claude-invoker-stream.ts +353 -0
  246. package/server/cli/headless/claude-invoker-tools.ts +187 -0
  247. package/server/cli/headless/claude-invoker.ts +15 -1096
  248. package/server/cli/headless/haiku-assessments.ts +365 -0
  249. package/server/cli/headless/headless-logger.ts +26 -5
  250. package/server/cli/headless/native-timeout-detector.ts +117 -0
  251. package/server/cli/headless/stall-assessor.ts +65 -618
  252. package/server/cli/improvisation-attachments.ts +148 -0
  253. package/server/cli/improvisation-retry.ts +602 -0
  254. package/server/cli/improvisation-session-manager.ts +140 -1349
  255. package/server/cli/improvisation-types.ts +98 -0
  256. package/server/cli/prompt-builders.ts +370 -0
  257. package/server/index.ts +35 -246
  258. package/server/mcp/bouncer-haiku.ts +182 -0
  259. package/server/mcp/bouncer-integration.ts +87 -248
  260. package/server/mcp/security-analysis.ts +217 -0
  261. package/server/mcp/security-audit.ts +1 -1
  262. package/server/mcp/security-patterns.ts +60 -283
  263. package/server/server-setup.ts +114 -0
  264. package/server/services/file-explorer-ops.ts +293 -0
  265. package/server/services/files.ts +20 -532
  266. package/server/services/plan/composer.ts +2 -1
  267. package/server/services/plan/executor.ts +3 -1
  268. package/server/services/plan/parser-core.ts +406 -0
  269. package/server/services/plan/parser-migration.ts +128 -0
  270. package/server/services/plan/parser.ts +52 -620
  271. package/server/services/plan/review-gate.ts +4 -2
  272. package/server/services/plan/types.ts +2 -0
  273. package/server/services/platform-credentials.ts +83 -0
  274. package/server/services/platform.ts +15 -141
  275. package/server/services/terminal/pty-manager.ts +66 -313
  276. package/server/services/terminal/pty-utils.ts +176 -0
  277. package/server/services/websocket/file-definition-handlers.ts +165 -0
  278. package/server/services/websocket/file-explorer-handlers.ts +37 -452
  279. package/server/services/websocket/file-search-handlers.ts +291 -0
  280. package/server/services/websocket/file-utils.ts +3 -3
  281. package/server/services/websocket/git-branch-handlers.ts +130 -0
  282. package/server/services/websocket/git-diff-handlers.ts +140 -0
  283. package/server/services/websocket/git-handlers.ts +40 -625
  284. package/server/services/websocket/git-log-handlers.ts +149 -0
  285. package/server/services/websocket/git-pr-handlers.ts +17 -62
  286. package/server/services/websocket/git-tag-handlers.ts +91 -0
  287. package/server/services/websocket/git-utils.ts +230 -0
  288. package/server/services/websocket/handler.ts +39 -126
  289. package/server/services/websocket/plan-board-handlers.ts +277 -0
  290. package/server/services/websocket/plan-execution-handlers.ts +184 -0
  291. package/server/services/websocket/plan-handlers.ts +8 -1114
  292. package/server/services/websocket/plan-helpers.ts +215 -0
  293. package/server/services/websocket/plan-issue-handlers.ts +204 -0
  294. package/server/services/websocket/plan-sprint-handlers.ts +252 -0
  295. package/server/services/websocket/quality-complexity.ts +294 -0
  296. package/server/services/websocket/quality-fix-agent.ts +181 -0
  297. package/server/services/websocket/quality-handlers.ts +36 -404
  298. package/server/services/websocket/quality-linting.ts +187 -0
  299. package/server/services/websocket/quality-review-agent.ts +246 -0
  300. package/server/services/websocket/quality-service.ts +11 -762
  301. package/server/services/websocket/quality-tools.ts +209 -0
  302. package/server/services/websocket/quality-types.ts +169 -0
  303. package/server/services/websocket/session-handlers.ts +5 -437
  304. package/server/services/websocket/session-history.ts +222 -0
  305. package/server/services/websocket/session-initialization.ts +209 -0
  306. package/server/services/websocket/types.ts +17 -0
@@ -1,546 +1,15 @@
1
1
  // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
2
  // Licensed under the MIT License. See LICENSE file for details.
3
3
 
4
- import { spawn } from 'node:child_process';
5
- import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
6
- import { extname, join, relative } from 'node:path';
4
+ import { extname } from 'node:path';
5
+ import { analyzeComplexity, analyzeFunctionLength } from './quality-complexity.js';
6
+ import { analyzeLinting } from './quality-linting.js';
7
+ import { collectSourceFiles, detectEcosystem, runCommand, type SourceFile } from './quality-tools.js';
8
+ import { type CategoryScore, type Ecosystem, FILE_LENGTH_THRESHOLD, hasInstalledToolInCategory, type QualityFinding, type QualityResults, type ScanProgress, TOTAL_STEPS } from './quality-types.js';
7
9
 
8
- // ============================================================================
9
- // Types
10
- // ============================================================================
11
-
12
- export interface QualityTool {
13
- name: string;
14
- installed: boolean;
15
- installCommand: string;
16
- category: 'linter' | 'formatter' | 'complexity' | 'general';
17
- }
18
-
19
- export interface CategoryScore {
20
- name: string;
21
- score: number;
22
- weight: number;
23
- effectiveWeight: number;
24
- available: boolean;
25
- issueCount?: number;
26
- details?: Record<string, unknown>;
27
- }
28
-
29
- export interface QualityFinding {
30
- severity: 'critical' | 'high' | 'medium' | 'low';
31
- category: string;
32
- file: string;
33
- line: number | null;
34
- title: string;
35
- description: string;
36
- suggestion?: string;
37
- }
38
-
39
- export interface QualityResults {
40
- overall: number;
41
- grade: string;
42
- categories: CategoryScore[];
43
- findings: QualityFinding[];
44
- codeReview: QualityFinding[];
45
- analyzedFiles: number;
46
- totalLines: number;
47
- timestamp: string;
48
- ecosystem: string[];
49
- }
50
-
51
- export interface ScanProgress {
52
- step: string;
53
- current: number;
54
- total: number;
55
- }
56
-
57
- type Ecosystem = 'node' | 'python' | 'rust' | 'go' | 'swift' | 'kotlin' | 'unknown';
58
-
59
- interface ToolSpec {
60
- name: string;
61
- check: string[];
62
- category: QualityTool['category'];
63
- installCmd: string;
64
- }
65
-
66
- // ============================================================================
67
- // Constants
68
- // ============================================================================
69
-
70
- const ECOSYSTEM_TOOLS: Record<Ecosystem, ToolSpec[]> = {
71
- node: [
72
- { name: 'eslint', check: ['npx', 'eslint', '--version'], category: 'linter', installCmd: 'npm install -D eslint' },
73
- { name: 'biome', check: ['npx', '@biomejs/biome', '--version'], category: 'linter', installCmd: 'npm install -D @biomejs/biome' },
74
- { name: 'prettier', check: ['npx', 'prettier', '--version'], category: 'formatter', installCmd: 'npm install -D prettier' },
75
- { name: 'typescript', check: ['npx', 'tsc', '--version'], category: 'general', installCmd: 'npm install -D typescript' },
76
- ],
77
- python: [
78
- { name: 'ruff', check: ['ruff', '--version'], category: 'linter', installCmd: 'uv tool install ruff || pip install ruff' },
79
- { name: 'black', check: ['black', '--version'], category: 'formatter', installCmd: 'uv tool install black || pip install black' },
80
- { name: 'radon', check: ['radon', '--version'], category: 'complexity', installCmd: 'uv tool install radon || pip install radon' },
81
- ],
82
- rust: [
83
- { name: 'clippy', check: ['cargo', 'clippy', '--version'], category: 'linter', installCmd: 'rustup component add clippy' },
84
- { name: 'rustfmt', check: ['rustfmt', '--version'], category: 'formatter', installCmd: 'rustup component add rustfmt' },
85
- ],
86
- go: [
87
- { name: 'golangci-lint', check: ['golangci-lint', '--version'], category: 'linter', installCmd: 'go install github.com/golangci-lint/golangci-lint/cmd/golangci-lint@latest' },
88
- { name: 'gofmt', check: ['gofmt', '-h'], category: 'formatter', installCmd: '(built-in with Go)' },
89
- ],
90
- swift: [
91
- { name: 'swiftlint', check: ['swiftlint', '--version'], category: 'linter', installCmd: 'brew install swiftlint' },
92
- { name: 'swiftformat', check: ['swiftformat', '--version'], category: 'formatter', installCmd: 'brew install swiftformat' },
93
- ],
94
- kotlin: [
95
- { name: 'ktlint', check: ['ktlint', '--version'], category: 'linter', installCmd: 'brew install ktlint' },
96
- { name: 'ktfmt', check: ['ktfmt', '--version'], category: 'formatter', installCmd: 'brew install ktfmt' },
97
- ],
98
- unknown: [],
99
- };
100
-
101
- const SOURCE_EXTENSIONS = new Set([
102
- '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
103
- '.py', '.pyi',
104
- '.rs',
105
- '.go',
106
- '.java', '.kt',
107
- '.cs',
108
- '.rb',
109
- '.php',
110
- '.swift',
111
- '.c', '.cpp', '.h', '.hpp',
112
- ]);
113
-
114
- const IGNORE_DIRS = new Set([
115
- 'node_modules', '.git', 'dist', 'build', '.next', '__pycache__',
116
- 'target', 'vendor', '.venv', 'venv', '.tox', 'coverage',
117
- '.mstro', '.cache', '.turbo', '.output',
118
- ]);
119
-
120
- const FILE_LENGTH_THRESHOLD = 300;
121
- const FUNCTION_LENGTH_THRESHOLD = 50;
122
- const TOTAL_STEPS = 7;
123
-
124
- function hasInstalledToolInCategory(
125
- installedSet: Set<string>,
126
- ecosystems: Ecosystem[],
127
- category: QualityTool['category'],
128
- ): boolean {
129
- for (const eco of ecosystems) {
130
- const specs = ECOSYSTEM_TOOLS[eco] || [];
131
- for (const spec of specs) {
132
- if (spec.category === category && installedSet.has(spec.name)) return true;
133
- }
134
- }
135
- return false;
136
- }
137
-
138
- // ============================================================================
139
- // Ecosystem Detection
140
- // ============================================================================
141
-
142
- export function detectEcosystem(dirPath: string): Ecosystem[] {
143
- const ecosystems: Ecosystem[] = [];
144
- try {
145
- const files = readdirSync(dirPath);
146
- if (files.includes('package.json')) ecosystems.push('node');
147
- if (files.includes('pyproject.toml') || files.includes('setup.py') || files.includes('requirements.txt')) ecosystems.push('python');
148
- if (files.includes('Cargo.toml')) ecosystems.push('rust');
149
- if (files.includes('go.mod')) ecosystems.push('go');
150
- if (files.includes('Package.swift') || files.some(f => f.endsWith('.xcodeproj') || f.endsWith('.xcworkspace'))) ecosystems.push('swift');
151
- if (files.includes('build.gradle') || files.includes('build.gradle.kts')) ecosystems.push('kotlin');
152
- } catch {
153
- // Directory not readable
154
- }
155
- if (ecosystems.length === 0) ecosystems.push('unknown');
156
- return ecosystems;
157
- }
158
-
159
- /** Detect the Node.js package manager from lockfiles */
160
- function detectNodePackageManager(dirPath: string): 'npm' | 'yarn' | 'pnpm' | 'bun' {
161
- try {
162
- const files = readdirSync(dirPath);
163
- if (files.includes('bun.lockb') || files.includes('bun.lock')) return 'bun';
164
- if (files.includes('pnpm-lock.yaml')) return 'pnpm';
165
- if (files.includes('yarn.lock')) return 'yarn';
166
- } catch {
167
- // Directory not readable
168
- }
169
- return 'npm';
170
- }
171
-
172
- /** Build the install command for a Node.js dev dependency */
173
- function nodeInstallCmd(pm: 'npm' | 'yarn' | 'pnpm' | 'bun', pkg: string): string {
174
- switch (pm) {
175
- case 'yarn': return `yarn add -D ${pkg}`;
176
- case 'pnpm': return `pnpm add -D ${pkg}`;
177
- case 'bun': return `bun add -d ${pkg}`;
178
- default: return `npm install -D ${pkg}`;
179
- }
180
- }
181
-
182
- // ============================================================================
183
- // Tool Detection
184
- // ============================================================================
185
-
186
- async function checkToolInstalled(check: string[], cwd: string): Promise<boolean> {
187
- return new Promise((resolve) => {
188
- const proc = spawn(check[0], check.slice(1), {
189
- cwd,
190
- stdio: ['ignore', 'pipe', 'pipe'],
191
- timeout: 10000,
192
- });
193
- proc.on('close', (code) => resolve(code === 0));
194
- proc.on('error', () => resolve(false));
195
- });
196
- }
197
-
198
- export async function detectTools(dirPath: string): Promise<{ tools: QualityTool[]; ecosystem: string[] }> {
199
- const ecosystems = detectEcosystem(dirPath);
200
- const tools: QualityTool[] = [];
201
- const nodePm = ecosystems.includes('node') ? detectNodePackageManager(dirPath) : 'npm';
202
-
203
- for (const eco of ecosystems) {
204
- const specs = ECOSYSTEM_TOOLS[eco] || [];
205
- for (const spec of specs) {
206
- const installed = await checkToolInstalled(spec.check, dirPath);
207
- // For node tools, resolve install command using the project's package manager
208
- const installCommand = eco === 'node'
209
- ? nodeInstallCmd(nodePm, spec.installCmd.replace(/^npm install -D /, ''))
210
- : spec.installCmd;
211
- tools.push({
212
- name: spec.name,
213
- installed,
214
- installCommand,
215
- category: spec.category,
216
- });
217
- }
218
- }
219
-
220
- return { tools, ecosystem: ecosystems };
221
- }
222
-
223
- // ============================================================================
224
- // Tool Installation
225
- // ============================================================================
226
-
227
- export async function installTools(
228
- dirPath: string,
229
- toolNames?: string[],
230
- ): Promise<{ tools: QualityTool[]; ecosystem: string[] }> {
231
- const { tools } = await detectTools(dirPath);
232
- const toInstall = tools.filter((t) => !t.installed && (!toolNames || toolNames.includes(t.name)));
233
-
234
- const failures: string[] = [];
235
- for (const tool of toInstall) {
236
- if (tool.installCommand.startsWith('(')) continue; // built-in, skip
237
- // Support chained commands with || (try first, fallback to second)
238
- const commands = tool.installCommand.split(' || ');
239
- let installed = false;
240
- for (const cmd of commands) {
241
- const parts = cmd.trim().split(' ');
242
- const result = await runCommand(parts[0], parts.slice(1), dirPath);
243
- if (result.exitCode === 0) { installed = true; break; }
244
- }
245
- if (!installed) {
246
- failures.push(`${tool.name}: all install methods failed`);
247
- }
248
- }
249
-
250
- // Re-detect after install
251
- const detected = await detectTools(dirPath);
252
-
253
- // Check if any requested tools are still missing after install
254
- const requestedNames = new Set(toolNames ?? toInstall.map((t) => t.name));
255
- const stillMissing = detected.tools.filter((t) => !t.installed && requestedNames.has(t.name)).map((t) => t.name);
256
-
257
- if (stillMissing.length > 0) {
258
- const detail = failures.length > 0 ? ` ${failures.join('; ')}` : '';
259
- throw new Error(`Failed to install: ${stillMissing.join(', ')}.${detail}`);
260
- }
261
-
262
- return detected;
263
- }
264
-
265
- // ============================================================================
266
- // File Scanning
267
- // ============================================================================
268
-
269
- interface SourceFile {
270
- path: string;
271
- relativePath: string;
272
- lines: number;
273
- content: string;
274
- }
275
-
276
- function tryStatSync(path: string): ReturnType<typeof statSync> | null {
277
- try { return statSync(path); } catch { return null; }
278
- }
279
-
280
- function tryReadFile(path: string): string | null {
281
- try { return readFileSync(path, 'utf-8'); } catch { return null; }
282
- }
283
-
284
- function tryReaddirSync(dir: string): string[] | null {
285
- try { return readdirSync(dir); } catch { return null; }
286
- }
287
-
288
- function tryReadSourceFile(fullPath: string, rootPath: string): SourceFile | null {
289
- const content = tryReadFile(fullPath);
290
- if (!content) return null;
291
- return {
292
- path: fullPath,
293
- relativePath: relative(rootPath, fullPath),
294
- lines: content.split('\n').length,
295
- content,
296
- };
297
- }
298
-
299
- function processEntry(entry: string, dir: string, rootPath: string, stack: string[], files: SourceFile[]): void {
300
- if (IGNORE_DIRS.has(entry)) return;
301
- const fullPath = join(dir, entry);
302
- const stat = tryStatSync(fullPath);
303
- if (!stat) return;
304
-
305
- if (stat.isDirectory()) { stack.push(fullPath); return; }
306
- if (!stat.isFile() || !SOURCE_EXTENSIONS.has(extname(entry).toLowerCase())) return;
307
-
308
- const sourceFile = tryReadSourceFile(fullPath, rootPath);
309
- if (sourceFile) files.push(sourceFile);
310
- }
311
-
312
- function collectSourceFiles(dirPath: string, rootPath: string): SourceFile[] {
313
- const files: SourceFile[] = [];
314
- const stack = [dirPath];
315
-
316
- while (stack.length > 0) {
317
- const dir = stack.pop()!;
318
- const entries = tryReaddirSync(dir);
319
- if (!entries) continue;
320
-
321
- for (const entry of entries) {
322
- processEntry(entry, dir, rootPath, stack, files);
323
- }
324
- }
325
-
326
- return files;
327
- }
328
-
329
- // ============================================================================
330
- // Command Runner
331
- // ============================================================================
332
-
333
- function runCommand(cmd: string, args: string[], cwd: string): Promise<{ stdout: string; stderr: string; exitCode: number }> {
334
- return new Promise((resolve) => {
335
- const proc = spawn(cmd, args, {
336
- cwd,
337
- stdio: ['ignore', 'pipe', 'pipe'],
338
- timeout: 120000,
339
- });
340
- let stdout = '';
341
- let stderr = '';
342
- proc.stdout?.on('data', (d: Buffer) => { stdout += d.toString(); });
343
- proc.stderr?.on('data', (d: Buffer) => { stderr += d.toString(); });
344
- proc.on('close', (code) => resolve({ stdout, stderr, exitCode: code ?? 1 }));
345
- proc.on('error', (err) => resolve({ stdout: '', stderr: err.message, exitCode: 1 }));
346
- });
347
- }
348
-
349
- // ============================================================================
350
- // Linting Analysis
351
- // ============================================================================
352
-
353
- interface LintAccumulator {
354
- errors: number;
355
- warnings: number;
356
- findings: QualityFinding[];
357
- ran: boolean;
358
- }
359
-
360
- function newLintAccumulator(): LintAccumulator {
361
- return { errors: 0, warnings: 0, findings: [], ran: false };
362
- }
363
-
364
- function biomeSeverity(severity: string): QualityFinding['severity'] {
365
- if (severity === 'error') return 'high';
366
- if (severity === 'warning') return 'medium';
367
- return 'low';
368
- }
369
-
370
- function processBiomeDiagnostic(d: Record<string, unknown>, acc: LintAccumulator): void {
371
- const sev = biomeSeverity(d.severity as string);
372
- if (d.severity === 'error') acc.errors++;
373
- else acc.warnings++;
374
- const location = d.location as Record<string, unknown> | undefined;
375
- const span = (location?.span as Record<string, unknown>) ?? {};
376
- const start = (span.start as Record<string, unknown>) ?? {};
377
- const message = d.message as Record<string, unknown> | string | undefined;
378
- acc.findings.push({
379
- severity: sev,
380
- category: 'linting',
381
- file: (location?.path as string) || '',
382
- line: (start.line as number) ?? null,
383
- title: (d.category as string) || 'Lint issue',
384
- description: (typeof message === 'object' ? (message?.text as string) : message) || '',
385
- });
386
- }
387
-
388
- function parseBiomeDiagnostics(stdout: string, acc: LintAccumulator): void {
389
- const parsed = JSON.parse(stdout);
390
- if (!parsed.diagnostics) return;
391
- for (const d of parsed.diagnostics) {
392
- processBiomeDiagnostic(d, acc);
393
- }
394
- }
395
-
396
- async function lintWithBiome(dirPath: string, acc: LintAccumulator): Promise<void> {
397
- const result = await runCommand('npx', ['@biomejs/biome', 'lint', '--reporter=json', '.'], dirPath);
398
- if (result.exitCode > 1) return;
399
-
400
- acc.ran = true;
401
- try {
402
- parseBiomeDiagnostics(result.stdout, acc);
403
- } catch {
404
- // JSON parse failed, try line counting
405
- acc.errors += (result.stdout.match(/error/gi) || []).length;
406
- acc.warnings += (result.stdout.match(/warning/gi) || []).length;
407
- acc.ran = acc.errors > 0 || acc.warnings > 0 || result.exitCode === 0;
408
- }
409
- }
410
-
411
- async function lintWithEslint(dirPath: string, acc: LintAccumulator): Promise<void> {
412
- const result = await runCommand('npx', ['eslint', '--format=json', '.'], dirPath);
413
- acc.ran = true;
414
- try {
415
- const parsed = JSON.parse(result.stdout);
416
- for (const file of parsed) {
417
- for (const msg of file.messages || []) {
418
- if (msg.severity === 2) acc.errors++;
419
- else acc.warnings++;
420
- acc.findings.push({
421
- severity: msg.severity === 2 ? 'high' : 'medium',
422
- category: 'linting',
423
- file: relative(dirPath, file.filePath),
424
- line: msg.line ?? null,
425
- title: msg.ruleId || 'Lint issue',
426
- description: msg.message,
427
- });
428
- }
429
- }
430
- } catch {
431
- acc.errors += (result.stderr.match(/error/gi) || []).length;
432
- acc.warnings += (result.stderr.match(/warning/gi) || []).length;
433
- }
434
- }
435
-
436
- async function lintNode(dirPath: string, acc: LintAccumulator): Promise<void> {
437
- const biomeConfig = existsSync(join(dirPath, 'biome.json')) || existsSync(join(dirPath, 'biome.jsonc'));
438
- if (biomeConfig) {
439
- await lintWithBiome(dirPath, acc);
440
- } else {
441
- await lintWithEslint(dirPath, acc);
442
- }
443
- }
444
-
445
- async function lintPython(dirPath: string, acc: LintAccumulator): Promise<void> {
446
- const result = await runCommand('ruff', ['check', '--output-format=json', '.'], dirPath);
447
- if (result.exitCode !== 0 && !result.stdout.trim().startsWith('[')) return;
448
-
449
- acc.ran = true;
450
- try {
451
- const parsed = JSON.parse(result.stdout);
452
- for (const item of parsed) {
453
- const sev = item.code?.startsWith('E') ? 'high' : 'medium';
454
- if (sev === 'high') acc.errors++;
455
- else acc.warnings++;
456
- acc.findings.push({
457
- severity: sev,
458
- category: 'linting',
459
- file: item.filename ? relative(dirPath, item.filename) : '',
460
- line: item.location?.row ?? null,
461
- title: item.code || 'Lint issue',
462
- description: item.message || '',
463
- });
464
- }
465
- } catch { /* ignore */ }
466
- }
467
-
468
- function processClippyMessage(msg: Record<string, unknown>, acc: LintAccumulator): void {
469
- if (msg.reason !== 'compiler-message' || !msg.message) return;
470
- const message = msg.message as Record<string, unknown>;
471
- const level = message.level as string;
472
- if (level === 'error') acc.errors++;
473
- else if (level === 'warning') acc.warnings++;
474
- const spans = message.spans as Array<Record<string, unknown>> | undefined;
475
- const span = spans?.[0];
476
- const code = message.code as Record<string, unknown> | undefined;
477
- acc.findings.push({
478
- severity: level === 'error' ? 'high' : 'medium',
479
- category: 'linting',
480
- file: (span?.file_name as string) || '',
481
- line: (span?.line_start as number) ?? null,
482
- title: (code?.code as string) || 'Clippy',
483
- description: (message.message as string) || '',
484
- });
485
- }
486
-
487
- function parseClippyOutput(stdout: string, acc: LintAccumulator): void {
488
- for (const line of stdout.split('\n')) {
489
- try {
490
- const msg = JSON.parse(line);
491
- processClippyMessage(msg, acc);
492
- } catch { /* not JSON line */ }
493
- }
494
- }
495
-
496
- async function lintRust(dirPath: string, acc: LintAccumulator): Promise<void> {
497
- const result = await runCommand('cargo', ['clippy', '--message-format=json', '--', '-W', 'clippy::all'], dirPath);
498
- if (result.exitCode > 1) return;
499
-
500
- acc.ran = true;
501
- parseClippyOutput(result.stdout, acc);
502
- }
503
-
504
- function computeLintScore(totalErrors: number, totalWarnings: number, totalLines: number): number {
505
- const kloc = Math.max(totalLines / 1000, 1);
506
- const penaltyRaw = totalErrors * 10 + totalWarnings * 3;
507
- const penaltyPerKloc = penaltyRaw / kloc;
508
-
509
- let score: number;
510
- if (penaltyPerKloc === 0) score = 100;
511
- else if (penaltyPerKloc <= 5) score = 100 - penaltyPerKloc * 2;
512
- else if (penaltyPerKloc <= 20) score = 90 - (penaltyPerKloc - 5) * 2;
513
- else if (penaltyPerKloc <= 50) score = 60 - (penaltyPerKloc - 20) * 1.5;
514
- else score = Math.max(0, 15 - (penaltyPerKloc - 50) * 0.3);
515
-
516
- return Math.round(Math.max(0, Math.min(100, score)));
517
- }
518
-
519
- async function analyzeLinting(
520
- dirPath: string,
521
- ecosystems: Ecosystem[],
522
- files: SourceFile[],
523
- ): Promise<{ score: number; findings: QualityFinding[]; available: boolean; issueCount: number }> {
524
- const acc = newLintAccumulator();
525
-
526
- if (ecosystems.includes('node')) await lintNode(dirPath, acc);
527
- if (ecosystems.includes('python')) await lintPython(dirPath, acc);
528
- if (ecosystems.includes('rust')) await lintRust(dirPath, acc);
529
-
530
- if (!acc.ran) {
531
- return { score: 0, findings: [], available: false, issueCount: 0 };
532
- }
533
-
534
- const totalLines = files.reduce((sum, f) => sum + f.lines, 0);
535
- const score = computeLintScore(acc.errors, acc.warnings, totalLines);
536
-
537
- return {
538
- score,
539
- findings: acc.findings.slice(0, 100),
540
- available: true,
541
- issueCount: acc.errors + acc.warnings,
542
- };
543
- }
10
+ export { detectEcosystem, detectTools, installTools } from './quality-tools.js';
11
+ // Re-export public API for backward compatibility
12
+ export type { CategoryScore, QualityFinding, QualityResults, QualityTool, ScanProgress } from './quality-types.js';
544
13
 
545
14
  // ============================================================================
546
15
  // Formatting Analysis
@@ -558,7 +27,6 @@ async function analyzeFormatting(
558
27
  if (ecosystems.includes('node')) {
559
28
  const result = await runCommand('npx', ['prettier', '--check', '.'], dirPath);
560
29
  ran = true;
561
- // prettier --check outputs filenames of unformatted files to stdout
562
30
  const unformatted = result.stdout.split('\n').filter((l) => l.trim() && !l.startsWith('Checking'));
563
31
  const nodeFiles = files.filter((f) => ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(extname(f.path)));
564
32
  totalFiles += nodeFiles.length;
@@ -625,225 +93,6 @@ function analyzeFileLength(files: SourceFile[]): { score: number; findings: Qual
625
93
  return { score: Math.min(100, score), findings: findings.slice(0, 50), issueCount: findings.length };
626
94
  }
627
95
 
628
- // ============================================================================
629
- // Function Length Analysis
630
- // ============================================================================
631
-
632
- interface FunctionInfo {
633
- name: string;
634
- file: string;
635
- startLine: number;
636
- lines: number;
637
- }
638
-
639
- // Match function declarations, arrow functions assigned to const/let, and methods
640
- const JS_FUNC_PATTERN = /^(\s*)(export\s+)?(async\s+)?function\s+(\w+)|^(\s*)(export\s+)?(const|let|var)\s+(\w+)\s*=\s*(async\s+)?\(|^(\s*)(public|private|protected)?\s*(async\s+)?(\w+)\s*\(/;
641
-
642
- function countBraceDeltas(line: string): number {
643
- let delta = 0;
644
- for (const ch of line) {
645
- if (ch === '{') delta++;
646
- else if (ch === '}') delta--;
647
- }
648
- return delta;
649
- }
650
-
651
- function matchJsFuncStart(line: string): { name: string; indent: number } | null {
652
- const match = JS_FUNC_PATTERN.exec(line);
653
- if (!match) return null;
654
- const name = match[4] || match[8] || match[13] || 'anonymous';
655
- const indent = (match[1] || match[5] || match[10] || '').length;
656
- return { name, indent };
657
- }
658
-
659
- function extractJsFunctions(file: SourceFile): FunctionInfo[] {
660
- const functions: FunctionInfo[] = [];
661
- const lines = file.content.split('\n');
662
- let braceDepth = 0;
663
- let currentFunc: { name: string; startLine: number; indent: number } | null = null;
664
- let funcStartBraceDepth = 0;
665
-
666
- for (let i = 0; i < lines.length; i++) {
667
- if (!currentFunc) {
668
- const funcStart = matchJsFuncStart(lines[i]);
669
- if (funcStart) {
670
- currentFunc = { name: funcStart.name, startLine: i + 1, indent: funcStart.indent };
671
- funcStartBraceDepth = braceDepth;
672
- }
673
- }
674
-
675
- braceDepth += countBraceDeltas(lines[i]);
676
-
677
- if (currentFunc && braceDepth <= funcStartBraceDepth && i > currentFunc.startLine - 1) {
678
- functions.push({
679
- name: currentFunc.name,
680
- file: file.relativePath,
681
- startLine: currentFunc.startLine,
682
- lines: i + 1 - currentFunc.startLine + 1,
683
- });
684
- currentFunc = null;
685
- }
686
- }
687
-
688
- return functions;
689
- }
690
-
691
- function extractPyFunctions(file: SourceFile): FunctionInfo[] {
692
- const functions: FunctionInfo[] = [];
693
- const lines = file.content.split('\n');
694
- const defPattern = /^(\s*)(async\s+)?def\s+(\w+)/;
695
- let currentFunc: { name: string; startLine: number; indent: number } | null = null;
696
-
697
- for (let i = 0; i < lines.length; i++) {
698
- const match = defPattern.exec(lines[i]);
699
- if (match) {
700
- if (currentFunc) {
701
- functions.push({
702
- name: currentFunc.name,
703
- file: file.relativePath,
704
- startLine: currentFunc.startLine,
705
- lines: i - currentFunc.startLine + 1,
706
- });
707
- }
708
- currentFunc = { name: match[3], startLine: i + 1, indent: match[1].length };
709
- } else if (currentFunc && lines[i].trim() && !lines[i].startsWith(' '.repeat(currentFunc.indent + 1)) && !lines[i].startsWith('\t')) {
710
- functions.push({
711
- name: currentFunc.name,
712
- file: file.relativePath,
713
- startLine: currentFunc.startLine,
714
- lines: i - currentFunc.startLine + 1,
715
- });
716
- currentFunc = null;
717
- }
718
- }
719
- if (currentFunc) {
720
- functions.push({
721
- name: currentFunc.name,
722
- file: file.relativePath,
723
- startLine: currentFunc.startLine,
724
- lines: lines.length - currentFunc.startLine + 1,
725
- });
726
- }
727
-
728
- return functions;
729
- }
730
-
731
- function extractFunctions(file: SourceFile): FunctionInfo[] {
732
- const ext = extname(file.path).toLowerCase();
733
- if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext)) return extractJsFunctions(file);
734
- if (['.py', '.pyi'].includes(ext)) return extractPyFunctions(file);
735
- return [];
736
- }
737
-
738
- function analyzeFunctionLength(files: SourceFile[]): { score: number; findings: QualityFinding[]; issueCount: number } {
739
- const allFunctions: FunctionInfo[] = [];
740
- for (const file of files) {
741
- allFunctions.push(...extractFunctions(file));
742
- }
743
-
744
- if (allFunctions.length === 0) return { score: 100, findings: [], issueCount: 0 };
745
-
746
- const findings: QualityFinding[] = [];
747
- let totalScore = 0;
748
-
749
- for (const func of allFunctions) {
750
- const ratio = Math.max(1, func.lines / FUNCTION_LENGTH_THRESHOLD);
751
- const funcScore = 100 / ratio ** 1.5;
752
- totalScore += funcScore;
753
-
754
- if (func.lines > FUNCTION_LENGTH_THRESHOLD) {
755
- findings.push({
756
- severity: func.lines > FUNCTION_LENGTH_THRESHOLD * 3 ? 'high' : func.lines > FUNCTION_LENGTH_THRESHOLD * 2 ? 'medium' : 'low',
757
- category: 'function-length',
758
- file: func.file,
759
- line: func.startLine,
760
- title: `${func.name}() has ${func.lines} lines (threshold: ${FUNCTION_LENGTH_THRESHOLD})`,
761
- description: `Function "${func.name}" exceeds the recommended length by ${func.lines - FUNCTION_LENGTH_THRESHOLD} lines.`,
762
- });
763
- }
764
- }
765
-
766
- const score = Math.round(totalScore / allFunctions.length);
767
- return { score: Math.min(100, score), findings: findings.slice(0, 50), issueCount: findings.length };
768
- }
769
-
770
- // ============================================================================
771
- // Cyclomatic Complexity (Heuristic)
772
- // ============================================================================
773
-
774
- function countCyclomaticComplexity(funcContent: string): number {
775
- let cc = 1; // base
776
- cc += (funcContent.match(/\bif\b/g) || []).length;
777
- cc += (funcContent.match(/\belse\s+if\b/g) || []).length;
778
- cc += (funcContent.match(/\bfor\b/g) || []).length;
779
- cc += (funcContent.match(/\bwhile\b/g) || []).length;
780
- cc += (funcContent.match(/\bcase\b/g) || []).length;
781
- cc += (funcContent.match(/\bcatch\b/g) || []).length;
782
- cc += (funcContent.match(/&&|\|\|/g) || []).length;
783
- cc += (funcContent.match(/\?\s*[^:]/g) || []).length; // ternary
784
- return cc;
785
- }
786
-
787
- function complexityToScore(cc: number): number {
788
- if (cc <= 5) return 100;
789
- if (cc <= 10) return 100 - (cc - 5) * 5;
790
- if (cc <= 15) return 75 - (cc - 10) * 5;
791
- if (cc <= 20) return 50 - (cc - 15) * 5;
792
- return Math.max(0, 25 - (cc - 20) * 2.5);
793
- }
794
-
795
- function getFuncContent(file: SourceFile, func: FunctionInfo): string {
796
- return file.content.split('\n').slice(func.startLine - 1, func.startLine - 1 + func.lines).join('\n');
797
- }
798
-
799
- function complexitySeverity(cc: number): QualityFinding['severity'] {
800
- if (cc > 20) return 'high';
801
- if (cc > 15) return 'medium';
802
- return 'low';
803
- }
804
-
805
- interface ComplexityAccumulator {
806
- weightedScore: number;
807
- weight: number;
808
- findings: QualityFinding[];
809
- }
810
-
811
- function analyzeFunc(file: SourceFile, func: FunctionInfo, acc: ComplexityAccumulator): void {
812
- const funcContent = getFuncContent(file, func);
813
- const cc = countCyclomaticComplexity(funcContent);
814
- const funcScore = complexityToScore(cc);
815
-
816
- acc.weightedScore += funcScore * func.lines;
817
- acc.weight += func.lines;
818
-
819
- if (cc > 10) {
820
- acc.findings.push({
821
- severity: complexitySeverity(cc),
822
- category: 'complexity',
823
- file: func.file,
824
- line: func.startLine,
825
- title: `${func.name}() has cyclomatic complexity ${cc}`,
826
- description: `Complexity of ${cc} exceeds the recommended threshold of 10. Consider refactoring into smaller functions.`,
827
- });
828
- }
829
- }
830
-
831
- function analyzeComplexity(files: SourceFile[]): { score: number; findings: QualityFinding[]; issueCount: number } {
832
- const acc: ComplexityAccumulator = { weightedScore: 0, weight: 0, findings: [] };
833
-
834
- for (const file of files) {
835
- const functions = extractFunctions(file);
836
- for (const func of functions) {
837
- analyzeFunc(file, func, acc);
838
- }
839
- }
840
-
841
- if (acc.weight === 0) return { score: 100, findings: [], issueCount: 0 };
842
-
843
- const score = Math.round(acc.weightedScore / acc.weight);
844
- return { score: Math.min(100, score), findings: acc.findings.slice(0, 50), issueCount: acc.findings.length };
845
- }
846
-
847
96
  // ============================================================================
848
97
  // Scoring
849
98
  // ============================================================================
@@ -956,9 +205,9 @@ export async function runQualityScan(
956
205
  ? await analyzeFormatting(dirPath, ecosystems, files)
957
206
  : { score: 0, available: false, issueCount: 0 };
958
207
 
959
- // Step 4: Analyze complexity
208
+ // Step 4: Analyze complexity (using real tools: Biome, ESLint, radon)
960
209
  progress('Analyzing complexity', 4);
961
- const complexityResult = analyzeComplexity(files);
210
+ const complexityResult = await analyzeComplexity(dirPath, ecosystems, installedToolNames);
962
211
 
963
212
  // Step 5: Check file lengths
964
213
  progress('Checking file lengths', 5);
@@ -993,7 +242,7 @@ export async function runQualityScan(
993
242
  score: complexityResult.score,
994
243
  weight: DEFAULT_WEIGHTS.complexity,
995
244
  effectiveWeight: DEFAULT_WEIGHTS.complexity,
996
- available: true,
245
+ available: complexityResult.available,
997
246
  issueCount: complexityResult.issueCount,
998
247
  },
999
248
  {