mstro-app 0.5.0 → 0.5.5

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 (649) hide show
  1. package/LICENSE +129 -190
  2. package/PRIVACY.md +11 -11
  3. package/README.md +75 -28
  4. package/bin/commands/config.js +1 -2
  5. package/bin/mstro.js +55 -5
  6. package/bin/postinstall.js +0 -1
  7. package/dist/server/cli/eta-estimator.d.ts +55 -0
  8. package/dist/server/cli/eta-estimator.d.ts.map +1 -0
  9. package/dist/server/cli/eta-estimator.js +222 -0
  10. package/dist/server/cli/eta-estimator.js.map +1 -0
  11. package/dist/server/cli/headless/claude-invoker-process.d.ts.map +1 -1
  12. package/dist/server/cli/headless/claude-invoker-process.js +0 -1
  13. package/dist/server/cli/headless/claude-invoker-process.js.map +1 -1
  14. package/dist/server/cli/headless/claude-invoker-stall.d.ts.map +1 -1
  15. package/dist/server/cli/headless/claude-invoker-stall.js +0 -1
  16. package/dist/server/cli/headless/claude-invoker-stall.js.map +1 -1
  17. package/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -1
  18. package/dist/server/cli/headless/claude-invoker-stream.js +0 -1
  19. package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -1
  20. package/dist/server/cli/headless/claude-invoker-tools.d.ts.map +1 -1
  21. package/dist/server/cli/headless/claude-invoker-tools.js +0 -1
  22. package/dist/server/cli/headless/claude-invoker-tools.js.map +1 -1
  23. package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
  24. package/dist/server/cli/headless/claude-invoker.js +0 -1
  25. package/dist/server/cli/headless/claude-invoker.js.map +1 -1
  26. package/dist/server/cli/headless/haiku-assessments.d.ts.map +1 -1
  27. package/dist/server/cli/headless/haiku-assessments.js +0 -1
  28. package/dist/server/cli/headless/haiku-assessments.js.map +1 -1
  29. package/dist/server/cli/headless/headless-logger.d.ts.map +1 -1
  30. package/dist/server/cli/headless/headless-logger.js +0 -1
  31. package/dist/server/cli/headless/headless-logger.js.map +1 -1
  32. package/dist/server/cli/headless/index.d.ts.map +1 -1
  33. package/dist/server/cli/headless/index.js +0 -1
  34. package/dist/server/cli/headless/index.js.map +1 -1
  35. package/dist/server/cli/headless/native-timeout-detector.d.ts.map +1 -1
  36. package/dist/server/cli/headless/native-timeout-detector.js +0 -1
  37. package/dist/server/cli/headless/native-timeout-detector.js.map +1 -1
  38. package/dist/server/cli/headless/output-utils.d.ts.map +1 -1
  39. package/dist/server/cli/headless/output-utils.js +0 -1
  40. package/dist/server/cli/headless/output-utils.js.map +1 -1
  41. package/dist/server/cli/headless/prompt-utils.d.ts.map +1 -1
  42. package/dist/server/cli/headless/prompt-utils.js +0 -1
  43. package/dist/server/cli/headless/prompt-utils.js.map +1 -1
  44. package/dist/server/cli/headless/resilient-runner.d.ts.map +1 -1
  45. package/dist/server/cli/headless/resilient-runner.js +0 -1
  46. package/dist/server/cli/headless/resilient-runner.js.map +1 -1
  47. package/dist/server/cli/headless/retry-strategies.d.ts.map +1 -1
  48. package/dist/server/cli/headless/retry-strategies.js +0 -1
  49. package/dist/server/cli/headless/retry-strategies.js.map +1 -1
  50. package/dist/server/cli/headless/runner.d.ts.map +1 -1
  51. package/dist/server/cli/headless/runner.js +0 -1
  52. package/dist/server/cli/headless/runner.js.map +1 -1
  53. package/dist/server/cli/headless/stall-assessor.d.ts +50 -0
  54. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  55. package/dist/server/cli/headless/stall-assessor.js +64 -10
  56. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  57. package/dist/server/cli/headless/tool-watchdog.d.ts +21 -0
  58. package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
  59. package/dist/server/cli/headless/tool-watchdog.js +19 -13
  60. package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
  61. package/dist/server/cli/headless/types.d.ts.map +1 -1
  62. package/dist/server/cli/headless/types.js +0 -1
  63. package/dist/server/cli/headless/types.js.map +1 -1
  64. package/dist/server/cli/improvisation-attachments.d.ts.map +1 -1
  65. package/dist/server/cli/improvisation-attachments.js +0 -1
  66. package/dist/server/cli/improvisation-attachments.js.map +1 -1
  67. package/dist/server/cli/improvisation-history-store.d.ts.map +1 -1
  68. package/dist/server/cli/improvisation-history-store.js +5 -2
  69. package/dist/server/cli/improvisation-history-store.js.map +1 -1
  70. package/dist/server/cli/improvisation-movements.d.ts.map +1 -1
  71. package/dist/server/cli/improvisation-movements.js +0 -1
  72. package/dist/server/cli/improvisation-movements.js.map +1 -1
  73. package/dist/server/cli/improvisation-output-queue.d.ts +5 -1
  74. package/dist/server/cli/improvisation-output-queue.d.ts.map +1 -1
  75. package/dist/server/cli/improvisation-output-queue.js +30 -8
  76. package/dist/server/cli/improvisation-output-queue.js.map +1 -1
  77. package/dist/server/cli/improvisation-retry.d.ts.map +1 -1
  78. package/dist/server/cli/improvisation-retry.js +0 -1
  79. package/dist/server/cli/improvisation-retry.js.map +1 -1
  80. package/dist/server/cli/improvisation-session-manager.d.ts +29 -0
  81. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  82. package/dist/server/cli/improvisation-session-manager.js +50 -2
  83. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  84. package/dist/server/cli/improvisation-types.d.ts +2 -0
  85. package/dist/server/cli/improvisation-types.d.ts.map +1 -1
  86. package/dist/server/cli/improvisation-types.js +0 -1
  87. package/dist/server/cli/improvisation-types.js.map +1 -1
  88. package/dist/server/cli/retry/retry-best-result.d.ts.map +1 -1
  89. package/dist/server/cli/retry/retry-best-result.js +0 -1
  90. package/dist/server/cli/retry/retry-best-result.js.map +1 -1
  91. package/dist/server/cli/retry/retry-context-loss.d.ts.map +1 -1
  92. package/dist/server/cli/retry/retry-context-loss.js +0 -1
  93. package/dist/server/cli/retry/retry-context-loss.js.map +1 -1
  94. package/dist/server/cli/retry/retry-premature-completion.d.ts.map +1 -1
  95. package/dist/server/cli/retry/retry-premature-completion.js +1 -2
  96. package/dist/server/cli/retry/retry-premature-completion.js.map +1 -1
  97. package/dist/server/cli/retry/retry-recovery-strategies.d.ts.map +1 -1
  98. package/dist/server/cli/retry/retry-recovery-strategies.js +0 -1
  99. package/dist/server/cli/retry/retry-recovery-strategies.js.map +1 -1
  100. package/dist/server/cli/retry/retry-resume-strategy.d.ts.map +1 -1
  101. package/dist/server/cli/retry/retry-resume-strategy.js +0 -1
  102. package/dist/server/cli/retry/retry-resume-strategy.js.map +1 -1
  103. package/dist/server/cli/retry/retry-runner-factory.d.ts.map +1 -1
  104. package/dist/server/cli/retry/retry-runner-factory.js +0 -1
  105. package/dist/server/cli/retry/retry-runner-factory.js.map +1 -1
  106. package/dist/server/cli/retry/retry-tool-results.d.ts.map +1 -1
  107. package/dist/server/cli/retry/retry-tool-results.js +0 -1
  108. package/dist/server/cli/retry/retry-tool-results.js.map +1 -1
  109. package/dist/server/cli/retry/retry-types.d.ts.map +1 -1
  110. package/dist/server/cli/retry/retry-types.js +0 -1
  111. package/dist/server/cli/retry/retry-types.js.map +1 -1
  112. package/dist/server/engines/EngineEvent.d.ts +126 -0
  113. package/dist/server/engines/EngineEvent.d.ts.map +1 -0
  114. package/dist/server/engines/EngineEvent.js +11 -0
  115. package/dist/server/engines/EngineEvent.js.map +1 -0
  116. package/dist/server/engines/claude/ClaudeCodeEngine.d.ts +47 -0
  117. package/dist/server/engines/claude/ClaudeCodeEngine.d.ts.map +1 -0
  118. package/dist/server/engines/claude/ClaudeCodeEngine.js +338 -0
  119. package/dist/server/engines/claude/ClaudeCodeEngine.js.map +1 -0
  120. package/dist/server/engines/factory.d.ts +21 -0
  121. package/dist/server/engines/factory.d.ts.map +1 -0
  122. package/dist/server/engines/factory.js +152 -0
  123. package/dist/server/engines/factory.js.map +1 -0
  124. package/dist/server/engines/opencode/OpenCodeEngine.d.ts +148 -0
  125. package/dist/server/engines/opencode/OpenCodeEngine.d.ts.map +1 -0
  126. package/dist/server/engines/opencode/OpenCodeEngine.js +630 -0
  127. package/dist/server/engines/opencode/OpenCodeEngine.js.map +1 -0
  128. package/dist/server/engines/opencode/OpenCodeServerManager.d.ts +172 -0
  129. package/dist/server/engines/opencode/OpenCodeServerManager.d.ts.map +1 -0
  130. package/dist/server/engines/opencode/OpenCodeServerManager.js +390 -0
  131. package/dist/server/engines/opencode/OpenCodeServerManager.js.map +1 -0
  132. package/dist/server/engines/opencode/model-catalog.d.ts +94 -0
  133. package/dist/server/engines/opencode/model-catalog.d.ts.map +1 -0
  134. package/dist/server/engines/opencode/model-catalog.js +141 -0
  135. package/dist/server/engines/opencode/model-catalog.js.map +1 -0
  136. package/dist/server/engines/types.d.ts +146 -0
  137. package/dist/server/engines/types.d.ts.map +1 -0
  138. package/dist/server/engines/types.js +4 -0
  139. package/dist/server/engines/types.js.map +1 -0
  140. package/dist/server/index.js +1 -2
  141. package/dist/server/index.js.map +1 -1
  142. package/dist/server/mcp/bouncer-cli.js +0 -1
  143. package/dist/server/mcp/bouncer-cli.js.map +1 -1
  144. package/dist/server/mcp/bouncer-haiku.d.ts +17 -4
  145. package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -1
  146. package/dist/server/mcp/bouncer-haiku.js +8 -125
  147. package/dist/server/mcp/bouncer-haiku.js.map +1 -1
  148. package/dist/server/mcp/bouncer-integration.d.ts +45 -0
  149. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
  150. package/dist/server/mcp/bouncer-integration.js +69 -6
  151. package/dist/server/mcp/bouncer-integration.js.map +1 -1
  152. package/dist/server/mcp/classifier/BouncerClassifier.d.ts +34 -0
  153. package/dist/server/mcp/classifier/BouncerClassifier.d.ts.map +1 -0
  154. package/dist/server/mcp/classifier/BouncerClassifier.js +4 -0
  155. package/dist/server/mcp/classifier/BouncerClassifier.js.map +1 -0
  156. package/dist/server/mcp/classifier/ClaudeBouncerClassifier.d.ts +17 -0
  157. package/dist/server/mcp/classifier/ClaudeBouncerClassifier.d.ts.map +1 -0
  158. package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js +142 -0
  159. package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js.map +1 -0
  160. package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.d.ts +68 -0
  161. package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.d.ts.map +1 -0
  162. package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.js +182 -0
  163. package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.js.map +1 -0
  164. package/dist/server/mcp/classifier/factory.d.ts +70 -0
  165. package/dist/server/mcp/classifier/factory.d.ts.map +1 -0
  166. package/dist/server/mcp/classifier/factory.js +155 -0
  167. package/dist/server/mcp/classifier/factory.js.map +1 -0
  168. package/dist/server/mcp/security-analysis.d.ts.map +1 -1
  169. package/dist/server/mcp/security-analysis.js +0 -1
  170. package/dist/server/mcp/security-analysis.js.map +1 -1
  171. package/dist/server/mcp/security-audit.d.ts.map +1 -1
  172. package/dist/server/mcp/security-audit.js +0 -1
  173. package/dist/server/mcp/security-audit.js.map +1 -1
  174. package/dist/server/mcp/security-patterns.d.ts.map +1 -1
  175. package/dist/server/mcp/security-patterns.js +0 -1
  176. package/dist/server/mcp/security-patterns.js.map +1 -1
  177. package/dist/server/mcp/server.js +0 -1
  178. package/dist/server/mcp/server.js.map +1 -1
  179. package/dist/server/routes/files.d.ts.map +1 -1
  180. package/dist/server/routes/files.js +0 -1
  181. package/dist/server/routes/files.js.map +1 -1
  182. package/dist/server/routes/improvise.d.ts.map +1 -1
  183. package/dist/server/routes/improvise.js +0 -1
  184. package/dist/server/routes/improvise.js.map +1 -1
  185. package/dist/server/routes/index.d.ts.map +1 -1
  186. package/dist/server/routes/index.js +0 -1
  187. package/dist/server/routes/index.js.map +1 -1
  188. package/dist/server/routes/instances.d.ts.map +1 -1
  189. package/dist/server/routes/instances.js +0 -1
  190. package/dist/server/routes/instances.js.map +1 -1
  191. package/dist/server/routes/notifications.d.ts.map +1 -1
  192. package/dist/server/routes/notifications.js +0 -1
  193. package/dist/server/routes/notifications.js.map +1 -1
  194. package/dist/server/server-setup.d.ts.map +1 -1
  195. package/dist/server/server-setup.js +0 -1
  196. package/dist/server/server-setup.js.map +1 -1
  197. package/dist/server/services/analytics.d.ts.map +1 -1
  198. package/dist/server/services/analytics.js +0 -1
  199. package/dist/server/services/analytics.js.map +1 -1
  200. package/dist/server/services/auth.d.ts.map +1 -1
  201. package/dist/server/services/auth.js +0 -1
  202. package/dist/server/services/auth.js.map +1 -1
  203. package/dist/server/services/client-id.d.ts.map +1 -1
  204. package/dist/server/services/client-id.js +0 -1
  205. package/dist/server/services/client-id.js.map +1 -1
  206. package/dist/server/services/file-explorer-ops.d.ts.map +1 -1
  207. package/dist/server/services/file-explorer-ops.js +0 -1
  208. package/dist/server/services/file-explorer-ops.js.map +1 -1
  209. package/dist/server/services/files.d.ts.map +1 -1
  210. package/dist/server/services/files.js +0 -1
  211. package/dist/server/services/files.js.map +1 -1
  212. package/dist/server/services/instances.d.ts.map +1 -1
  213. package/dist/server/services/instances.js +0 -1
  214. package/dist/server/services/instances.js.map +1 -1
  215. package/dist/server/services/pathUtils.d.ts.map +1 -1
  216. package/dist/server/services/pathUtils.js +0 -1
  217. package/dist/server/services/pathUtils.js.map +1 -1
  218. package/dist/server/services/plan/agent-loader.d.ts.map +1 -1
  219. package/dist/server/services/plan/agent-loader.js +0 -1
  220. package/dist/server/services/plan/agent-loader.js.map +1 -1
  221. package/dist/server/services/plan/agent-resolver.d.ts +26 -0
  222. package/dist/server/services/plan/agent-resolver.d.ts.map +1 -0
  223. package/dist/server/services/plan/agent-resolver.js +102 -0
  224. package/dist/server/services/plan/agent-resolver.js.map +1 -0
  225. package/dist/server/services/plan/board-config.d.ts.map +1 -1
  226. package/dist/server/services/plan/board-config.js +0 -1
  227. package/dist/server/services/plan/board-config.js.map +1 -1
  228. package/dist/server/services/plan/composer.d.ts.map +1 -1
  229. package/dist/server/services/plan/composer.js +59 -12
  230. package/dist/server/services/plan/composer.js.map +1 -1
  231. package/dist/server/services/plan/config-installer.d.ts.map +1 -1
  232. package/dist/server/services/plan/config-installer.js +0 -1
  233. package/dist/server/services/plan/config-installer.js.map +1 -1
  234. package/dist/server/services/plan/dependency-resolver.d.ts.map +1 -1
  235. package/dist/server/services/plan/dependency-resolver.js +0 -1
  236. package/dist/server/services/plan/dependency-resolver.js.map +1 -1
  237. package/dist/server/services/plan/executor.d.ts.map +1 -1
  238. package/dist/server/services/plan/executor.js +48 -4
  239. package/dist/server/services/plan/executor.js.map +1 -1
  240. package/dist/server/services/plan/front-matter.d.ts.map +1 -1
  241. package/dist/server/services/plan/front-matter.js +0 -1
  242. package/dist/server/services/plan/front-matter.js.map +1 -1
  243. package/dist/server/services/plan/issue-classification.d.ts.map +1 -1
  244. package/dist/server/services/plan/issue-classification.js +0 -1
  245. package/dist/server/services/plan/issue-classification.js.map +1 -1
  246. package/dist/server/services/plan/issue-loader.d.ts.map +1 -1
  247. package/dist/server/services/plan/issue-loader.js +0 -1
  248. package/dist/server/services/plan/issue-loader.js.map +1 -1
  249. package/dist/server/services/plan/issue-prompt-builder.d.ts.map +1 -1
  250. package/dist/server/services/plan/issue-prompt-builder.js +33 -2
  251. package/dist/server/services/plan/issue-prompt-builder.js.map +1 -1
  252. package/dist/server/services/plan/issue-retry.d.ts +3 -1
  253. package/dist/server/services/plan/issue-retry.d.ts.map +1 -1
  254. package/dist/server/services/plan/issue-retry.js +2 -1
  255. package/dist/server/services/plan/issue-retry.js.map +1 -1
  256. package/dist/server/services/plan/issue-writer.d.ts.map +1 -1
  257. package/dist/server/services/plan/issue-writer.js +0 -1
  258. package/dist/server/services/plan/issue-writer.js.map +1 -1
  259. package/dist/server/services/plan/output-manager.d.ts.map +1 -1
  260. package/dist/server/services/plan/output-manager.js +0 -1
  261. package/dist/server/services/plan/output-manager.js.map +1 -1
  262. package/dist/server/services/plan/parser-core.d.ts.map +1 -1
  263. package/dist/server/services/plan/parser-core.js +1 -1
  264. package/dist/server/services/plan/parser-core.js.map +1 -1
  265. package/dist/server/services/plan/parser-migration.d.ts.map +1 -1
  266. package/dist/server/services/plan/parser-migration.js +0 -1
  267. package/dist/server/services/plan/parser-migration.js.map +1 -1
  268. package/dist/server/services/plan/parser.d.ts.map +1 -1
  269. package/dist/server/services/plan/parser.js +0 -1
  270. package/dist/server/services/plan/parser.js.map +1 -1
  271. package/dist/server/services/plan/progress-log.d.ts.map +1 -1
  272. package/dist/server/services/plan/progress-log.js +0 -1
  273. package/dist/server/services/plan/progress-log.js.map +1 -1
  274. package/dist/server/services/plan/prompt-builder.d.ts.map +1 -1
  275. package/dist/server/services/plan/prompt-builder.js +0 -1
  276. package/dist/server/services/plan/prompt-builder.js.map +1 -1
  277. package/dist/server/services/plan/readiness-planner.d.ts.map +1 -1
  278. package/dist/server/services/plan/readiness-planner.js +0 -1
  279. package/dist/server/services/plan/readiness-planner.js.map +1 -1
  280. package/dist/server/services/plan/review-gate.d.ts.map +1 -1
  281. package/dist/server/services/plan/review-gate.js +0 -1
  282. package/dist/server/services/plan/review-gate.js.map +1 -1
  283. package/dist/server/services/plan/state-reconciler.d.ts.map +1 -1
  284. package/dist/server/services/plan/state-reconciler.js +0 -1
  285. package/dist/server/services/plan/state-reconciler.js.map +1 -1
  286. package/dist/server/services/plan/types.d.ts +1 -0
  287. package/dist/server/services/plan/types.d.ts.map +1 -1
  288. package/dist/server/services/plan/types.js +0 -1
  289. package/dist/server/services/plan/types.js.map +1 -1
  290. package/dist/server/services/plan/watcher.d.ts.map +1 -1
  291. package/dist/server/services/plan/watcher.js +0 -1
  292. package/dist/server/services/plan/watcher.js.map +1 -1
  293. package/dist/server/services/platform-credentials.d.ts.map +1 -1
  294. package/dist/server/services/platform-credentials.js +0 -1
  295. package/dist/server/services/platform-credentials.js.map +1 -1
  296. package/dist/server/services/platform-token-lifecycle.d.ts +70 -0
  297. package/dist/server/services/platform-token-lifecycle.d.ts.map +1 -0
  298. package/dist/server/services/platform-token-lifecycle.js +156 -0
  299. package/dist/server/services/platform-token-lifecycle.js.map +1 -0
  300. package/dist/server/services/platform.d.ts +21 -56
  301. package/dist/server/services/platform.d.ts.map +1 -1
  302. package/dist/server/services/platform.js +98 -142
  303. package/dist/server/services/platform.js.map +1 -1
  304. package/dist/server/services/sentry.d.ts.map +1 -1
  305. package/dist/server/services/sentry.js +0 -1
  306. package/dist/server/services/sentry.js.map +1 -1
  307. package/dist/server/services/settings.d.ts +76 -2
  308. package/dist/server/services/settings.d.ts.map +1 -1
  309. package/dist/server/services/settings.js +127 -5
  310. package/dist/server/services/settings.js.map +1 -1
  311. package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
  312. package/dist/server/services/terminal/pty-manager.js +0 -1
  313. package/dist/server/services/terminal/pty-manager.js.map +1 -1
  314. package/dist/server/services/terminal/pty-utils.d.ts.map +1 -1
  315. package/dist/server/services/terminal/pty-utils.js +0 -1
  316. package/dist/server/services/terminal/pty-utils.js.map +1 -1
  317. package/dist/server/services/websocket/autocomplete.d.ts.map +1 -1
  318. package/dist/server/services/websocket/autocomplete.js +0 -1
  319. package/dist/server/services/websocket/autocomplete.js.map +1 -1
  320. package/dist/server/services/websocket/file-definition-handlers.d.ts.map +1 -1
  321. package/dist/server/services/websocket/file-definition-handlers.js +0 -1
  322. package/dist/server/services/websocket/file-definition-handlers.js.map +1 -1
  323. package/dist/server/services/websocket/file-download-handler.d.ts.map +1 -1
  324. package/dist/server/services/websocket/file-download-handler.js +0 -1
  325. package/dist/server/services/websocket/file-download-handler.js.map +1 -1
  326. package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -1
  327. package/dist/server/services/websocket/file-explorer-handlers.js +0 -1
  328. package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
  329. package/dist/server/services/websocket/file-search-handlers.d.ts.map +1 -1
  330. package/dist/server/services/websocket/file-search-handlers.js +0 -1
  331. package/dist/server/services/websocket/file-search-handlers.js.map +1 -1
  332. package/dist/server/services/websocket/file-upload-handler.d.ts +2 -3
  333. package/dist/server/services/websocket/file-upload-handler.d.ts.map +1 -1
  334. package/dist/server/services/websocket/file-upload-handler.js +4 -7
  335. package/dist/server/services/websocket/file-upload-handler.js.map +1 -1
  336. package/dist/server/services/websocket/file-utils.d.ts.map +1 -1
  337. package/dist/server/services/websocket/file-utils.js +0 -1
  338. package/dist/server/services/websocket/file-utils.js.map +1 -1
  339. package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -1
  340. package/dist/server/services/websocket/git-branch-handlers.js +19 -7
  341. package/dist/server/services/websocket/git-branch-handlers.js.map +1 -1
  342. package/dist/server/services/websocket/git-diff-handlers.d.ts.map +1 -1
  343. package/dist/server/services/websocket/git-diff-handlers.js +0 -1
  344. package/dist/server/services/websocket/git-diff-handlers.js.map +1 -1
  345. package/dist/server/services/websocket/git-handlers.d.ts.map +1 -1
  346. package/dist/server/services/websocket/git-handlers.js +58 -6
  347. package/dist/server/services/websocket/git-handlers.js.map +1 -1
  348. package/dist/server/services/websocket/git-head-watcher.d.ts.map +1 -1
  349. package/dist/server/services/websocket/git-head-watcher.js +0 -1
  350. package/dist/server/services/websocket/git-head-watcher.js.map +1 -1
  351. package/dist/server/services/websocket/git-log-handlers.d.ts.map +1 -1
  352. package/dist/server/services/websocket/git-log-handlers.js +0 -1
  353. package/dist/server/services/websocket/git-log-handlers.js.map +1 -1
  354. package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -1
  355. package/dist/server/services/websocket/git-pr-handlers.js +0 -1
  356. package/dist/server/services/websocket/git-pr-handlers.js.map +1 -1
  357. package/dist/server/services/websocket/git-tag-handlers.d.ts.map +1 -1
  358. package/dist/server/services/websocket/git-tag-handlers.js +0 -1
  359. package/dist/server/services/websocket/git-tag-handlers.js.map +1 -1
  360. package/dist/server/services/websocket/git-utils.d.ts +18 -3
  361. package/dist/server/services/websocket/git-utils.d.ts.map +1 -1
  362. package/dist/server/services/websocket/git-utils.js +58 -8
  363. package/dist/server/services/websocket/git-utils.js.map +1 -1
  364. package/dist/server/services/websocket/git-worktree-handlers.d.ts.map +1 -1
  365. package/dist/server/services/websocket/git-worktree-handlers.js +230 -14
  366. package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -1
  367. package/dist/server/services/websocket/handler-context.d.ts.map +1 -1
  368. package/dist/server/services/websocket/handler-context.js +0 -1
  369. package/dist/server/services/websocket/handler-context.js.map +1 -1
  370. package/dist/server/services/websocket/handler.d.ts +17 -1
  371. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  372. package/dist/server/services/websocket/handler.js +57 -6
  373. package/dist/server/services/websocket/handler.js.map +1 -1
  374. package/dist/server/services/websocket/index.d.ts.map +1 -1
  375. package/dist/server/services/websocket/index.js +0 -1
  376. package/dist/server/services/websocket/index.js.map +1 -1
  377. package/dist/server/services/websocket/msg-id-tracker.d.ts.map +1 -1
  378. package/dist/server/services/websocket/msg-id-tracker.js +0 -1
  379. package/dist/server/services/websocket/msg-id-tracker.js.map +1 -1
  380. package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -1
  381. package/dist/server/services/websocket/plan-board-handlers.js +0 -1
  382. package/dist/server/services/websocket/plan-board-handlers.js.map +1 -1
  383. package/dist/server/services/websocket/plan-execution-handlers.d.ts.map +1 -1
  384. package/dist/server/services/websocket/plan-execution-handlers.js +6 -2
  385. package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -1
  386. package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -1
  387. package/dist/server/services/websocket/plan-handlers.js +0 -1
  388. package/dist/server/services/websocket/plan-handlers.js.map +1 -1
  389. package/dist/server/services/websocket/plan-helpers.d.ts.map +1 -1
  390. package/dist/server/services/websocket/plan-helpers.js +0 -1
  391. package/dist/server/services/websocket/plan-helpers.js.map +1 -1
  392. package/dist/server/services/websocket/plan-issue-handlers.d.ts.map +1 -1
  393. package/dist/server/services/websocket/plan-issue-handlers.js +0 -1
  394. package/dist/server/services/websocket/plan-issue-handlers.js.map +1 -1
  395. package/dist/server/services/websocket/plan-sprint-handlers.d.ts.map +1 -1
  396. package/dist/server/services/websocket/plan-sprint-handlers.js +0 -1
  397. package/dist/server/services/websocket/plan-sprint-handlers.js.map +1 -1
  398. package/dist/server/services/websocket/quality-complexity.d.ts.map +1 -1
  399. package/dist/server/services/websocket/quality-complexity.js +78 -27
  400. package/dist/server/services/websocket/quality-complexity.js.map +1 -1
  401. package/dist/server/services/websocket/quality-eta.d.ts +47 -0
  402. package/dist/server/services/websocket/quality-eta.d.ts.map +1 -0
  403. package/dist/server/services/websocket/quality-eta.js +110 -0
  404. package/dist/server/services/websocket/quality-eta.js.map +1 -0
  405. package/dist/server/services/websocket/quality-grading.d.ts +69 -0
  406. package/dist/server/services/websocket/quality-grading.d.ts.map +1 -0
  407. package/dist/server/services/websocket/quality-grading.js +650 -0
  408. package/dist/server/services/websocket/quality-grading.js.map +1 -0
  409. package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
  410. package/dist/server/services/websocket/quality-handlers.js +145 -8
  411. package/dist/server/services/websocket/quality-handlers.js.map +1 -1
  412. package/dist/server/services/websocket/quality-linting.d.ts.map +1 -1
  413. package/dist/server/services/websocket/quality-linting.js +0 -1
  414. package/dist/server/services/websocket/quality-linting.js.map +1 -1
  415. package/dist/server/services/websocket/quality-operations.d.ts +34 -0
  416. package/dist/server/services/websocket/quality-operations.d.ts.map +1 -0
  417. package/dist/server/services/websocket/quality-operations.js +47 -0
  418. package/dist/server/services/websocket/quality-operations.js.map +1 -0
  419. package/dist/server/services/websocket/quality-persistence.d.ts +23 -0
  420. package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
  421. package/dist/server/services/websocket/quality-persistence.js +38 -12
  422. package/dist/server/services/websocket/quality-persistence.js.map +1 -1
  423. package/dist/server/services/websocket/quality-review-agent.d.ts +1 -1
  424. package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
  425. package/dist/server/services/websocket/quality-review-agent.js +105 -57
  426. package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
  427. package/dist/server/services/websocket/quality-service.d.ts +12 -2
  428. package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
  429. package/dist/server/services/websocket/quality-service.js +387 -72
  430. package/dist/server/services/websocket/quality-service.js.map +1 -1
  431. package/dist/server/services/websocket/quality-tools.d.ts +22 -1
  432. package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
  433. package/dist/server/services/websocket/quality-tools.js +55 -3
  434. package/dist/server/services/websocket/quality-tools.js.map +1 -1
  435. package/dist/server/services/websocket/quality-types.d.ts +52 -3
  436. package/dist/server/services/websocket/quality-types.d.ts.map +1 -1
  437. package/dist/server/services/websocket/quality-types.js +1 -2
  438. package/dist/server/services/websocket/quality-types.js.map +1 -1
  439. package/dist/server/services/websocket/session-handlers.d.ts +3 -1
  440. package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
  441. package/dist/server/services/websocket/session-handlers.js +57 -10
  442. package/dist/server/services/websocket/session-handlers.js.map +1 -1
  443. package/dist/server/services/websocket/session-history.d.ts.map +1 -1
  444. package/dist/server/services/websocket/session-history.js +3 -1
  445. package/dist/server/services/websocket/session-history.js.map +1 -1
  446. package/dist/server/services/websocket/session-initialization.d.ts.map +1 -1
  447. package/dist/server/services/websocket/session-initialization.js +158 -43
  448. package/dist/server/services/websocket/session-initialization.js.map +1 -1
  449. package/dist/server/services/websocket/session-registry.d.ts +25 -0
  450. package/dist/server/services/websocket/session-registry.d.ts.map +1 -1
  451. package/dist/server/services/websocket/session-registry.js +19 -1
  452. package/dist/server/services/websocket/session-registry.js.map +1 -1
  453. package/dist/server/services/websocket/settings-handlers.d.ts +1 -1
  454. package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -1
  455. package/dist/server/services/websocket/settings-handlers.js +35 -5
  456. package/dist/server/services/websocket/settings-handlers.js.map +1 -1
  457. package/dist/server/services/websocket/skill-handlers.d.ts.map +1 -1
  458. package/dist/server/services/websocket/skill-handlers.js +0 -1
  459. package/dist/server/services/websocket/skill-handlers.js.map +1 -1
  460. package/dist/server/services/websocket/skill-watcher.d.ts.map +1 -1
  461. package/dist/server/services/websocket/skill-watcher.js +0 -1
  462. package/dist/server/services/websocket/skill-watcher.js.map +1 -1
  463. package/dist/server/services/websocket/tab-broadcast.d.ts +7 -2
  464. package/dist/server/services/websocket/tab-broadcast.d.ts.map +1 -1
  465. package/dist/server/services/websocket/tab-broadcast.js +10 -3
  466. package/dist/server/services/websocket/tab-broadcast.js.map +1 -1
  467. package/dist/server/services/websocket/tab-event-buffer.d.ts +97 -8
  468. package/dist/server/services/websocket/tab-event-buffer.d.ts.map +1 -1
  469. package/dist/server/services/websocket/tab-event-buffer.js +138 -13
  470. package/dist/server/services/websocket/tab-event-buffer.js.map +1 -1
  471. package/dist/server/services/websocket/tab-event-replay.d.ts +29 -13
  472. package/dist/server/services/websocket/tab-event-replay.d.ts.map +1 -1
  473. package/dist/server/services/websocket/tab-event-replay.js +55 -3
  474. package/dist/server/services/websocket/tab-event-replay.js.map +1 -1
  475. package/dist/server/services/websocket/tab-handlers.d.ts +9 -1
  476. package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -1
  477. package/dist/server/services/websocket/tab-handlers.js +47 -3
  478. package/dist/server/services/websocket/tab-handlers.js.map +1 -1
  479. package/dist/server/services/websocket/terminal-handlers.d.ts.map +1 -1
  480. package/dist/server/services/websocket/terminal-handlers.js +39 -4
  481. package/dist/server/services/websocket/terminal-handlers.js.map +1 -1
  482. package/dist/server/services/websocket/types.d.ts +30 -7
  483. package/dist/server/services/websocket/types.d.ts.map +1 -1
  484. package/dist/server/services/websocket/types.js +12 -7
  485. package/dist/server/services/websocket/types.js.map +1 -1
  486. package/dist/server/utils/agent-manager.d.ts.map +1 -1
  487. package/dist/server/utils/agent-manager.js +0 -1
  488. package/dist/server/utils/agent-manager.js.map +1 -1
  489. package/dist/server/utils/paths.d.ts.map +1 -1
  490. package/dist/server/utils/paths.js +0 -1
  491. package/dist/server/utils/paths.js.map +1 -1
  492. package/dist/server/utils/port-manager.d.ts.map +1 -1
  493. package/dist/server/utils/port-manager.js +0 -1
  494. package/dist/server/utils/port-manager.js.map +1 -1
  495. package/dist/server/utils/port.d.ts.map +1 -1
  496. package/dist/server/utils/port.js +0 -1
  497. package/dist/server/utils/port.js.map +1 -1
  498. package/package.json +6 -4
  499. package/server/cli/eta-estimator.ts +249 -0
  500. package/server/cli/headless/claude-invoker-process.ts +0 -1
  501. package/server/cli/headless/claude-invoker-stall.ts +0 -1
  502. package/server/cli/headless/claude-invoker-stream.ts +0 -1
  503. package/server/cli/headless/claude-invoker-tools.ts +0 -1
  504. package/server/cli/headless/claude-invoker.ts +0 -1
  505. package/server/cli/headless/haiku-assessments.ts +0 -1
  506. package/server/cli/headless/headless-logger.ts +0 -1
  507. package/server/cli/headless/index.ts +0 -1
  508. package/server/cli/headless/native-timeout-detector.ts +0 -1
  509. package/server/cli/headless/output-utils.ts +0 -1
  510. package/server/cli/headless/prompt-utils.ts +0 -1
  511. package/server/cli/headless/resilient-runner.ts +0 -1
  512. package/server/cli/headless/retry-strategies.ts +0 -1
  513. package/server/cli/headless/runner.ts +0 -1
  514. package/server/cli/headless/stall-assessor.ts +93 -1
  515. package/server/cli/headless/tool-watchdog.ts +21 -1
  516. package/server/cli/headless/types.ts +0 -1
  517. package/server/cli/improvisation-attachments.ts +0 -1
  518. package/server/cli/improvisation-history-store.ts +4 -2
  519. package/server/cli/improvisation-movements.ts +0 -1
  520. package/server/cli/improvisation-output-queue.ts +29 -8
  521. package/server/cli/improvisation-retry.ts +0 -1
  522. package/server/cli/improvisation-session-manager.ts +54 -2
  523. package/server/cli/improvisation-types.ts +2 -1
  524. package/server/cli/retry/retry-best-result.ts +0 -1
  525. package/server/cli/retry/retry-context-loss.ts +0 -1
  526. package/server/cli/retry/retry-premature-completion.ts +1 -2
  527. package/server/cli/retry/retry-recovery-strategies.ts +0 -1
  528. package/server/cli/retry/retry-resume-strategy.ts +0 -1
  529. package/server/cli/retry/retry-runner-factory.ts +0 -1
  530. package/server/cli/retry/retry-tool-results.ts +0 -1
  531. package/server/cli/retry/retry-types.ts +0 -1
  532. package/server/engines/EngineEvent.ts +156 -0
  533. package/server/engines/claude/ClaudeCodeEngine.ts +404 -0
  534. package/server/engines/factory.ts +176 -0
  535. package/server/engines/opencode/OpenCodeEngine.ts +786 -0
  536. package/server/engines/opencode/OpenCodeServerManager.ts +577 -0
  537. package/server/engines/opencode/model-catalog.ts +217 -0
  538. package/server/engines/types.ts +173 -0
  539. package/server/index.ts +1 -2
  540. package/server/mcp/bouncer-cli.ts +0 -1
  541. package/server/mcp/bouncer-haiku.ts +21 -146
  542. package/server/mcp/bouncer-integration.ts +107 -6
  543. package/server/mcp/classifier/BouncerClassifier.ts +40 -0
  544. package/server/mcp/classifier/ClaudeBouncerClassifier.ts +189 -0
  545. package/server/mcp/classifier/OpenCodeBouncerClassifier.ts +305 -0
  546. package/server/mcp/classifier/factory.ts +195 -0
  547. package/server/mcp/security-analysis.ts +0 -1
  548. package/server/mcp/security-audit.ts +0 -1
  549. package/server/mcp/security-patterns.ts +0 -1
  550. package/server/mcp/server.ts +0 -1
  551. package/server/routes/files.ts +0 -1
  552. package/server/routes/improvise.ts +0 -1
  553. package/server/routes/index.ts +0 -1
  554. package/server/routes/instances.ts +0 -1
  555. package/server/routes/notifications.ts +0 -1
  556. package/server/server-setup.ts +0 -1
  557. package/server/services/analytics.ts +0 -1
  558. package/server/services/auth.ts +0 -1
  559. package/server/services/client-id.ts +0 -1
  560. package/server/services/file-explorer-ops.ts +0 -1
  561. package/server/services/files.ts +0 -1
  562. package/server/services/instances.ts +0 -1
  563. package/server/services/pathUtils.ts +0 -1
  564. package/server/services/plan/agent-loader.ts +0 -1
  565. package/server/services/plan/agent-resolver.ts +115 -0
  566. package/server/services/plan/agents/code-review.md +43 -11
  567. package/server/services/plan/board-config.ts +0 -1
  568. package/server/services/plan/composer.ts +63 -12
  569. package/server/services/plan/config-installer.ts +0 -1
  570. package/server/services/plan/dependency-resolver.ts +0 -1
  571. package/server/services/plan/executor.ts +48 -4
  572. package/server/services/plan/front-matter.ts +0 -1
  573. package/server/services/plan/issue-classification.ts +0 -1
  574. package/server/services/plan/issue-loader.ts +0 -1
  575. package/server/services/plan/issue-prompt-builder.ts +39 -2
  576. package/server/services/plan/issue-retry.ts +5 -2
  577. package/server/services/plan/issue-writer.ts +0 -1
  578. package/server/services/plan/output-manager.ts +0 -1
  579. package/server/services/plan/parser-core.ts +1 -1
  580. package/server/services/plan/parser-migration.ts +0 -1
  581. package/server/services/plan/parser.ts +0 -1
  582. package/server/services/plan/progress-log.ts +0 -1
  583. package/server/services/plan/prompt-builder.ts +0 -1
  584. package/server/services/plan/readiness-planner.ts +0 -1
  585. package/server/services/plan/review-gate.ts +0 -1
  586. package/server/services/plan/state-reconciler.ts +0 -1
  587. package/server/services/plan/types.ts +4 -1
  588. package/server/services/plan/watcher.ts +0 -1
  589. package/server/services/platform-credentials.ts +0 -1
  590. package/server/services/platform-token-lifecycle.ts +171 -0
  591. package/server/services/platform.ts +106 -148
  592. package/server/services/sentry.ts +0 -1
  593. package/server/services/settings.ts +161 -5
  594. package/server/services/terminal/pty-manager.ts +0 -1
  595. package/server/services/terminal/pty-utils.ts +0 -1
  596. package/server/services/websocket/autocomplete.ts +0 -1
  597. package/server/services/websocket/file-definition-handlers.ts +0 -1
  598. package/server/services/websocket/file-download-handler.ts +0 -1
  599. package/server/services/websocket/file-explorer-handlers.ts +0 -1
  600. package/server/services/websocket/file-search-handlers.ts +0 -1
  601. package/server/services/websocket/file-upload-handler.ts +6 -5
  602. package/server/services/websocket/file-utils.ts +0 -1
  603. package/server/services/websocket/git-branch-handlers.ts +20 -7
  604. package/server/services/websocket/git-diff-handlers.ts +0 -1
  605. package/server/services/websocket/git-handlers.ts +66 -10
  606. package/server/services/websocket/git-head-watcher.ts +0 -1
  607. package/server/services/websocket/git-log-handlers.ts +0 -1
  608. package/server/services/websocket/git-pr-handlers.ts +0 -1
  609. package/server/services/websocket/git-tag-handlers.ts +0 -1
  610. package/server/services/websocket/git-utils.ts +69 -9
  611. package/server/services/websocket/git-worktree-handlers.ts +260 -17
  612. package/server/services/websocket/handler-context.ts +0 -1
  613. package/server/services/websocket/handler.ts +62 -6
  614. package/server/services/websocket/index.ts +0 -1
  615. package/server/services/websocket/msg-id-tracker.ts +0 -1
  616. package/server/services/websocket/plan-board-handlers.ts +0 -1
  617. package/server/services/websocket/plan-execution-handlers.ts +6 -2
  618. package/server/services/websocket/plan-handlers.ts +0 -1
  619. package/server/services/websocket/plan-helpers.ts +0 -1
  620. package/server/services/websocket/plan-issue-handlers.ts +0 -1
  621. package/server/services/websocket/plan-sprint-handlers.ts +0 -1
  622. package/server/services/websocket/quality-complexity.ts +80 -27
  623. package/server/services/websocket/quality-eta.ts +155 -0
  624. package/server/services/websocket/quality-grading.ts +834 -0
  625. package/server/services/websocket/quality-handlers.ts +153 -8
  626. package/server/services/websocket/quality-linting.ts +0 -1
  627. package/server/services/websocket/quality-operations.ts +72 -0
  628. package/server/services/websocket/quality-persistence.ts +47 -8
  629. package/server/services/websocket/quality-review-agent.ts +154 -65
  630. package/server/services/websocket/quality-service.ts +415 -68
  631. package/server/services/websocket/quality-tools.ts +62 -3
  632. package/server/services/websocket/quality-types.ts +61 -4
  633. package/server/services/websocket/session-handlers.ts +64 -11
  634. package/server/services/websocket/session-history.ts +3 -1
  635. package/server/services/websocket/session-initialization.ts +189 -47
  636. package/server/services/websocket/session-registry.ts +37 -1
  637. package/server/services/websocket/settings-handlers.ts +41 -5
  638. package/server/services/websocket/skill-handlers.ts +0 -1
  639. package/server/services/websocket/skill-watcher.ts +0 -1
  640. package/server/services/websocket/tab-broadcast.ts +10 -3
  641. package/server/services/websocket/tab-event-buffer.ts +143 -12
  642. package/server/services/websocket/tab-event-replay.ts +70 -4
  643. package/server/services/websocket/tab-handlers.ts +53 -6
  644. package/server/services/websocket/terminal-handlers.ts +39 -3
  645. package/server/services/websocket/types.ts +39 -8
  646. package/server/utils/agent-manager.ts +0 -1
  647. package/server/utils/paths.ts +0 -1
  648. package/server/utils/port-manager.ts +0 -1
  649. package/server/utils/port.ts +0 -1
@@ -0,0 +1,834 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+
3
+ // ============================================================================
4
+ // Multi-Dimensional Quality Grading
5
+ // ============================================================================
6
+ //
7
+ // Three independent dimensions, severity-driven where it matters most:
8
+ //
9
+ // - Security — strictest; any medium-or-worse issue capped below B
10
+ // - Reliability — slightly lenient (not every complexity warning is a bug)
11
+ // - Maintainability — density-based (issues / KLOC) with a severity cap
12
+ //
13
+ // Letter grade scale (no `D` band — F covers 56-69, F- below 55):
14
+ //
15
+ // A+ 97-100 A 93-96 A- 90-92 "ship it"
16
+ // B+ 87-89 B 83-86 B- 80-82 "minor cleanup"
17
+ // C+ 77-79 C 73-76 C- 70-72 "needs work"
18
+ // F+ 65-69 F 56-64 F- 0-55 "broken"
19
+ //
20
+ // Three "auto-fail" rules layer on top of the dimension grades:
21
+ //
22
+ // 1. Build/compile errors → caps Reliability at F-
23
+ // 2. Critical security issue → caps Security at F-
24
+ // 3. Architectural findings → drop the affected dim's grade by 1-2 letters
25
+ //
26
+ // Industry alignment: SonarQube uses severity-driven A-E grades; Code Climate
27
+ // uses density-driven A-F. This module borrows the strictness of the former
28
+ // for Security/Reliability and the density model of the latter for
29
+ // Maintainability — matching the two metrics where each works best.
30
+ //
31
+ // All functions in this module are pure: same inputs -> same outputs, no I/O.
32
+ // ============================================================================
33
+
34
+ // ============================================================================
35
+ // Types
36
+ // ============================================================================
37
+
38
+ export type DimensionName = 'security' | 'reliability' | 'maintainability';
39
+
40
+ /**
41
+ * Letter grades. New code emits all variants except `'D'` (kept only for
42
+ * legacy reports persisted before the +/- rollout). See quality-types.ts for
43
+ * the score-band reference.
44
+ */
45
+ export type Grade =
46
+ | 'A+' | 'A' | 'A-'
47
+ | 'B+' | 'B' | 'B-'
48
+ | 'C+' | 'C' | 'C-'
49
+ | 'D'
50
+ | 'F+' | 'F' | 'F-'
51
+ | 'N/A';
52
+
53
+ /** Letter grades excluding modifiers — used internally for band logic. */
54
+ type BaseGrade = 'A' | 'B' | 'C' | 'F';
55
+
56
+ type Severity = 'critical' | 'high' | 'medium' | 'low';
57
+
58
+ export interface DimensionScore {
59
+ name: DimensionName;
60
+ score: number;
61
+ grade: Grade;
62
+ rationale: string;
63
+ available: boolean;
64
+ findingCount: number;
65
+ worstSeverity: Severity | null;
66
+ }
67
+
68
+ export interface QualityGate {
69
+ passed: boolean;
70
+ failingConditions: string[];
71
+ }
72
+
73
+ export interface QualityRating {
74
+ overall: { score: number; grade: Grade };
75
+ dimensions: DimensionScore[];
76
+ qualityGate: QualityGate;
77
+ gradeRationale: string;
78
+ }
79
+
80
+ // ============================================================================
81
+ // Category -> Dimension Mapping
82
+ // ============================================================================
83
+
84
+ const SECURITY_CATEGORIES = new Set<string>(['security']);
85
+ const RELIABILITY_CATEGORIES = new Set<string>(['bugs', 'logic', 'performance', 'complexity', 'build']);
86
+ const MAINTAINABILITY_CATEGORIES = new Set<string>([
87
+ 'lint',
88
+ 'linting',
89
+ 'format',
90
+ 'file-length',
91
+ 'function-length',
92
+ 'architecture',
93
+ 'oop',
94
+ 'maintainability',
95
+ ]);
96
+
97
+ /**
98
+ * Map a finding category to one of the three quality dimensions.
99
+ * Unknown categories default to maintainability (the catch-all bucket) so
100
+ * that surprise categories never silently disappear from the grade.
101
+ */
102
+ export function categoryToDimension(category: string): DimensionName {
103
+ if (SECURITY_CATEGORIES.has(category)) return 'security';
104
+ if (RELIABILITY_CATEGORIES.has(category)) return 'reliability';
105
+ if (MAINTAINABILITY_CATEGORIES.has(category)) return 'maintainability';
106
+ return 'maintainability';
107
+ }
108
+
109
+ /** Categories that represent architectural problems — used by the arch penalty. */
110
+ const ARCHITECTURE_CATEGORIES = new Set<string>(['architecture', 'oop']);
111
+
112
+ // ============================================================================
113
+ // Score Bands & Modifier Math
114
+ // ============================================================================
115
+
116
+ /**
117
+ * Score boundaries for each base grade. Note the gap between C (70+) and F+
118
+ * (≤69): the band 60-69 maps to F+ instead of D, per product spec ("60s and
119
+ * below is F").
120
+ */
121
+ const BASE_BAND_TOP: Record<BaseGrade, number> = {
122
+ A: 100,
123
+ B: 89,
124
+ C: 79,
125
+ F: 69, // F covers 56-69 (F+ for 65-69, F for 56-64) — F- splits off below
126
+ };
127
+ const BASE_BAND_BOTTOM: Record<BaseGrade, number> = {
128
+ A: 90,
129
+ B: 80,
130
+ C: 70,
131
+ F: 56, // F- covers 0-55 — handled specially in scoreToGrade()
132
+ };
133
+
134
+ /**
135
+ * Convert a 0-100 score to the full letter grade including +/- modifier.
136
+ *
137
+ * Within an A/B/C band, the band is split into thirds:
138
+ * X- bottom third (e.g., A-: 90-92)
139
+ * X middle third (e.g., A : 93-96)
140
+ * X+ top third (e.g., A+: 97-100)
141
+ *
142
+ * The F band uses two slices instead of three because there is no academic
143
+ * "F0" anchor and the user wanted F+/F/F-:
144
+ * F- 0-55 "critically broken"
145
+ * F 56-64 "broken"
146
+ * F+ 65-69 "barely failing"
147
+ *
148
+ * Compile/critical-severity hard caps are applied separately, not by score.
149
+ */
150
+ export function scoreToGrade(score: number): Grade {
151
+ if (score >= 97) return 'A+';
152
+ if (score >= 93) return 'A';
153
+ if (score >= 90) return 'A-';
154
+ if (score >= 87) return 'B+';
155
+ if (score >= 83) return 'B';
156
+ if (score >= 80) return 'B-';
157
+ if (score >= 77) return 'C+';
158
+ if (score >= 73) return 'C';
159
+ if (score >= 70) return 'C-';
160
+ if (score >= 65) return 'F+';
161
+ if (score >= 56) return 'F';
162
+ return 'F-';
163
+ }
164
+
165
+ /**
166
+ * Legacy single-letter conversion. Returns the *base* grade only (no
167
+ * modifier) for compatibility with callers that pre-date the +/- rollout
168
+ * (`scoreBreakdown.categoryPenalties[].grade`, etc.). New surfaces should
169
+ * call `scoreToGrade()` instead.
170
+ */
171
+ export function gradeFromScore(score: number): Grade {
172
+ const full = scoreToGrade(score);
173
+ // Strip the modifier so legacy callers still see exactly one of A/B/C/F.
174
+ return baseGradeOf(full);
175
+ }
176
+
177
+ /** Strip the +/- modifier from a letter grade. */
178
+ function baseGradeOf(g: Grade): Grade {
179
+ if (g === 'N/A' || g === 'D') return g;
180
+ if (g.startsWith('A')) return 'A';
181
+ if (g.startsWith('B')) return 'B';
182
+ if (g.startsWith('C')) return 'C';
183
+ return 'F';
184
+ }
185
+
186
+ /**
187
+ * Linearly interpolate a score within a base band.
188
+ *
189
+ * `position` is in [0, 1]: 0 = "as bad as this grade gets" (band bottom),
190
+ * 1 = "as good as this grade gets" (band top, just below the next grade).
191
+ */
192
+ function scoreInBand(grade: BaseGrade, position: number): number {
193
+ const clamped = Math.max(0, Math.min(1, position));
194
+ const bottom = BASE_BAND_BOTTOM[grade];
195
+ const top = BASE_BAND_TOP[grade];
196
+ return Math.round(bottom + (top - bottom) * clamped);
197
+ }
198
+
199
+ // ============================================================================
200
+ // Severity Helpers
201
+ // ============================================================================
202
+
203
+ function isSeverity(s: string): s is Severity {
204
+ return s === 'critical' || s === 'high' || s === 'medium' || s === 'low';
205
+ }
206
+
207
+ interface SeverityCounts {
208
+ critical: number;
209
+ high: number;
210
+ medium: number;
211
+ low: number;
212
+ total: number;
213
+ }
214
+
215
+ function countSeverities(findings: Array<{ severity: string }>): SeverityCounts {
216
+ const counts: SeverityCounts = { critical: 0, high: 0, medium: 0, low: 0, total: 0 };
217
+ for (const f of findings) {
218
+ if (!isSeverity(f.severity)) continue;
219
+ counts[f.severity]++;
220
+ counts.total++;
221
+ }
222
+ return counts;
223
+ }
224
+
225
+ function worstSeverity(counts: SeverityCounts): Severity | null {
226
+ if (counts.critical > 0) return 'critical';
227
+ if (counts.high > 0) return 'high';
228
+ if (counts.medium > 0) return 'medium';
229
+ if (counts.low > 0) return 'low';
230
+ return null;
231
+ }
232
+
233
+ /**
234
+ * Result of consulting a dimension's severity escape hatch — used by both
235
+ * Reliability and Maintainability to short-circuit a forgiving density grade
236
+ * when the underlying findings include a critical or high. The grade is the
237
+ * worst the dimension may receive after applying the escape; the consumer
238
+ * still picks `min(severityEscape.grade, band.grade)` so an even worse
239
+ * density-derived grade isn't paved over.
240
+ */
241
+ interface SeverityEscape {
242
+ grade: BaseGrade;
243
+ note: string;
244
+ }
245
+
246
+ // ============================================================================
247
+ // Security Dimension
248
+ // ============================================================================
249
+
250
+ /**
251
+ * Security grading — strictest of the three dimensions. Any medium-or-worse
252
+ * security finding immediately drops the grade below B because security
253
+ * issues can't be amortized over codebase size.
254
+ *
255
+ * A critical security issue caps at F- (the worst grade). One low-severity
256
+ * finding still earns a B- because every team has a few.
257
+ */
258
+ function gradeSecurity(findings: Array<{ severity: string }>): DimensionScore {
259
+ const counts = countSeverities(findings);
260
+ const worst = worstSeverity(counts);
261
+
262
+ if (counts.total === 0) {
263
+ return makeDimension('security', 100, '0 security findings', 0, null);
264
+ }
265
+
266
+ if (counts.critical > 0) {
267
+ // Critical security issue → F-, not just F. There's no recovering by
268
+ // averaging this away across a clean codebase.
269
+ return makeDimension(
270
+ 'security',
271
+ Math.max(0, 55 - counts.critical * 5),
272
+ `${counts.critical} critical-severity security ${pluralize('issue', counts.critical)}`,
273
+ counts.total,
274
+ worst,
275
+ );
276
+ }
277
+
278
+ let baseGrade: BaseGrade;
279
+ let position: number;
280
+ let rationale: string;
281
+
282
+ if (counts.high > 0) {
283
+ baseGrade = 'F';
284
+ position = 1 / (1 + counts.high);
285
+ rationale = `${counts.high} high-severity security ${pluralize('issue', counts.high)}`;
286
+ } else if (counts.medium > 0) {
287
+ baseGrade = 'C';
288
+ position = 1 / (1 + counts.medium);
289
+ rationale = `${counts.medium} medium-severity security ${pluralize('issue', counts.medium)}`;
290
+ } else {
291
+ // Only low-severity findings.
292
+ baseGrade = 'B';
293
+ position = 1 / Math.max(1, counts.low);
294
+ rationale = `${counts.low} low-severity security ${pluralize('issue', counts.low)}`;
295
+ }
296
+
297
+ const score = scoreInBand(baseGrade, position);
298
+ return makeDimension('security', score, rationale, counts.total, worst);
299
+ }
300
+
301
+ // ============================================================================
302
+ // Reliability Dimension
303
+ // ============================================================================
304
+ //
305
+ // Reliability uses the same density-based model as Maintainability, with a
306
+ // severity escape hatch — softer than the previous "2+ high → F" rule, which
307
+ // over-penalised codebases that had a handful of edge-case bugs flagged on
308
+ // rarely-executed paths. The new model:
309
+ //
310
+ // - Density ladder (issues per KLOC) at ≥5 KLOC
311
+ // - Absolute-count ladder at <5 KLOC (small projects shouldn't be density-
312
+ // rated; one extra finding moves the needle by 1.0+/KLOC)
313
+ // - Severity escape: 1 critical caps at F, any high caps at C
314
+ //
315
+ // Rationale: a real-world 10 KLOC service with 4 plausibly-improbable HIGH
316
+ // bugs (race conditions on degraded paths, edge-case fly-replay leaks)
317
+ // previously landed at F, dragging the entire app to F. Under this model
318
+ // it lands at C — "needs work" — which matches how a senior engineer would
319
+ // triage it on a code review. Critical bugs and compile errors still hit
320
+ // the F-tier through the escape hatch.
321
+ //
322
+ // Build/compile errors enter via the `build` category with severity `critical`,
323
+ // so they trip the escape hatch and hit F regardless of density.
324
+
325
+ interface ReliabilityBand {
326
+ grade: BaseGrade;
327
+ position: number;
328
+ label: string;
329
+ }
330
+
331
+ function reliabilityByCount(n: number): ReliabilityBand {
332
+ // Stricter than Maintainability's count ladder: a couple of real bugs hurt
333
+ // more than a couple of lint warnings, but a single isolated medium bug on
334
+ // a small project shouldn't pin the codebase at C.
335
+ const label = `${n} reliability ${pluralize('issue', n)}`;
336
+ if (n <= 2) return { grade: 'A', position: 1 - n / 2, label };
337
+ if (n <= 6) return { grade: 'B', position: 1 - (n - 2) / 4, label };
338
+ if (n <= 15) return { grade: 'C', position: 1 - (n - 6) / 9, label };
339
+ return { grade: 'F', position: 1 / (1 + (n - 15) / 15), label };
340
+ }
341
+
342
+ function reliabilityByDensity(n: number, kloc: number): ReliabilityBand {
343
+ // Density thresholds are tighter than Maintainability (5/10/25). A 50 KLOC
344
+ // codebase with 100 reliability bugs (density 2) is "minor cleanup", not
345
+ // pristine — but 1.4/KLOC is still A-band because real-world projects
346
+ // never get to zero. The escape hatch handles severity outliers above this.
347
+ const density = n / kloc;
348
+ const label = `${roundOne(density)} reliability ${pluralize('issue', n)} / KLOC`;
349
+ if (density < 1.5) return { grade: 'A', position: 1 - density / 1.5, label };
350
+ if (density < 4) return { grade: 'B', position: 1 - (density - 1.5) / 2.5, label };
351
+ if (density < 8) return { grade: 'C', position: 1 - (density - 4) / 4, label };
352
+ return { grade: 'F', position: 1 / (1 + (density - 8) / 8), label };
353
+ }
354
+
355
+ function reliabilityEscape(counts: SeverityCounts): SeverityEscape | null {
356
+ if (counts.critical > 0) {
357
+ return { grade: 'F', note: `${counts.critical} critical-severity ${pluralize('bug', counts.critical)}` };
358
+ }
359
+ if (counts.high > 0) {
360
+ return { grade: 'C', note: `${counts.high} high-severity ${pluralize('bug', counts.high)}` };
361
+ }
362
+ return null;
363
+ }
364
+
365
+ /**
366
+ * Reliability grading — density-based with a severity escape hatch.
367
+ *
368
+ * - Empty / ≤1 low: A-band (clean by convention).
369
+ * - Density-based grade (≥5 KLOC) or count-based grade (<5 KLOC) drives
370
+ * the baseline. Both ladders mirror Maintainability's so reliability and
371
+ * maintainability remain comparable at a glance.
372
+ * - Severity escape: critical → F, high → C. This matches Maintainability and
373
+ * prevents a handful of medium-density bugs from being silently rated A
374
+ * when at least one is severe.
375
+ *
376
+ * Build/compile errors flow in via `build` category with severity `critical`
377
+ * and therefore land at F via the escape hatch — no special-case branching.
378
+ */
379
+ function gradeReliability(findings: Array<{ severity: string }>, totalLines: number): DimensionScore {
380
+ const counts = countSeverities(findings);
381
+ const worst = worstSeverity(counts);
382
+ const kloc = Math.max(totalLines / 1000, 1.0);
383
+
384
+ if (counts.total === 0) {
385
+ return makeDimension('reliability', 100, '0 reliability findings', 0, null);
386
+ }
387
+
388
+ // ≤1 low and nothing else is treated as clean — every team has one.
389
+ if (counts.low <= 1 && counts.medium === 0 && counts.high === 0 && counts.critical === 0) {
390
+ return makeDimension('reliability', scoreInBand('A', 0.5), '1 low-severity reliability issue', counts.total, worst);
391
+ }
392
+
393
+ const band = kloc < 5 ? reliabilityByCount(counts.total) : reliabilityByDensity(counts.total, kloc);
394
+ const severityCap = reliabilityEscape(counts);
395
+ const useCap = severityCap && baseIsWorse(severityCap.grade, band.grade);
396
+ const finalGrade = useCap ? severityCap.grade : band.grade;
397
+ const finalPosition = useCap ? 0.5 : band.position;
398
+ const rationale = useCap ? `${band.label}, ${severityCap.note}` : band.label;
399
+
400
+ return makeDimension('reliability', scoreInBand(finalGrade, finalPosition), rationale, counts.total, worst);
401
+ }
402
+
403
+ // ============================================================================
404
+ // Maintainability Dimension
405
+ // ============================================================================
406
+
407
+ interface MaintainabilityBand {
408
+ grade: BaseGrade;
409
+ position: number;
410
+ label: string;
411
+ }
412
+
413
+ function maintainabilityByCount(n: number): MaintainabilityBand {
414
+ const label = `${n} maintainability ${pluralize('issue', n)}`;
415
+ if (n <= 5) return { grade: 'A', position: 1 - n / 5, label };
416
+ if (n <= 15) return { grade: 'B', position: 1 - (n - 5) / 10, label };
417
+ if (n <= 30) return { grade: 'C', position: 1 - (n - 15) / 15, label };
418
+ return { grade: 'F', position: 1 / (1 + (n - 30) / 30), label };
419
+ }
420
+
421
+ function maintainabilityByDensity(n: number, kloc: number): MaintainabilityBand {
422
+ const density = n / kloc;
423
+ const label = `${roundOne(density)} ${pluralize('issue', n)} / KLOC`;
424
+ if (density < 5) return { grade: 'A', position: 1 - density / 5, label };
425
+ if (density < 10) return { grade: 'B', position: 1 - (density - 5) / 5, label };
426
+ if (density < 25) return { grade: 'C', position: 1 - (density - 10) / 15, label };
427
+ return { grade: 'F', position: 1 / (1 + (density - 25) / 25), label };
428
+ }
429
+
430
+ function maintainabilityEscape(counts: SeverityCounts): SeverityEscape | null {
431
+ if (counts.critical > 0) {
432
+ return { grade: 'F', note: `${counts.critical} critical-severity ${pluralize('issue', counts.critical)}` };
433
+ }
434
+ if (counts.high > 0) {
435
+ return { grade: 'C', note: `${counts.high} high-severity ${pluralize('issue', counts.high)}` };
436
+ }
437
+ return null;
438
+ }
439
+
440
+ /**
441
+ * Maintainability uses a density-based grade (issues per KLOC) once the
442
+ * codebase is at least 5 KLOC. For smaller codebases, density is too noisy
443
+ * (one extra lint issue moves density by 1.0+), so we fall back to absolute
444
+ * counts — preventing tiny projects from being unfairly penalized.
445
+ *
446
+ * Severity escape hatch: a critical maintainability finding (e.g., a 3000-
447
+ * line file with high cohesion-violation severity) caps at F; a high-severity
448
+ * one caps at C. "Worst wins" — we take min of density-grade and severity-cap.
449
+ */
450
+ function gradeMaintainability(findings: Array<{ severity: string }>, totalLines: number): DimensionScore {
451
+ const counts = countSeverities(findings);
452
+ const kloc = Math.max(totalLines / 1000, 1.0);
453
+
454
+ if (counts.total === 0) {
455
+ return makeDimension('maintainability', 100, '0 maintainability findings', 0, null);
456
+ }
457
+
458
+ const band = kloc < 5 ? maintainabilityByCount(counts.total) : maintainabilityByDensity(counts.total, kloc);
459
+ const severityCap = maintainabilityEscape(counts);
460
+ const useCap = severityCap && baseIsWorse(severityCap.grade, band.grade);
461
+ const finalGrade = useCap ? severityCap.grade : band.grade;
462
+ const finalPosition = useCap ? 0.5 : band.position;
463
+ const rationale = useCap ? `${band.label}, ${severityCap.note}` : band.label;
464
+
465
+ return makeDimension('maintainability', scoreInBand(finalGrade, finalPosition), rationale, counts.total, worstSeverity(counts));
466
+ }
467
+
468
+ // ============================================================================
469
+ // Architectural Penalty
470
+ // ============================================================================
471
+
472
+ /**
473
+ * Drop a dimension's grade by N letters because of architectural findings.
474
+ *
475
+ * Rationale: a high-severity architectural problem (god class, leaky
476
+ * abstraction, broken layering) is qualitatively different from a long-file
477
+ * lint warning — it pollutes every change that touches the affected code.
478
+ * The user spec calls for explicit letter-grade drops:
479
+ *
480
+ * - 1 high-severity arch issue → drop 1 letter
481
+ * - 2+ high-severity arch issues → drop 2 letters
482
+ * - any critical-severity arch issue → drop 2 letters
483
+ *
484
+ * Letters drop A → B → C → F → F-. We never go lower than F-. The drop is
485
+ * applied AFTER the dimension's normal grading so the displayed score still
486
+ * reflects the underlying finding count, but the letter grade carries the
487
+ * architectural weight that a density-based score would otherwise miss.
488
+ */
489
+ function archDropCount(archFindings: Array<{ severity: string }>): number {
490
+ let highCount = 0;
491
+ let criticalCount = 0;
492
+ for (const f of archFindings) {
493
+ if (f.severity === 'critical') criticalCount++;
494
+ else if (f.severity === 'high') highCount++;
495
+ }
496
+ if (criticalCount >= 1) return 2;
497
+ if (highCount >= 2) return 2;
498
+ if (highCount >= 1) return 1;
499
+ return 0;
500
+ }
501
+
502
+ const BASE_LETTERS: BaseGrade[] = ['A', 'B', 'C', 'F'];
503
+
504
+ function gradeModifier(grade: Grade): '' | '+' | '-' {
505
+ if (grade.endsWith('+')) return '+';
506
+ if (grade.endsWith('-')) return '-';
507
+ return '';
508
+ }
509
+
510
+ function applyModifierToTargetBase(targetBase: BaseGrade, modifier: '' | '+' | '-'): Grade {
511
+ // F's modifier semantics differ from A/B/C: F+ is "barely failing" while
512
+ // A+/B+/C+ are "top of band." For simplicity we map any modifier on F to
513
+ // its matching variant, and use F- (the worst) for any post-F overshoot.
514
+ if (targetBase === 'F') {
515
+ if (modifier === '+') return 'F+';
516
+ if (modifier === '-') return 'F-';
517
+ return 'F';
518
+ }
519
+ if (modifier === '+') return `${targetBase}+` as Grade;
520
+ if (modifier === '-') return `${targetBase}-` as Grade;
521
+ return targetBase as Grade;
522
+ }
523
+
524
+ /**
525
+ * Drop a grade by N "letters." A "letter" here means a full base-grade step
526
+ * (A → B → C → F → F-), preserving the modifier when possible. So A+ dropped
527
+ * by 1 becomes B+, not A. Stops at F-.
528
+ */
529
+ function dropGradeByLetters(grade: Grade, letters: number): Grade {
530
+ if (letters <= 0 || grade === 'N/A' || grade === 'D') return grade;
531
+ const baseLetter = baseGradeOf(grade);
532
+ const baseIdx = BASE_LETTERS.indexOf(baseLetter as BaseGrade);
533
+ if (baseIdx === -1) return grade;
534
+ const targetBaseIdx = baseIdx + letters;
535
+ // Past the F base — bottom out at F- (the absolute worst grade).
536
+ if (targetBaseIdx > 3) return 'F-';
537
+ const targetBase = BASE_LETTERS[targetBaseIdx];
538
+ return applyModifierToTargetBase(targetBase, gradeModifier(grade));
539
+ }
540
+
541
+ function applyArchPenalty(dim: DimensionScore, archFindings: Array<{ severity: string }>): DimensionScore {
542
+ const drop = archDropCount(archFindings);
543
+ if (drop === 0) return dim;
544
+ const dropped = dropGradeByLetters(dim.grade, drop);
545
+ if (dropped === dim.grade) return dim;
546
+ const archCount = archFindings.length;
547
+ const noun = pluralize('architectural finding', archCount);
548
+ const note = `dropped ${drop} ${pluralize('letter', drop)} by ${archCount} ${noun}`;
549
+ return {
550
+ ...dim,
551
+ grade: dropped,
552
+ // Re-anchor score to the new band's midpoint so score and letter agree.
553
+ score: anchorScoreToGrade(dropped, dim.score),
554
+ rationale: dim.rationale === '0 maintainability findings' || dim.findingCount === 0
555
+ ? note
556
+ : `${dim.rationale}; ${note}`,
557
+ };
558
+ }
559
+
560
+ /**
561
+ * Re-snap a score to fall within the band of the given grade. Used after
562
+ * applying the architectural penalty so the displayed score never disagrees
563
+ * with the displayed letter (e.g., grade C with score 89 would be jarring).
564
+ *
565
+ * If the original score is already in-band, keep it; otherwise pick the
566
+ * band's midpoint as a sensible default.
567
+ */
568
+ function anchorScoreToGrade(grade: Grade, originalScore: number): number {
569
+ if (grade === 'N/A' || grade === 'D') return originalScore;
570
+ const ranges: Record<Exclude<Grade, 'N/A' | 'D'>, [number, number]> = {
571
+ 'A+': [97, 100], A: [93, 96], 'A-': [90, 92],
572
+ 'B+': [87, 89], B: [83, 86], 'B-': [80, 82],
573
+ 'C+': [77, 79], C: [73, 76], 'C-': [70, 72],
574
+ 'F+': [65, 69], F: [56, 64], 'F-': [0, 55],
575
+ };
576
+ const [lo, hi] = ranges[grade as Exclude<Grade, 'N/A' | 'D'>];
577
+ if (originalScore >= lo && originalScore <= hi) return originalScore;
578
+ return Math.round((lo + hi) / 2);
579
+ }
580
+
581
+ // ============================================================================
582
+ // Grade Comparison Helpers
583
+ // ============================================================================
584
+
585
+ const BASE_RANK: Record<BaseGrade, number> = { F: 1, C: 2, B: 3, A: 4 };
586
+
587
+ function baseIsWorse(a: BaseGrade, b: BaseGrade): boolean {
588
+ return BASE_RANK[a] < BASE_RANK[b];
589
+ }
590
+
591
+ const FULL_RANK: Record<Exclude<Grade, 'N/A' | 'D'>, number> = {
592
+ 'F-': 0, F: 1, 'F+': 2,
593
+ 'C-': 3, C: 4, 'C+': 5,
594
+ 'B-': 6, B: 7, 'B+': 8,
595
+ 'A-': 9, A: 10, 'A+': 11,
596
+ };
597
+
598
+ function gradeRank(g: Grade): number {
599
+ if (g === 'N/A') return -1;
600
+ if (g === 'D') return 1.5; // legacy: between F+ and C-
601
+ return FULL_RANK[g as Exclude<Grade, 'N/A' | 'D'>];
602
+ }
603
+
604
+ function worstOf(grades: Grade[]): Grade {
605
+ let worst: Grade = 'A+';
606
+ for (const g of grades) {
607
+ if (g === 'N/A') continue;
608
+ if (gradeRank(g) < gradeRank(worst)) worst = g;
609
+ }
610
+ return worst;
611
+ }
612
+
613
+ // ============================================================================
614
+ // Misc Helpers
615
+ // ============================================================================
616
+
617
+ function pluralize(word: string, n: number): string {
618
+ return n === 1 ? word : `${word}s`;
619
+ }
620
+
621
+ function roundOne(n: number): number {
622
+ return Math.round(n * 10) / 10;
623
+ }
624
+
625
+ function dimensionDisplayName(name: DimensionName): string {
626
+ return name.charAt(0).toUpperCase() + name.slice(1);
627
+ }
628
+
629
+ function makeDimension(
630
+ name: DimensionName,
631
+ score: number,
632
+ rationale: string,
633
+ findingCount: number,
634
+ worst: Severity | null,
635
+ ): DimensionScore {
636
+ return {
637
+ name,
638
+ score,
639
+ grade: scoreToGrade(score),
640
+ rationale,
641
+ available: true,
642
+ findingCount,
643
+ worstSeverity: worst,
644
+ };
645
+ }
646
+
647
+ function naDimension(name: DimensionName): DimensionScore {
648
+ return {
649
+ name,
650
+ score: 0,
651
+ grade: 'N/A',
652
+ rationale: 'No tools available to evaluate',
653
+ available: false,
654
+ findingCount: 0,
655
+ worstSeverity: null,
656
+ };
657
+ }
658
+
659
+ // ============================================================================
660
+ // Top-Level Entry Point
661
+ // ============================================================================
662
+
663
+ function bucketByDimension(
664
+ findings: Array<{ severity: string; category: string }>,
665
+ ): {
666
+ security: Array<{ severity: string; category: string }>;
667
+ reliability: Array<{ severity: string; category: string }>;
668
+ maintainability: Array<{ severity: string; category: string }>;
669
+ architecture: Array<{ severity: string; category: string }>;
670
+ } {
671
+ const security: Array<{ severity: string; category: string }> = [];
672
+ const reliability: Array<{ severity: string; category: string }> = [];
673
+ const maintainability: Array<{ severity: string; category: string }> = [];
674
+ const architecture: Array<{ severity: string; category: string }> = [];
675
+ for (const f of findings) {
676
+ if (ARCHITECTURE_CATEGORIES.has(f.category)) architecture.push(f);
677
+ const dim = categoryToDimension(f.category);
678
+ if (dim === 'security') security.push(f);
679
+ else if (dim === 'reliability') reliability.push(f);
680
+ else maintainability.push(f);
681
+ }
682
+ return { security, reliability, maintainability, architecture };
683
+ }
684
+
685
+ function isDimensionAvailable(
686
+ dim: DimensionName,
687
+ hasFindings: boolean,
688
+ options?: { availableDimensions?: Set<DimensionName>; forceNA?: Set<DimensionName> },
689
+ ): boolean {
690
+ if (options?.forceNA?.has(dim)) return false;
691
+ const explicit = options?.availableDimensions;
692
+ if (explicit) return explicit.has(dim);
693
+ // Auto-detect: maintainability always on, security/reliability iff findings exist.
694
+ return dim === 'maintainability' ? true : hasFindings;
695
+ }
696
+
697
+ /**
698
+ * Combine the available dimensions into a single overall grade + score.
699
+ *
700
+ * "Worst dimension wins" for the letter grade — a single failing dimension
701
+ * caps the overall score, matching how SonarQube's quality gate behaves.
702
+ * The numeric score is `min(avg, worst)` so a great Maintainability score
703
+ * can't paper over a Security failure.
704
+ */
705
+ function computeOverall(availableDims: DimensionScore[]): { grade: Grade; score: number } {
706
+ if (availableDims.length === 0) {
707
+ return { grade: 'N/A', score: 0 };
708
+ }
709
+ const grades = availableDims.map((d) => d.grade);
710
+ const scores = availableDims.map((d) => d.score);
711
+ const avg = scores.reduce((s, n) => s + n, 0) / scores.length;
712
+ const worst = worstOf(grades);
713
+ // Re-snap the displayed score so it lives in the worst dimension's band —
714
+ // otherwise we'd display a B-letter with a C-numeric score (or vice versa).
715
+ const blendedScore = Math.round(Math.min(avg, Math.min(...scores)));
716
+ return { grade: worst, score: anchorScoreToGrade(worst, blendedScore) };
717
+ }
718
+
719
+ export function computeQualityRating(
720
+ allFindings: Array<{ severity: string; category: string }>,
721
+ totalLines: number,
722
+ options?: { availableDimensions?: Set<DimensionName>; forceNA?: Set<DimensionName> },
723
+ ): QualityRating {
724
+ const buckets = bucketByDimension(allFindings);
725
+
726
+ // Initial dimension grades, before architectural penalty.
727
+ const security = isDimensionAvailable('security', buckets.security.length > 0, options)
728
+ ? gradeSecurity(buckets.security)
729
+ : naDimension('security');
730
+ const reliabilityRaw = isDimensionAvailable('reliability', buckets.reliability.length > 0, options)
731
+ ? gradeReliability(buckets.reliability, totalLines)
732
+ : naDimension('reliability');
733
+ const maintainabilityRaw = isDimensionAvailable('maintainability', true, options)
734
+ ? gradeMaintainability(buckets.maintainability, totalLines)
735
+ : naDimension('maintainability');
736
+
737
+ // Architectural penalty: hits whichever dimension(s) have arch findings
738
+ // bucketed into them (currently maintainability via the category map).
739
+ const archFindings = buckets.architecture;
740
+ const maintainability = maintainabilityRaw.available
741
+ ? applyArchPenalty(maintainabilityRaw, archFindings)
742
+ : maintainabilityRaw;
743
+
744
+ const dimensions: DimensionScore[] = [security, reliabilityRaw, maintainability];
745
+ const availableDims = dimensions.filter((d) => d.available);
746
+ const overall = computeOverall(availableDims);
747
+ const qualityGate = computeQualityGate(security, reliabilityRaw, archFindings.length);
748
+ const gradeRationale = computeGradeRationale(availableDims, overall.grade, allFindings.length);
749
+
750
+ return {
751
+ overall,
752
+ dimensions,
753
+ qualityGate,
754
+ gradeRationale,
755
+ };
756
+ }
757
+
758
+ // ============================================================================
759
+ // Quality Gate
760
+ // ============================================================================
761
+
762
+ /**
763
+ * The Quality Gate is a coarse PASS/FAIL signal layered on top of the grades.
764
+ * It only fires for the most user-actionable thresholds — any C-or-worse
765
+ * security grade, any F-tier reliability grade, or 2+ high-severity
766
+ * architectural findings. N/A dimensions never trigger a fail (we don't fail
767
+ * on missing data).
768
+ */
769
+ function isFTier(g: Grade): boolean {
770
+ return g === 'F+' || g === 'F' || g === 'F-' || g === 'D';
771
+ }
772
+
773
+ function isCorWorse(g: Grade): boolean {
774
+ return baseGradeOf(g) === 'C' || isFTier(g);
775
+ }
776
+
777
+ function computeQualityGate(
778
+ security: DimensionScore,
779
+ reliability: DimensionScore,
780
+ archFindingCount: number,
781
+ ): QualityGate {
782
+ const failingConditions: string[] = [];
783
+
784
+ if (security.available && isCorWorse(security.grade)) {
785
+ failingConditions.push(`Security grade ${security.grade} — ${security.rationale}`);
786
+ }
787
+ if (reliability.available && isFTier(reliability.grade)) {
788
+ failingConditions.push(`Reliability grade ${reliability.grade} — ${reliability.rationale}`);
789
+ }
790
+ if (archFindingCount >= 2) {
791
+ failingConditions.push(`${archFindingCount} architectural findings`);
792
+ }
793
+
794
+ return {
795
+ passed: failingConditions.length === 0,
796
+ failingConditions,
797
+ };
798
+ }
799
+
800
+ // ============================================================================
801
+ // Grade Rationale
802
+ // ============================================================================
803
+
804
+ function computeGradeRationale(
805
+ availableDims: DimensionScore[],
806
+ overallGrade: Grade,
807
+ totalFindingCount: number,
808
+ ): string {
809
+ if (totalFindingCount === 0) {
810
+ return 'Clean — no findings detected';
811
+ }
812
+ if (availableDims.length === 0 || overallGrade === 'N/A') {
813
+ return 'No dimensions available to grade';
814
+ }
815
+
816
+ // All available dimensions share the same base letter -> "consistent
817
+ // quality". With +/- modifiers it's normal for sibling dimensions to land
818
+ // at A vs A+ depending on within-band position; calling that "inconsistent"
819
+ // would be misleading. We compare base letters so the user-facing message
820
+ // captures the high-level shape rather than every minor band difference.
821
+ const firstBase = baseGradeOf(availableDims[0].grade);
822
+ const allSameBase = availableDims.every((d) => baseGradeOf(d.grade) === firstBase);
823
+ if (allSameBase) {
824
+ return `All dimensions ${firstBase}-tier — consistent quality`;
825
+ }
826
+
827
+ // Find the dimension that pinned the overall grade (worst available).
828
+ const worstDim =
829
+ availableDims.find((d) => d.grade === overallGrade) ??
830
+ // Fallback shouldn't fire since overallGrade was derived from availableDims.
831
+ availableDims[0];
832
+
833
+ return `Capped at ${overallGrade} by ${dimensionDisplayName(worstDim.name)} (${worstDim.rationale})`;
834
+ }