modestbench 0.2.0 → 0.3.1

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 (357) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +131 -34
  3. package/dist/cli/commands/analyze.cjs +60 -0
  4. package/dist/cli/commands/analyze.cjs.map +1 -0
  5. package/dist/cli/commands/analyze.d.cts +35 -0
  6. package/dist/cli/commands/analyze.d.cts.map +1 -0
  7. package/dist/cli/commands/analyze.d.ts +35 -0
  8. package/dist/cli/commands/analyze.d.ts.map +1 -0
  9. package/dist/cli/commands/analyze.js +56 -0
  10. package/dist/cli/commands/analyze.js.map +1 -0
  11. package/dist/cli/commands/baseline.cjs +404 -0
  12. package/dist/cli/commands/baseline.cjs.map +1 -0
  13. package/dist/cli/commands/baseline.d.cts +72 -0
  14. package/dist/cli/commands/baseline.d.cts.map +1 -0
  15. package/dist/cli/commands/baseline.d.ts +72 -0
  16. package/dist/cli/commands/baseline.d.ts.map +1 -0
  17. package/dist/cli/commands/baseline.js +396 -0
  18. package/dist/cli/commands/baseline.js.map +1 -0
  19. package/dist/cli/commands/history.d.cts +1 -1
  20. package/dist/cli/commands/history.d.cts.map +1 -1
  21. package/dist/cli/commands/history.d.ts +1 -1
  22. package/dist/cli/commands/history.d.ts.map +1 -1
  23. package/dist/cli/commands/init.cjs +99 -166
  24. package/dist/cli/commands/init.cjs.map +1 -1
  25. package/dist/cli/commands/init.d.cts +4 -4
  26. package/dist/cli/commands/init.d.cts.map +1 -1
  27. package/dist/cli/commands/init.d.ts +4 -4
  28. package/dist/cli/commands/init.d.ts.map +1 -1
  29. package/dist/cli/commands/init.js +99 -166
  30. package/dist/cli/commands/init.js.map +1 -1
  31. package/dist/cli/commands/run.cjs +146 -127
  32. package/dist/cli/commands/run.cjs.map +1 -1
  33. package/dist/cli/commands/run.d.cts +16 -3
  34. package/dist/cli/commands/run.d.cts.map +1 -1
  35. package/dist/cli/commands/run.d.ts +16 -3
  36. package/dist/cli/commands/run.d.ts.map +1 -1
  37. package/dist/cli/commands/run.js +145 -93
  38. package/dist/cli/commands/run.js.map +1 -1
  39. package/dist/cli/index.cjs +583 -394
  40. package/dist/cli/index.cjs.map +1 -1
  41. package/dist/cli/index.d.cts +4 -16
  42. package/dist/cli/index.d.cts.map +1 -1
  43. package/dist/cli/index.d.ts +4 -16
  44. package/dist/cli/index.d.ts.map +1 -1
  45. package/dist/cli/index.js +575 -386
  46. package/dist/cli/index.js.map +1 -1
  47. package/dist/config/budget-schema.cjs +172 -0
  48. package/dist/config/budget-schema.cjs.map +1 -0
  49. package/dist/config/budget-schema.d.cts +59 -0
  50. package/dist/config/budget-schema.d.cts.map +1 -0
  51. package/dist/config/budget-schema.d.ts +59 -0
  52. package/dist/config/budget-schema.d.ts.map +1 -0
  53. package/dist/config/budget-schema.js +166 -0
  54. package/dist/config/budget-schema.js.map +1 -0
  55. package/dist/config/schema.cjs +182 -2
  56. package/dist/config/schema.cjs.map +1 -1
  57. package/dist/config/schema.d.cts +122 -3
  58. package/dist/config/schema.d.cts.map +1 -1
  59. package/dist/config/schema.d.ts +122 -3
  60. package/dist/config/schema.d.ts.map +1 -1
  61. package/dist/config/schema.js +180 -1
  62. package/dist/config/schema.js.map +1 -1
  63. package/dist/constants.cjs +45 -2
  64. package/dist/constants.cjs.map +1 -1
  65. package/dist/constants.d.cts +41 -0
  66. package/dist/constants.d.cts.map +1 -1
  67. package/dist/constants.d.ts +41 -0
  68. package/dist/constants.d.ts.map +1 -1
  69. package/dist/constants.js +44 -1
  70. package/dist/constants.js.map +1 -1
  71. package/dist/core/engine.cjs +114 -23
  72. package/dist/core/engine.cjs.map +1 -1
  73. package/dist/core/engine.d.cts +7 -7
  74. package/dist/core/engine.d.cts.map +1 -1
  75. package/dist/core/engine.d.ts +7 -7
  76. package/dist/core/engine.d.ts.map +1 -1
  77. package/dist/core/engine.js +115 -24
  78. package/dist/core/engine.js.map +1 -1
  79. package/dist/core/engines/accurate-engine.cjs +171 -36
  80. package/dist/core/engines/accurate-engine.cjs.map +1 -1
  81. package/dist/core/engines/accurate-engine.d.cts +5 -0
  82. package/dist/core/engines/accurate-engine.d.cts.map +1 -1
  83. package/dist/core/engines/accurate-engine.d.ts +5 -0
  84. package/dist/core/engines/accurate-engine.d.ts.map +1 -1
  85. package/dist/core/engines/accurate-engine.js +171 -36
  86. package/dist/core/engines/accurate-engine.js.map +1 -1
  87. package/dist/core/engines/tinybench-engine.cjs +3 -2
  88. package/dist/core/engines/tinybench-engine.cjs.map +1 -1
  89. package/dist/core/engines/tinybench-engine.d.cts.map +1 -1
  90. package/dist/core/engines/tinybench-engine.d.ts.map +1 -1
  91. package/dist/core/engines/tinybench-engine.js +3 -2
  92. package/dist/core/engines/tinybench-engine.js.map +1 -1
  93. package/dist/core/output-path-resolver.cjs +8 -1
  94. package/dist/core/output-path-resolver.cjs.map +1 -1
  95. package/dist/core/output-path-resolver.d.cts.map +1 -1
  96. package/dist/core/output-path-resolver.d.ts.map +1 -1
  97. package/dist/core/output-path-resolver.js +9 -2
  98. package/dist/core/output-path-resolver.js.map +1 -1
  99. package/dist/errors/base.cjs +12 -3
  100. package/dist/errors/base.cjs.map +1 -1
  101. package/dist/errors/base.d.cts +7 -0
  102. package/dist/errors/base.d.cts.map +1 -1
  103. package/dist/errors/base.d.ts +7 -0
  104. package/dist/errors/base.d.ts.map +1 -1
  105. package/dist/errors/base.js +10 -2
  106. package/dist/errors/base.js.map +1 -1
  107. package/dist/errors/budget.cjs +37 -0
  108. package/dist/errors/budget.cjs.map +1 -0
  109. package/dist/errors/budget.d.cts +31 -0
  110. package/dist/errors/budget.d.cts.map +1 -0
  111. package/dist/errors/budget.d.ts +31 -0
  112. package/dist/errors/budget.d.ts.map +1 -0
  113. package/dist/errors/budget.js +33 -0
  114. package/dist/errors/budget.js.map +1 -0
  115. package/dist/errors/index.cjs +4 -1
  116. package/dist/errors/index.cjs.map +1 -1
  117. package/dist/errors/index.d.cts +1 -0
  118. package/dist/errors/index.d.cts.map +1 -1
  119. package/dist/errors/index.d.ts +1 -0
  120. package/dist/errors/index.d.ts.map +1 -1
  121. package/dist/errors/index.js +2 -0
  122. package/dist/errors/index.js.map +1 -1
  123. package/dist/index.cjs +13 -1
  124. package/dist/index.cjs.map +1 -1
  125. package/dist/index.d.cts +5 -0
  126. package/dist/index.d.cts.map +1 -1
  127. package/dist/index.d.ts +5 -0
  128. package/dist/index.d.ts.map +1 -1
  129. package/dist/index.js +7 -0
  130. package/dist/index.js.map +1 -1
  131. package/dist/reporters/csv.cjs +37 -17
  132. package/dist/reporters/csv.cjs.map +1 -1
  133. package/dist/reporters/csv.d.cts +3 -6
  134. package/dist/reporters/csv.d.cts.map +1 -1
  135. package/dist/reporters/csv.d.ts +3 -6
  136. package/dist/reporters/csv.d.ts.map +1 -1
  137. package/dist/reporters/csv.js +37 -17
  138. package/dist/reporters/csv.js.map +1 -1
  139. package/dist/reporters/human.cjs +290 -67
  140. package/dist/reporters/human.cjs.map +1 -1
  141. package/dist/reporters/human.d.cts +25 -13
  142. package/dist/reporters/human.d.cts.map +1 -1
  143. package/dist/reporters/human.d.ts +25 -13
  144. package/dist/reporters/human.d.ts.map +1 -1
  145. package/dist/reporters/human.js +290 -67
  146. package/dist/reporters/human.js.map +1 -1
  147. package/dist/reporters/json.cjs +23 -48
  148. package/dist/reporters/json.cjs.map +1 -1
  149. package/dist/reporters/json.d.cts +2 -28
  150. package/dist/reporters/json.d.cts.map +1 -1
  151. package/dist/reporters/json.d.ts +2 -28
  152. package/dist/reporters/json.d.ts.map +1 -1
  153. package/dist/reporters/json.js +25 -50
  154. package/dist/reporters/json.js.map +1 -1
  155. package/dist/reporters/profile-human.cjs +154 -0
  156. package/dist/reporters/profile-human.cjs.map +1 -0
  157. package/dist/reporters/profile-human.d.cts +44 -0
  158. package/dist/reporters/profile-human.d.cts.map +1 -0
  159. package/dist/reporters/profile-human.d.ts +44 -0
  160. package/dist/reporters/profile-human.d.ts.map +1 -0
  161. package/dist/reporters/profile-human.js +147 -0
  162. package/dist/reporters/profile-human.js.map +1 -0
  163. package/dist/reporters/simple.cjs +67 -45
  164. package/dist/reporters/simple.cjs.map +1 -1
  165. package/dist/reporters/simple.d.cts +14 -14
  166. package/dist/reporters/simple.d.cts.map +1 -1
  167. package/dist/reporters/simple.d.ts +14 -14
  168. package/dist/reporters/simple.d.ts.map +1 -1
  169. package/dist/reporters/simple.js +67 -45
  170. package/dist/reporters/simple.js.map +1 -1
  171. package/dist/schema/modestbench-config.schema.json +153 -0
  172. package/dist/services/baseline-storage.cjs +151 -0
  173. package/dist/services/baseline-storage.cjs.map +1 -0
  174. package/dist/services/baseline-storage.d.cts +55 -0
  175. package/dist/services/baseline-storage.d.cts.map +1 -0
  176. package/dist/services/baseline-storage.d.ts +55 -0
  177. package/dist/services/baseline-storage.d.ts.map +1 -0
  178. package/dist/services/baseline-storage.js +147 -0
  179. package/dist/services/baseline-storage.js.map +1 -0
  180. package/dist/services/budget-evaluator.cjs +146 -0
  181. package/dist/services/budget-evaluator.cjs.map +1 -0
  182. package/dist/services/budget-evaluator.d.cts +29 -0
  183. package/dist/services/budget-evaluator.d.cts.map +1 -0
  184. package/dist/services/budget-evaluator.d.ts +29 -0
  185. package/dist/services/budget-evaluator.d.ts.map +1 -0
  186. package/dist/services/budget-evaluator.js +142 -0
  187. package/dist/services/budget-evaluator.js.map +1 -0
  188. package/dist/services/config-manager.cjs +24 -10
  189. package/dist/services/config-manager.cjs.map +1 -1
  190. package/dist/services/config-manager.d.cts +6 -1
  191. package/dist/services/config-manager.d.cts.map +1 -1
  192. package/dist/services/config-manager.d.ts +6 -1
  193. package/dist/services/config-manager.d.ts.map +1 -1
  194. package/dist/services/config-manager.js +24 -10
  195. package/dist/services/config-manager.js.map +1 -1
  196. package/dist/services/file-loader.cjs +3 -6
  197. package/dist/services/file-loader.cjs.map +1 -1
  198. package/dist/services/file-loader.d.cts.map +1 -1
  199. package/dist/services/file-loader.d.ts.map +1 -1
  200. package/dist/services/file-loader.js +3 -6
  201. package/dist/services/file-loader.js.map +1 -1
  202. package/dist/services/profiler/profile-filter.cjs +116 -0
  203. package/dist/services/profiler/profile-filter.cjs.map +1 -0
  204. package/dist/services/profiler/profile-filter.d.cts +20 -0
  205. package/dist/services/profiler/profile-filter.d.cts.map +1 -0
  206. package/dist/services/profiler/profile-filter.d.ts +20 -0
  207. package/dist/services/profiler/profile-filter.d.ts.map +1 -0
  208. package/dist/services/profiler/profile-filter.js +112 -0
  209. package/dist/services/profiler/profile-filter.js.map +1 -0
  210. package/dist/services/profiler/profile-parser.cjs +139 -0
  211. package/dist/services/profiler/profile-parser.cjs.map +1 -0
  212. package/dist/services/profiler/profile-parser.d.cts +18 -0
  213. package/dist/services/profiler/profile-parser.d.cts.map +1 -0
  214. package/dist/services/profiler/profile-parser.d.ts +18 -0
  215. package/dist/services/profiler/profile-parser.d.ts.map +1 -0
  216. package/dist/services/profiler/profile-parser.js +132 -0
  217. package/dist/services/profiler/profile-parser.js.map +1 -0
  218. package/dist/services/profiler/profile-runner.cjs +90 -0
  219. package/dist/services/profiler/profile-runner.cjs.map +1 -0
  220. package/dist/services/profiler/profile-runner.d.cts +29 -0
  221. package/dist/services/profiler/profile-runner.d.cts.map +1 -0
  222. package/dist/services/profiler/profile-runner.d.ts +29 -0
  223. package/dist/services/profiler/profile-runner.d.ts.map +1 -0
  224. package/dist/services/profiler/profile-runner.js +86 -0
  225. package/dist/services/profiler/profile-runner.js.map +1 -0
  226. package/dist/services/progress-manager.cjs +10 -2
  227. package/dist/services/progress-manager.cjs.map +1 -1
  228. package/dist/services/progress-manager.d.cts +2 -0
  229. package/dist/services/progress-manager.d.cts.map +1 -1
  230. package/dist/services/progress-manager.d.ts +2 -0
  231. package/dist/services/progress-manager.d.ts.map +1 -1
  232. package/dist/services/progress-manager.js +10 -2
  233. package/dist/services/progress-manager.js.map +1 -1
  234. package/dist/services/reporter-registry.cjs +18 -24
  235. package/dist/services/reporter-registry.cjs.map +1 -1
  236. package/dist/services/reporter-registry.d.cts +18 -40
  237. package/dist/services/reporter-registry.d.cts.map +1 -1
  238. package/dist/services/reporter-registry.d.ts +18 -40
  239. package/dist/services/reporter-registry.d.ts.map +1 -1
  240. package/dist/services/reporter-registry.js +18 -24
  241. package/dist/services/reporter-registry.js.map +1 -1
  242. package/dist/types/budgets.cjs +8 -0
  243. package/dist/types/budgets.cjs.map +1 -0
  244. package/dist/types/budgets.d.cts +149 -0
  245. package/dist/types/budgets.d.cts.map +1 -0
  246. package/dist/types/budgets.d.ts +149 -0
  247. package/dist/types/budgets.d.ts.map +1 -0
  248. package/dist/types/budgets.js +7 -0
  249. package/dist/types/budgets.js.map +1 -0
  250. package/dist/types/cli.cjs +2 -11
  251. package/dist/types/cli.cjs.map +1 -1
  252. package/dist/types/cli.d.cts +3 -227
  253. package/dist/types/cli.d.cts.map +1 -1
  254. package/dist/types/cli.d.ts +3 -227
  255. package/dist/types/cli.d.ts.map +1 -1
  256. package/dist/types/cli.js +2 -11
  257. package/dist/types/cli.js.map +1 -1
  258. package/dist/types/core.cjs +6 -1
  259. package/dist/types/core.cjs.map +1 -1
  260. package/dist/types/core.d.cts +15 -2
  261. package/dist/types/core.d.cts.map +1 -1
  262. package/dist/types/core.d.ts +15 -2
  263. package/dist/types/core.d.ts.map +1 -1
  264. package/dist/types/core.js +2 -1
  265. package/dist/types/core.js.map +1 -1
  266. package/dist/types/index.cjs +5 -0
  267. package/dist/types/index.cjs.map +1 -1
  268. package/dist/types/index.d.cts +2 -0
  269. package/dist/types/index.d.cts.map +1 -1
  270. package/dist/types/index.d.ts +2 -0
  271. package/dist/types/index.d.ts.map +1 -1
  272. package/dist/types/index.js +2 -0
  273. package/dist/types/index.js.map +1 -1
  274. package/dist/types/interfaces.d.cts +19 -8
  275. package/dist/types/interfaces.d.cts.map +1 -1
  276. package/dist/types/interfaces.d.ts +19 -8
  277. package/dist/types/interfaces.d.ts.map +1 -1
  278. package/dist/types/profiler.cjs +11 -0
  279. package/dist/types/profiler.cjs.map +1 -0
  280. package/dist/types/profiler.d.cts +102 -0
  281. package/dist/types/profiler.d.cts.map +1 -0
  282. package/dist/types/profiler.d.ts +102 -0
  283. package/dist/types/profiler.d.ts.map +1 -0
  284. package/dist/types/profiler.js +10 -0
  285. package/dist/types/profiler.js.map +1 -0
  286. package/dist/types/utility.cjs.map +1 -1
  287. package/dist/types/utility.d.cts +0 -8
  288. package/dist/types/utility.d.cts.map +1 -1
  289. package/dist/types/utility.d.ts +0 -8
  290. package/dist/types/utility.d.ts.map +1 -1
  291. package/dist/types/utility.js.map +1 -1
  292. package/dist/utils/identifiers.cjs +32 -0
  293. package/dist/utils/identifiers.cjs.map +1 -0
  294. package/dist/utils/identifiers.d.cts +32 -0
  295. package/dist/utils/identifiers.d.cts.map +1 -0
  296. package/dist/utils/identifiers.d.ts +32 -0
  297. package/dist/utils/identifiers.d.ts.map +1 -0
  298. package/dist/utils/identifiers.js +27 -0
  299. package/dist/utils/identifiers.js.map +1 -0
  300. package/dist/utils/package.cjs +40 -0
  301. package/dist/utils/package.cjs.map +1 -0
  302. package/dist/utils/package.d.cts +15 -0
  303. package/dist/utils/package.d.cts.map +1 -0
  304. package/dist/utils/package.d.ts +15 -0
  305. package/dist/utils/package.d.ts.map +1 -0
  306. package/dist/utils/package.js +33 -0
  307. package/dist/utils/package.js.map +1 -0
  308. package/dist/utils/type-guards.cjs +48 -0
  309. package/dist/utils/type-guards.cjs.map +1 -0
  310. package/dist/utils/type-guards.d.cts +22 -0
  311. package/dist/utils/type-guards.d.cts.map +1 -0
  312. package/dist/utils/type-guards.d.ts +22 -0
  313. package/dist/utils/type-guards.d.ts.map +1 -0
  314. package/dist/utils/type-guards.js +43 -0
  315. package/dist/utils/type-guards.js.map +1 -0
  316. package/package.json +18 -19
  317. package/src/cli/commands/analyze.ts +101 -0
  318. package/src/cli/commands/baseline.ts +577 -0
  319. package/src/cli/commands/history.ts +1 -1
  320. package/src/cli/commands/init.ts +116 -194
  321. package/src/cli/commands/run.ts +183 -113
  322. package/src/cli/index.ts +425 -183
  323. package/src/config/budget-schema.ts +189 -0
  324. package/src/config/schema.ts +260 -1
  325. package/src/constants.ts +53 -1
  326. package/src/core/engine.ts +169 -22
  327. package/src/core/engines/accurate-engine.ts +195 -44
  328. package/src/core/engines/tinybench-engine.ts +3 -2
  329. package/src/core/output-path-resolver.ts +10 -2
  330. package/src/errors/base.ts +11 -2
  331. package/src/errors/budget.ts +38 -0
  332. package/src/errors/index.ts +3 -0
  333. package/src/index.ts +9 -0
  334. package/src/reporters/csv.ts +54 -25
  335. package/src/reporters/human.ts +434 -115
  336. package/src/reporters/json.ts +26 -71
  337. package/src/reporters/profile-human.ts +210 -0
  338. package/src/reporters/simple.ts +88 -54
  339. package/src/services/baseline-storage.ts +199 -0
  340. package/src/services/budget-evaluator.ts +182 -0
  341. package/src/services/config-manager.ts +24 -9
  342. package/src/services/file-loader.ts +3 -6
  343. package/src/services/profiler/profile-filter.ts +147 -0
  344. package/src/services/profiler/profile-parser.ts +194 -0
  345. package/src/services/profiler/profile-runner.ts +121 -0
  346. package/src/services/progress-manager.ts +12 -2
  347. package/src/services/reporter-registry.ts +46 -81
  348. package/src/types/budgets.ts +180 -0
  349. package/src/types/cli.ts +5 -238
  350. package/src/types/core.ts +52 -10
  351. package/src/types/index.ts +5 -0
  352. package/src/types/interfaces.ts +24 -6
  353. package/src/types/profiler.ts +135 -0
  354. package/src/types/utility.ts +0 -10
  355. package/src/utils/identifiers.ts +58 -0
  356. package/src/utils/package.ts +35 -0
  357. package/src/utils/type-guards.ts +51 -0
@@ -0,0 +1,189 @@
1
+ import { z } from 'zod';
2
+
3
+ import type { BaselineStorage } from '../types/budgets.js';
4
+
5
+ /**
6
+ * Zod schema for budget configuration
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ /**
12
+ * Parse time string to nanoseconds Supports: "10ms", "5s", "100us", "50ns"
13
+ */
14
+ export const parseTimeString = (value: string): number => {
15
+ const match = value.match(/^(\d+(?:\.\d+)?)(ns|us|ms|s)$/i);
16
+ if (!match) {
17
+ throw new Error(
18
+ `Invalid time format: "${value}". Expected format like "10ms", "5s", "100us", "50ns"`,
19
+ );
20
+ }
21
+
22
+ const num = parseFloat(match[1]!);
23
+ const unit = match[2]!.toLowerCase();
24
+
25
+ const multipliers = {
26
+ ms: 1_000_000,
27
+ ns: 1,
28
+ s: 1_000_000_000,
29
+ us: 1_000,
30
+ };
31
+
32
+ return num * multipliers[unit as keyof typeof multipliers];
33
+ };
34
+
35
+ /**
36
+ * Parse percentage string to decimal Supports: "10%", "5.5%"
37
+ *
38
+ * Note: Does not round result - preserves full precision from input
39
+ */
40
+ export const parsePercentageString = (value: string): number => {
41
+ const match = value.match(/^(\d+(?:\.\d+)?)%$/);
42
+ if (!match) {
43
+ throw new Error(
44
+ `Invalid percentage format: "${value}". Expected format like "10%", "5.5%"`,
45
+ );
46
+ }
47
+
48
+ return parseFloat(match[1]!) / 100;
49
+ };
50
+
51
+ /**
52
+ * Time or nanoseconds
53
+ */
54
+ const timeSchema = z
55
+ .union([
56
+ z.number().int().nonnegative().describe('Time in nanoseconds'),
57
+ z
58
+ .string()
59
+ .transform(parseTimeString)
60
+ .describe('Time string (e.g., "10ms")'),
61
+ ])
62
+ .describe('Time value as nanoseconds or time string');
63
+
64
+ /**
65
+ * Percentage or decimal
66
+ */
67
+ const percentageSchema = z
68
+ .union([
69
+ z
70
+ .number()
71
+ .min(0)
72
+ .max(1)
73
+ .describe('Percentage as decimal (e.g., 0.1 for 10%)'),
74
+ z
75
+ .string()
76
+ .transform(parsePercentageString)
77
+ .describe('Percentage string (e.g., "10%")'),
78
+ ])
79
+ .describe('Percentage value as decimal or percentage string');
80
+
81
+ /**
82
+ * Absolute budget schema
83
+ */
84
+ const absoluteBudgetSchema = z
85
+ .object({
86
+ maxP99: timeSchema
87
+ .optional()
88
+ .describe('Maximum 99th percentile execution time'),
89
+ maxTime: timeSchema.optional().describe('Maximum mean execution time'),
90
+ minOpsPerSec: z
91
+ .number()
92
+ .positive()
93
+ .optional()
94
+ .describe('Minimum operations per second'),
95
+ })
96
+ .describe('Absolute performance budget thresholds');
97
+
98
+ /**
99
+ * Relative budget schema
100
+ */
101
+ const relativeBudgetSchema = z
102
+ .object({
103
+ baseline: z
104
+ .string()
105
+ .optional()
106
+ .describe('Name of baseline to compare against'),
107
+ maxRegression: percentageSchema
108
+ .optional()
109
+ .describe('Maximum allowed performance regression'),
110
+ })
111
+ .describe('Relative performance budget thresholds compared to baseline');
112
+
113
+ /**
114
+ * Complete budget schema
115
+ */
116
+ export const budgetSchema = z
117
+ .object({
118
+ absolute: absoluteBudgetSchema
119
+ .optional()
120
+ .describe('Absolute performance thresholds'),
121
+ relative: relativeBudgetSchema
122
+ .optional()
123
+ .describe('Relative performance thresholds'),
124
+ })
125
+ .describe('Performance budget configuration');
126
+
127
+ /**
128
+ * Baseline reference schema
129
+ *
130
+ * Note: This validates and transforms to branded types (RunId, TaskId). The
131
+ * transforms are safe as they only add compile-time type information.
132
+ */
133
+ const baselineReferenceSchema = z
134
+ .object({
135
+ branch: z.string().optional().describe('Git branch name'),
136
+ commit: z
137
+ .string()
138
+ .length(40)
139
+ .regex(/^[0-9a-f]{40}$/)
140
+ .optional()
141
+ .describe('Full Git commit hash (40 hex characters)'),
142
+ date: z.coerce.date().describe('Date baseline was created'),
143
+ name: z.string().describe('Baseline name identifier'),
144
+ runId: z
145
+ .string()
146
+ .length(7)
147
+ .regex(/^[0-9a-z]{7}$/)
148
+ .describe('Benchmark run ID (7 lowercase alphanumeric characters)'),
149
+ summary: z
150
+ .record(
151
+ z.string(),
152
+ z.object({
153
+ mean: z.number().describe('Mean execution time in nanoseconds'),
154
+ opsPerSecond: z.number().describe('Operations per second'),
155
+ p99: z
156
+ .number()
157
+ .optional()
158
+ .describe('99th percentile execution time in nanoseconds'),
159
+ }),
160
+ )
161
+ .describe('Summary of benchmark results for each task'),
162
+ })
163
+ .describe('Named baseline reference with benchmark results summary');
164
+
165
+ /**
166
+ * Baseline storage schema
167
+ */
168
+ export const baselineStorageSchema = z
169
+ .object({
170
+ baselines: z
171
+ .record(z.string(), baselineReferenceSchema)
172
+ .describe('Map of baseline names to baseline references'),
173
+ default: z.string().optional().describe('Default baseline name'),
174
+ version: z.string().describe('Schema version'),
175
+ })
176
+ .describe('Baseline storage file format');
177
+
178
+ /**
179
+ * Validate baseline storage
180
+ *
181
+ * Note: The parsed data contains plain strings that are cast to branded types
182
+ * (RunId, TaskId). This is safe because branded types are compile-time only
183
+ * constructs.
184
+ */
185
+ export const validateBaselineStorage = (storage: unknown): BaselineStorage => {
186
+ const parsed = baselineStorageSchema.parse(storage);
187
+ // Cast is safe: branded types are erased at runtime
188
+ return parsed as unknown as BaselineStorage;
189
+ };
@@ -11,6 +11,7 @@ import * as z from 'zod';
11
11
  import type { ModestBenchConfig } from '../types/core.js';
12
12
 
13
13
  import { BENCHMARK_FILE_PATTERN } from '../constants.js';
14
+ import { parsePercentageString, parseTimeString } from './budget-schema.js';
14
15
 
15
16
  /**
16
17
  * Schema for threshold configuration
@@ -56,6 +57,46 @@ const thresholdConfigSchema = z
56
57
  title: 'Threshold Configuration',
57
58
  });
58
59
 
60
+ /**
61
+ * Inline budget schema for configuration (no transforms for JSON Schema
62
+ * compatibility - transforms are applied manually in transformBudgets
63
+ * function)
64
+ */
65
+ const budgetSchema = z
66
+ .object({
67
+ absolute: z
68
+ .object({
69
+ maxP99: z
70
+ .union([z.number().positive(), z.string()])
71
+ .optional()
72
+ .describe('Maximum 99th percentile in nanoseconds or time string'),
73
+ maxTime: z
74
+ .union([z.number().positive(), z.string()])
75
+ .describe(
76
+ 'Maximum mean time in nanoseconds or time string (e.g., "10ms")',
77
+ ),
78
+ minOpsPerSec: z
79
+ .number()
80
+ .positive()
81
+ .optional()
82
+ .describe('Minimum operations per second'),
83
+ })
84
+ .optional()
85
+ .describe('Absolute performance thresholds'),
86
+ relative: z
87
+ .object({
88
+ maxRegression: z
89
+ .union([z.number().min(0).max(1), z.string()])
90
+ .optional()
91
+ .describe(
92
+ 'Maximum regression as decimal (0.1) or percentage string ("10%")',
93
+ ),
94
+ })
95
+ .optional()
96
+ .describe('Relative performance thresholds vs baseline'),
97
+ })
98
+ .describe('Performance budget with absolute and/or relative thresholds');
99
+
59
100
  /**
60
101
  * Schema for the main ModestBench configuration
61
102
  *
@@ -71,6 +112,27 @@ const modestBenchConfigSchema = z
71
112
  'JSON Schema reference for IDE support (not used by ModestBench)',
72
113
  ),
73
114
  bail: z.boolean().describe('Stop benchmark execution on first failure'),
115
+ baseline: z
116
+ .string()
117
+ .optional()
118
+ .describe(
119
+ 'Name of baseline to use for relative budget comparisons. Must match a saved baseline name.',
120
+ ),
121
+ budgetMode: z
122
+ .enum(['fail', 'warn', 'report'])
123
+ .optional()
124
+ .describe(
125
+ 'How to handle budget violations: "fail" exits with error (default), "warn" shows warnings, "report" includes in output without failing',
126
+ ),
127
+ budgets: z
128
+ .record(
129
+ z.string(),
130
+ z.record(z.string(), z.record(z.string(), budgetSchema)),
131
+ )
132
+ .optional()
133
+ .describe(
134
+ 'Performance budgets organized by file → suite → task. Budgets define acceptable performance thresholds.',
135
+ ),
74
136
  exclude: z
75
137
  .array(z.string())
76
138
  .describe(
@@ -109,6 +171,55 @@ const modestBenchConfigSchema = z
109
171
  .describe(
110
172
  `Glob pattern(s) for discovering benchmark files. Can be a single pattern string or array of patterns (e.g., "**/*${BENCHMARK_FILE_PATTERN}")`,
111
173
  ),
174
+ profile: z
175
+ .object({
176
+ exclude: z
177
+ .array(z.string())
178
+ .optional()
179
+ .describe('Glob patterns to exclude from profiling results'),
180
+ focus: z
181
+ .array(z.string())
182
+ .optional()
183
+ .describe(
184
+ 'Glob patterns to focus on in profiling results. If specified, only matching files will be shown',
185
+ ),
186
+ minCallCount: z
187
+ .number()
188
+ .int()
189
+ .nonnegative()
190
+ .optional()
191
+ .describe(
192
+ 'Minimum number of times a function must be called to be included in results',
193
+ ),
194
+ minExecutionPercent: z
195
+ .number()
196
+ .nonnegative()
197
+ .max(100)
198
+ .default(1.0)
199
+ .describe(
200
+ 'Minimum execution percentage threshold for including functions in results',
201
+ ),
202
+ outputFile: z
203
+ .string()
204
+ .optional()
205
+ .describe('Path to write profile report to file'),
206
+ smartDetection: z
207
+ .boolean()
208
+ .default(true)
209
+ .describe(
210
+ 'Automatically detect and focus on user code, excluding node_modules and Node.js internals',
211
+ ),
212
+ topN: z
213
+ .number()
214
+ .int()
215
+ .positive()
216
+ .default(25)
217
+ .describe('Maximum number of top functions to show in results'),
218
+ })
219
+ .optional()
220
+ .describe(
221
+ 'Configuration for profile command to identify benchmark candidates',
222
+ ),
112
223
  quiet: z
113
224
  .boolean()
114
225
  .describe(
@@ -176,6 +287,136 @@ export const partialModestBenchConfigSchema: z.ZodType<
176
287
  Partial<ModestBenchConfig>
177
288
  > = modestBenchConfigSchema.partial();
178
289
 
290
+ /**
291
+ * Input budget type (before transformation)
292
+ */
293
+ interface BudgetInput {
294
+ absolute?: {
295
+ maxP99?: number | string;
296
+ maxTime?: number | string;
297
+ minOpsPerSec?: number;
298
+ };
299
+ relative?: {
300
+ maxRegression?: number | string;
301
+ };
302
+ }
303
+
304
+ /**
305
+ * Output budget type (after transformation)
306
+ */
307
+ interface BudgetOutput {
308
+ absolute?: {
309
+ maxP99?: number;
310
+ maxTime?: number;
311
+ minOpsPerSec?: number;
312
+ };
313
+ relative?: {
314
+ maxRegression?: number;
315
+ };
316
+ }
317
+
318
+ /**
319
+ * Transform budget values (parse time/percentage strings)
320
+ */
321
+ const transformBudgetValues = (budget: BudgetInput): BudgetOutput => {
322
+ const transformed: BudgetOutput = {};
323
+
324
+ if (budget.absolute) {
325
+ transformed.absolute = {};
326
+
327
+ // Copy minOpsPerSec as-is (already a number)
328
+ if (budget.absolute.minOpsPerSec !== undefined) {
329
+ transformed.absolute.minOpsPerSec = budget.absolute.minOpsPerSec;
330
+ }
331
+
332
+ // Parse time strings
333
+ if (budget.absolute.maxTime !== undefined) {
334
+ transformed.absolute.maxTime =
335
+ typeof budget.absolute.maxTime === 'string'
336
+ ? parseTimeString(budget.absolute.maxTime)
337
+ : budget.absolute.maxTime;
338
+ }
339
+ if (budget.absolute.maxP99 !== undefined) {
340
+ transformed.absolute.maxP99 =
341
+ typeof budget.absolute.maxP99 === 'string'
342
+ ? parseTimeString(budget.absolute.maxP99)
343
+ : budget.absolute.maxP99;
344
+ }
345
+ }
346
+
347
+ if (budget.relative) {
348
+ transformed.relative = {};
349
+
350
+ // Parse percentage strings
351
+ if (budget.relative.maxRegression !== undefined) {
352
+ transformed.relative.maxRegression =
353
+ typeof budget.relative.maxRegression === 'string'
354
+ ? parsePercentageString(budget.relative.maxRegression)
355
+ : budget.relative.maxRegression;
356
+ }
357
+ }
358
+
359
+ return transformed;
360
+ };
361
+
362
+ /**
363
+ * Transform nested budget structure to flat TaskId → Budget mapping Also parses
364
+ * time and percentage strings
365
+ *
366
+ * @internal
367
+ */
368
+ const transformBudgets = (
369
+ nested: Record<string, Record<string, Record<string, unknown>>> | undefined,
370
+ ): Record<string, unknown> | undefined => {
371
+ if (!nested) {
372
+ return undefined;
373
+ }
374
+
375
+ const flat: Record<string, unknown> = {};
376
+
377
+ for (const [file, suites] of Object.entries(nested)) {
378
+ for (const [suite, tasks] of Object.entries(suites)) {
379
+ for (const [task, budget] of Object.entries(tasks)) {
380
+ const taskId = `${file}/${suite}/${task}`;
381
+ // Transform budget values (parse strings)
382
+ flat[taskId] = transformBudgetValues(budget as BudgetInput);
383
+ }
384
+ }
385
+ }
386
+
387
+ return flat;
388
+ };
389
+
390
+ /**
391
+ * Safely parse and validate a partial configuration object with budget
392
+ * transformation
393
+ *
394
+ * @param config - The configuration object to validate
395
+ * @returns A result object with either success: true and data, or success:
396
+ * false and error
397
+ */
398
+ export const safeParsePartialConfig = (config: unknown) => {
399
+ const result = partialModestBenchConfigSchema.safeParse(config);
400
+
401
+ // Transform nested budgets to flat structure after validation
402
+ if (result.success && result.data.budgets) {
403
+ return {
404
+ ...result,
405
+ data: {
406
+ ...result.data,
407
+ budgets: transformBudgets(
408
+ result.data.budgets as Record<
409
+ string,
410
+ Record<string, Record<string, unknown>>
411
+ >,
412
+ ),
413
+ },
414
+ };
415
+ }
416
+
417
+ return result;
418
+ };
419
+
179
420
  /**
180
421
  * Safely parse and validate a configuration object
181
422
  *
@@ -184,5 +425,23 @@ export const partialModestBenchConfigSchema: z.ZodType<
184
425
  * false and error
185
426
  */
186
427
  export const safeParseConfig = (config: unknown) => {
187
- return modestBenchConfigSchema.safeParse(config);
428
+ const result = modestBenchConfigSchema.safeParse(config);
429
+
430
+ // Transform nested budgets to flat structure after validation
431
+ if (result.success && result.data.budgets) {
432
+ return {
433
+ ...result,
434
+ data: {
435
+ ...result.data,
436
+ budgets: transformBudgets(
437
+ result.data.budgets as Record<
438
+ string,
439
+ Record<string, Record<string, unknown>>
440
+ >,
441
+ ),
442
+ },
443
+ };
444
+ }
445
+
446
+ return result;
188
447
  };
package/src/constants.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { type Engine } from './types/cli.js';
2
+
1
3
  /**
2
4
  * Supported benchmark file extensions
3
5
  */
@@ -20,21 +22,71 @@ export const BENCHMARK_FILE_PATTERN = `.bench.{${Array.from(
20
22
  .map((ext) => ext.slice(1))
21
23
  .join(',')}}`;
22
24
 
25
+ /**
26
+ * Timeout before we force-quit when aborting benchmarks (ms)
27
+ */
28
+ export const ABORT_TIMEOUT = 500;
29
+
30
+ /**
31
+ * Exit codes for the CLI
32
+ */
33
+ export const ExitCodes = {
34
+ BENCHMARK_FAILURES: 1,
35
+ CONFIG_ERROR: 2,
36
+ DISCOVERY_ERROR: 3,
37
+ RUNTIME_ERROR: 5,
38
+ SUCCESS: 0,
39
+ UNKNOWN_ERROR: 99,
40
+ VALIDATION_ERROR: 4,
41
+ } as const;
42
+
43
+ /**
44
+ * Supported benchmark engines
45
+ */
46
+ export const Engines = {
47
+ ACCURATE: 'accurate',
48
+ TINYBENCH: 'tinybench',
49
+ } as const satisfies Record<string, Engine>;
50
+
51
+ /**
52
+ * Default benchmark engine
53
+ */
54
+ export const DEFAULT_ENGINE = Engines.TINYBENCH;
55
+
56
+ /**
57
+ * Supported reporters
58
+ */
59
+ export const Reporters = {
60
+ CSV: 'csv',
61
+ HUMAN: 'human',
62
+ JSON: 'json',
63
+ SIMPLE: 'simple',
64
+ } as const;
65
+
66
+ /**
67
+ * Default reporter
68
+ */
69
+ export const DEFAULT_REPORTER = Reporters.HUMAN;
70
+
23
71
  /**
24
72
  * Error codes for all ModestBench errors
25
73
  *
26
74
  * Use these constants to check error types instead of instanceof checks.
27
75
  */
28
76
  export const ErrorCodes = {
77
+ //#region budget-errors
78
+ BUDGET_EXCEEDED: 'ERR_MB_BUDGET_EXCEEDED',
29
79
  //#region cli-errors
30
80
  CLI_INVALID_ARGUMENT: 'ERR_MB_CLI_INVALID_ARGUMENT',
31
- CLI_INVALID_DATE_FORMAT: 'ERR_MB_CLI_INVALID_DATE_FORMAT',
32
81
  //#endregion
33
82
 
83
+ CLI_INVALID_DATE_FORMAT: 'ERR_MB_CLI_INVALID_DATE_FORMAT',
34
84
  //#region config-errors
35
85
  CONFIG_LOAD_FAILED: 'ERR_MB_CONFIG_LOAD_FAILED',
36
86
  CONFIG_NOT_FOUND: 'ERR_MB_CONFIG_NOT_FOUND',
37
87
  CONFIG_UNSUPPORTED_FORMAT: 'ERR_MB_CONFIG_UNSUPPORTED_FORMAT',
88
+ //#endregion
89
+
38
90
  CONFIG_VALIDATION_FAILED: 'ERR_MB_CONFIG_VALIDATION_FAILED',
39
91
  //#endregion
40
92