modestbench 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/CHANGELOG.md +20 -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 +88 -155
  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 +88 -155
  30. package/dist/cli/commands/init.js.map +1 -1
  31. package/dist/cli/commands/run.cjs +132 -114
  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 +131 -80
  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 +103 -21
  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 +104 -22
  78. package/dist/core/engine.js.map +1 -1
  79. package/dist/core/output-path-resolver.cjs +8 -1
  80. package/dist/core/output-path-resolver.cjs.map +1 -1
  81. package/dist/core/output-path-resolver.d.cts.map +1 -1
  82. package/dist/core/output-path-resolver.d.ts.map +1 -1
  83. package/dist/core/output-path-resolver.js +9 -2
  84. package/dist/core/output-path-resolver.js.map +1 -1
  85. package/dist/errors/base.cjs +12 -3
  86. package/dist/errors/base.cjs.map +1 -1
  87. package/dist/errors/base.d.cts +7 -0
  88. package/dist/errors/base.d.cts.map +1 -1
  89. package/dist/errors/base.d.ts +7 -0
  90. package/dist/errors/base.d.ts.map +1 -1
  91. package/dist/errors/base.js +10 -2
  92. package/dist/errors/base.js.map +1 -1
  93. package/dist/errors/budget.cjs +37 -0
  94. package/dist/errors/budget.cjs.map +1 -0
  95. package/dist/errors/budget.d.cts +31 -0
  96. package/dist/errors/budget.d.cts.map +1 -0
  97. package/dist/errors/budget.d.ts +31 -0
  98. package/dist/errors/budget.d.ts.map +1 -0
  99. package/dist/errors/budget.js +33 -0
  100. package/dist/errors/budget.js.map +1 -0
  101. package/dist/errors/index.cjs +4 -1
  102. package/dist/errors/index.cjs.map +1 -1
  103. package/dist/errors/index.d.cts +1 -0
  104. package/dist/errors/index.d.cts.map +1 -1
  105. package/dist/errors/index.d.ts +1 -0
  106. package/dist/errors/index.d.ts.map +1 -1
  107. package/dist/errors/index.js +2 -0
  108. package/dist/errors/index.js.map +1 -1
  109. package/dist/index.cjs +13 -1
  110. package/dist/index.cjs.map +1 -1
  111. package/dist/index.d.cts +5 -0
  112. package/dist/index.d.cts.map +1 -1
  113. package/dist/index.d.ts +5 -0
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +7 -0
  116. package/dist/index.js.map +1 -1
  117. package/dist/reporters/csv.cjs +37 -17
  118. package/dist/reporters/csv.cjs.map +1 -1
  119. package/dist/reporters/csv.d.cts +3 -6
  120. package/dist/reporters/csv.d.cts.map +1 -1
  121. package/dist/reporters/csv.d.ts +3 -6
  122. package/dist/reporters/csv.d.ts.map +1 -1
  123. package/dist/reporters/csv.js +37 -17
  124. package/dist/reporters/csv.js.map +1 -1
  125. package/dist/reporters/human.cjs +66 -40
  126. package/dist/reporters/human.cjs.map +1 -1
  127. package/dist/reporters/human.d.cts +14 -13
  128. package/dist/reporters/human.d.cts.map +1 -1
  129. package/dist/reporters/human.d.ts +14 -13
  130. package/dist/reporters/human.d.ts.map +1 -1
  131. package/dist/reporters/human.js +66 -40
  132. package/dist/reporters/human.js.map +1 -1
  133. package/dist/reporters/json.cjs +23 -48
  134. package/dist/reporters/json.cjs.map +1 -1
  135. package/dist/reporters/json.d.cts +2 -28
  136. package/dist/reporters/json.d.cts.map +1 -1
  137. package/dist/reporters/json.d.ts +2 -28
  138. package/dist/reporters/json.d.ts.map +1 -1
  139. package/dist/reporters/json.js +25 -50
  140. package/dist/reporters/json.js.map +1 -1
  141. package/dist/reporters/profile-human.cjs +149 -0
  142. package/dist/reporters/profile-human.cjs.map +1 -0
  143. package/dist/reporters/profile-human.d.cts +44 -0
  144. package/dist/reporters/profile-human.d.cts.map +1 -0
  145. package/dist/reporters/profile-human.d.ts +44 -0
  146. package/dist/reporters/profile-human.d.ts.map +1 -0
  147. package/dist/reporters/profile-human.js +142 -0
  148. package/dist/reporters/profile-human.js.map +1 -0
  149. package/dist/reporters/simple.cjs +64 -44
  150. package/dist/reporters/simple.cjs.map +1 -1
  151. package/dist/reporters/simple.d.cts +14 -14
  152. package/dist/reporters/simple.d.cts.map +1 -1
  153. package/dist/reporters/simple.d.ts +14 -14
  154. package/dist/reporters/simple.d.ts.map +1 -1
  155. package/dist/reporters/simple.js +64 -44
  156. package/dist/reporters/simple.js.map +1 -1
  157. package/dist/schema/modestbench-config.schema.json +153 -0
  158. package/dist/services/baseline-storage.cjs +151 -0
  159. package/dist/services/baseline-storage.cjs.map +1 -0
  160. package/dist/services/baseline-storage.d.cts +55 -0
  161. package/dist/services/baseline-storage.d.cts.map +1 -0
  162. package/dist/services/baseline-storage.d.ts +55 -0
  163. package/dist/services/baseline-storage.d.ts.map +1 -0
  164. package/dist/services/baseline-storage.js +147 -0
  165. package/dist/services/baseline-storage.js.map +1 -0
  166. package/dist/services/budget-evaluator.cjs +146 -0
  167. package/dist/services/budget-evaluator.cjs.map +1 -0
  168. package/dist/services/budget-evaluator.d.cts +29 -0
  169. package/dist/services/budget-evaluator.d.cts.map +1 -0
  170. package/dist/services/budget-evaluator.d.ts +29 -0
  171. package/dist/services/budget-evaluator.d.ts.map +1 -0
  172. package/dist/services/budget-evaluator.js +142 -0
  173. package/dist/services/budget-evaluator.js.map +1 -0
  174. package/dist/services/config-manager.cjs +23 -9
  175. package/dist/services/config-manager.cjs.map +1 -1
  176. package/dist/services/config-manager.d.cts +6 -1
  177. package/dist/services/config-manager.d.cts.map +1 -1
  178. package/dist/services/config-manager.d.ts +6 -1
  179. package/dist/services/config-manager.d.ts.map +1 -1
  180. package/dist/services/config-manager.js +23 -9
  181. package/dist/services/config-manager.js.map +1 -1
  182. package/dist/services/file-loader.cjs +3 -6
  183. package/dist/services/file-loader.cjs.map +1 -1
  184. package/dist/services/file-loader.d.cts.map +1 -1
  185. package/dist/services/file-loader.d.ts.map +1 -1
  186. package/dist/services/file-loader.js +3 -6
  187. package/dist/services/file-loader.js.map +1 -1
  188. package/dist/services/profiler/profile-filter.cjs +113 -0
  189. package/dist/services/profiler/profile-filter.cjs.map +1 -0
  190. package/dist/services/profiler/profile-filter.d.cts +20 -0
  191. package/dist/services/profiler/profile-filter.d.cts.map +1 -0
  192. package/dist/services/profiler/profile-filter.d.ts +20 -0
  193. package/dist/services/profiler/profile-filter.d.ts.map +1 -0
  194. package/dist/services/profiler/profile-filter.js +109 -0
  195. package/dist/services/profiler/profile-filter.js.map +1 -0
  196. package/dist/services/profiler/profile-parser.cjs +139 -0
  197. package/dist/services/profiler/profile-parser.cjs.map +1 -0
  198. package/dist/services/profiler/profile-parser.d.cts +18 -0
  199. package/dist/services/profiler/profile-parser.d.cts.map +1 -0
  200. package/dist/services/profiler/profile-parser.d.ts +18 -0
  201. package/dist/services/profiler/profile-parser.d.ts.map +1 -0
  202. package/dist/services/profiler/profile-parser.js +132 -0
  203. package/dist/services/profiler/profile-parser.js.map +1 -0
  204. package/dist/services/profiler/profile-runner.cjs +90 -0
  205. package/dist/services/profiler/profile-runner.cjs.map +1 -0
  206. package/dist/services/profiler/profile-runner.d.cts +29 -0
  207. package/dist/services/profiler/profile-runner.d.cts.map +1 -0
  208. package/dist/services/profiler/profile-runner.d.ts +29 -0
  209. package/dist/services/profiler/profile-runner.d.ts.map +1 -0
  210. package/dist/services/profiler/profile-runner.js +86 -0
  211. package/dist/services/profiler/profile-runner.js.map +1 -0
  212. package/dist/services/reporter-registry.cjs +18 -24
  213. package/dist/services/reporter-registry.cjs.map +1 -1
  214. package/dist/services/reporter-registry.d.cts +18 -40
  215. package/dist/services/reporter-registry.d.cts.map +1 -1
  216. package/dist/services/reporter-registry.d.ts +18 -40
  217. package/dist/services/reporter-registry.d.ts.map +1 -1
  218. package/dist/services/reporter-registry.js +18 -24
  219. package/dist/services/reporter-registry.js.map +1 -1
  220. package/dist/types/budgets.cjs +8 -0
  221. package/dist/types/budgets.cjs.map +1 -0
  222. package/dist/types/budgets.d.cts +149 -0
  223. package/dist/types/budgets.d.cts.map +1 -0
  224. package/dist/types/budgets.d.ts +149 -0
  225. package/dist/types/budgets.d.ts.map +1 -0
  226. package/dist/types/budgets.js +7 -0
  227. package/dist/types/budgets.js.map +1 -0
  228. package/dist/types/cli.cjs +2 -11
  229. package/dist/types/cli.cjs.map +1 -1
  230. package/dist/types/cli.d.cts +3 -227
  231. package/dist/types/cli.d.cts.map +1 -1
  232. package/dist/types/cli.d.ts +3 -227
  233. package/dist/types/cli.d.ts.map +1 -1
  234. package/dist/types/cli.js +2 -11
  235. package/dist/types/cli.js.map +1 -1
  236. package/dist/types/core.cjs +6 -1
  237. package/dist/types/core.cjs.map +1 -1
  238. package/dist/types/core.d.cts +13 -2
  239. package/dist/types/core.d.cts.map +1 -1
  240. package/dist/types/core.d.ts +13 -2
  241. package/dist/types/core.d.ts.map +1 -1
  242. package/dist/types/core.js +2 -1
  243. package/dist/types/core.js.map +1 -1
  244. package/dist/types/index.cjs +5 -0
  245. package/dist/types/index.cjs.map +1 -1
  246. package/dist/types/index.d.cts +2 -0
  247. package/dist/types/index.d.cts.map +1 -1
  248. package/dist/types/index.d.ts +2 -0
  249. package/dist/types/index.d.ts.map +1 -1
  250. package/dist/types/index.js +2 -0
  251. package/dist/types/index.js.map +1 -1
  252. package/dist/types/interfaces.d.cts +15 -8
  253. package/dist/types/interfaces.d.cts.map +1 -1
  254. package/dist/types/interfaces.d.ts +15 -8
  255. package/dist/types/interfaces.d.ts.map +1 -1
  256. package/dist/types/profiler.cjs +11 -0
  257. package/dist/types/profiler.cjs.map +1 -0
  258. package/dist/types/profiler.d.cts +100 -0
  259. package/dist/types/profiler.d.cts.map +1 -0
  260. package/dist/types/profiler.d.ts +100 -0
  261. package/dist/types/profiler.d.ts.map +1 -0
  262. package/dist/types/profiler.js +10 -0
  263. package/dist/types/profiler.js.map +1 -0
  264. package/dist/types/utility.cjs.map +1 -1
  265. package/dist/types/utility.d.cts +0 -8
  266. package/dist/types/utility.d.cts.map +1 -1
  267. package/dist/types/utility.d.ts +0 -8
  268. package/dist/types/utility.d.ts.map +1 -1
  269. package/dist/types/utility.js.map +1 -1
  270. package/dist/utils/identifiers.cjs +32 -0
  271. package/dist/utils/identifiers.cjs.map +1 -0
  272. package/dist/utils/identifiers.d.cts +32 -0
  273. package/dist/utils/identifiers.d.cts.map +1 -0
  274. package/dist/utils/identifiers.d.ts +32 -0
  275. package/dist/utils/identifiers.d.ts.map +1 -0
  276. package/dist/utils/identifiers.js +27 -0
  277. package/dist/utils/identifiers.js.map +1 -0
  278. package/dist/utils/package.cjs +40 -0
  279. package/dist/utils/package.cjs.map +1 -0
  280. package/dist/utils/package.d.cts +15 -0
  281. package/dist/utils/package.d.cts.map +1 -0
  282. package/dist/utils/package.d.ts +15 -0
  283. package/dist/utils/package.d.ts.map +1 -0
  284. package/dist/utils/package.js +33 -0
  285. package/dist/utils/package.js.map +1 -0
  286. package/dist/utils/type-guards.cjs +48 -0
  287. package/dist/utils/type-guards.cjs.map +1 -0
  288. package/dist/utils/type-guards.d.cts +22 -0
  289. package/dist/utils/type-guards.d.cts.map +1 -0
  290. package/dist/utils/type-guards.d.ts +22 -0
  291. package/dist/utils/type-guards.d.ts.map +1 -0
  292. package/dist/utils/type-guards.js +43 -0
  293. package/dist/utils/type-guards.js.map +1 -0
  294. package/package.json +10 -10
  295. package/src/cli/commands/analyze.ts +101 -0
  296. package/src/cli/commands/baseline.ts +577 -0
  297. package/src/cli/commands/history.ts +1 -1
  298. package/src/cli/commands/init.ts +105 -183
  299. package/src/cli/commands/run.ts +167 -98
  300. package/src/cli/index.ts +425 -183
  301. package/src/config/budget-schema.ts +189 -0
  302. package/src/config/schema.ts +260 -1
  303. package/src/constants.ts +53 -1
  304. package/src/core/engine.ts +151 -20
  305. package/src/core/output-path-resolver.ts +10 -2
  306. package/src/errors/base.ts +11 -2
  307. package/src/errors/budget.ts +38 -0
  308. package/src/errors/index.ts +3 -0
  309. package/src/index.ts +9 -0
  310. package/src/reporters/csv.ts +54 -25
  311. package/src/reporters/human.ts +88 -47
  312. package/src/reporters/json.ts +26 -71
  313. package/src/reporters/profile-human.ts +204 -0
  314. package/src/reporters/simple.ts +84 -53
  315. package/src/services/baseline-storage.ts +199 -0
  316. package/src/services/budget-evaluator.ts +182 -0
  317. package/src/services/config-manager.ts +23 -8
  318. package/src/services/file-loader.ts +3 -6
  319. package/src/services/profiler/profile-filter.ts +143 -0
  320. package/src/services/profiler/profile-parser.ts +194 -0
  321. package/src/services/profiler/profile-runner.ts +121 -0
  322. package/src/services/reporter-registry.ts +46 -81
  323. package/src/types/budgets.ts +180 -0
  324. package/src/types/cli.ts +5 -238
  325. package/src/types/core.ts +50 -10
  326. package/src/types/index.ts +5 -0
  327. package/src/types/interfaces.ts +16 -6
  328. package/src/types/profiler.ts +132 -0
  329. package/src/types/utility.ts +0 -10
  330. package/src/utils/identifiers.ts +58 -0
  331. package/src/utils/package.ts +35 -0
  332. package/src/utils/type-guards.ts +51 -0
@@ -7,13 +7,17 @@
7
7
  */
8
8
 
9
9
  import { randomBytes } from 'node:crypto';
10
+ import { relative as pathRelative } from 'node:path';
10
11
 
11
12
  import type {
13
+ BaselineSummaryData,
12
14
  BenchmarkDefinition,
13
15
  BenchmarkEngine,
14
16
  BenchmarkRun,
15
17
  BenchmarkSuite,
16
18
  BenchmarkTask,
19
+ Budget,
20
+ BudgetSummary,
17
21
  CiInfo,
18
22
  ConfigurationManager,
19
23
  EnvironmentInfo,
@@ -26,7 +30,9 @@ import type {
26
30
  Reporter,
27
31
  ReporterRegistry,
28
32
  RunConfiguration,
33
+ RunId,
29
34
  SuiteResult,
35
+ TaskId,
30
36
  TaskResult,
31
37
  ValidationError,
32
38
  ValidationResult,
@@ -35,11 +41,15 @@ import type {
35
41
 
36
42
  import {
37
43
  BenchmarkExecutionError,
44
+ BudgetExceededError,
38
45
  FileDiscoveryError,
39
46
  SchemaValidationError,
40
47
  SetupError,
41
48
  StructureValidationError,
42
49
  } from '../errors/index.js';
50
+ import { BaselineStorageService } from '../services/baseline-storage.js';
51
+ import { BudgetEvaluator } from '../services/budget-evaluator.js';
52
+ import { createRunId, createTaskId } from '../types/index.js';
43
53
 
44
54
  /**
45
55
  * Dependencies required by the BenchmarkEngine
@@ -78,6 +88,20 @@ export abstract class ModestBenchEngine implements BenchmarkEngine {
78
88
  this.progressManager = dependencies.progressManager;
79
89
  }
80
90
 
91
+ /**
92
+ * Generate a unique run ID
93
+ *
94
+ * Uses crypto.randomBytes for cryptographically random 7-character IDs.
95
+ * Format: 7 lowercase alphanumeric characters (e.g., "k3m9x2p")
96
+ */
97
+ private static generateRunId(this: void): RunId {
98
+ // Generate random bytes, convert to hex, then to base36, take first 7 chars
99
+ const hex = randomBytes(4).toString('hex');
100
+ const num = parseInt(hex, 16);
101
+ const id = num.toString(36).padStart(7, '0').substring(0, 7);
102
+ return createRunId(id);
103
+ }
104
+
81
105
  /**
82
106
  * Discover benchmark files matching the pattern(s)
83
107
  */
@@ -136,7 +160,7 @@ export abstract class ModestBenchEngine implements BenchmarkEngine {
136
160
  }
137
161
 
138
162
  // 4. Initialize progress tracking
139
- const runId = this.generateRunId();
163
+ const runId = ModestBenchEngine.generateRunId();
140
164
 
141
165
  // Pre-calculate total tasks for progress tracking
142
166
  let totalTasks = 0;
@@ -212,9 +236,9 @@ export abstract class ModestBenchEngine implements BenchmarkEngine {
212
236
 
213
237
  // Register progress callbacks with reporters that support them
214
238
  for (const reporter of reporters) {
215
- if (typeof reporter.onProgress === 'function') {
239
+ if (reporter.onProgress) {
216
240
  this.progressManager.onProgress((state) => {
217
- void reporter.onProgress(state);
241
+ void reporter.onProgress?.(state);
218
242
  });
219
243
  }
220
244
  }
@@ -227,12 +251,17 @@ export abstract class ModestBenchEngine implements BenchmarkEngine {
227
251
 
228
252
  for (const filePath of files) {
229
253
  try {
230
- // Call reporter onFileStart
231
- await this.callReporters(reporters, 'onFileStart', filePath);
254
+ // Normalize file path to be relative to cwd
255
+ const cwd = config.cwd || process.cwd();
256
+ const relativePath = pathRelative(cwd, filePath);
257
+
258
+ // Call reporter onFileStart with relative path
259
+ await this.callReporters(reporters, 'onFileStart', relativePath);
232
260
 
233
261
  const fileResult = await this.executeBenchmarkFile(
234
262
  filePath,
235
263
  mergedConfig,
264
+ cwd,
236
265
  reporters,
237
266
  signal,
238
267
  );
@@ -246,6 +275,16 @@ export abstract class ModestBenchEngine implements BenchmarkEngine {
246
275
  currentFile: filePath,
247
276
  filesCompleted: fileResults.length,
248
277
  });
278
+
279
+ // Check for bail: stop execution if any task failed
280
+ if (mergedConfig.bail) {
281
+ const hasFailedTask = fileResult.suites.some((suite) =>
282
+ suite.tasks.some((task) => task.error),
283
+ );
284
+ if (hasFailedTask) {
285
+ break;
286
+ }
287
+ }
249
288
  } catch (error) {
250
289
  const fileError =
251
290
  error instanceof Error ? error : new Error(String(error));
@@ -267,6 +306,11 @@ export abstract class ModestBenchEngine implements BenchmarkEngine {
267
306
 
268
307
  // Call reporter onFileEnd for error case
269
308
  await this.callReporters(reporters, 'onFileEnd', errorResult);
309
+
310
+ // Check bail flag for file-level errors
311
+ if (mergedConfig.bail) {
312
+ break;
313
+ }
270
314
  }
271
315
  }
272
316
 
@@ -302,10 +346,103 @@ export abstract class ModestBenchEngine implements BenchmarkEngine {
302
346
  }
303
347
 
304
348
  const overallMean = totalOperations > 0 ? totalTime / totalOperations : 0;
349
+ // Evaluate budgets if configured
350
+ let budgetSummary: BudgetSummary | undefined;
351
+
352
+ if (config.budgets && Object.keys(config.budgets).length > 0) {
353
+ const evaluator = new BudgetEvaluator();
354
+ const baselineStorage = new BaselineStorageService(process.cwd());
355
+
356
+ // Collect task results
357
+ const taskResults = new Map<TaskId, TaskResult>();
358
+
359
+ for (const file of fileResults) {
360
+ for (const suite of file.suites) {
361
+ for (const task of suite.tasks) {
362
+ if (!task.error) {
363
+ // file.filePath is already relative to cwd
364
+ const taskId = createTaskId(
365
+ file.filePath,
366
+ suite.name,
367
+ task.name,
368
+ );
369
+ taskResults.set(taskId, task);
370
+ }
371
+ }
372
+ }
373
+ }
374
+
375
+ // Load baseline data if needed for relative budgets
376
+ let baselineData: Map<TaskId, BaselineSummaryData> | undefined;
377
+
378
+ // Check if any budgets use relative thresholds
379
+ const hasRelativeBudgets = Object.values(config.budgets).some(
380
+ (budget) => (budget as Budget).relative,
381
+ );
382
+
383
+ if (hasRelativeBudgets) {
384
+ const baselineName =
385
+ config.baseline || (await baselineStorage.getDefault());
386
+
387
+ if (baselineName) {
388
+ const baseline = await baselineStorage.getBaseline(baselineName);
389
+
390
+ if (baseline) {
391
+ // Cast keys to TaskId since they come from validated baseline storage
392
+ baselineData = new Map(
393
+ Object.entries(baseline.summary) as [
394
+ TaskId,
395
+ BaselineSummaryData,
396
+ ][],
397
+ );
398
+ } else {
399
+ console.warn(
400
+ `Warning: Baseline "${baselineName}" not found. Relative budgets will be skipped.`,
401
+ );
402
+ }
403
+ } else {
404
+ console.warn(
405
+ 'Warning: Relative budgets configured but no baseline specified. Relative budgets will be skipped.',
406
+ );
407
+ }
408
+ }
409
+
410
+ // Evaluate budgets
411
+ budgetSummary = evaluator.evaluateRun(
412
+ config.budgets as Record<string, Budget>,
413
+ taskResults,
414
+ baselineData,
415
+ );
416
+
417
+ // Notify reporters of budget results
418
+ for (const reporter of reporters) {
419
+ if (reporter.onBudgetResult) {
420
+ await reporter.onBudgetResult(budgetSummary);
421
+ }
422
+ }
423
+
424
+ // Handle budget failures based on budgetMode
425
+ if (budgetSummary.failed > 0) {
426
+ const mode = config.budgetMode || 'fail';
427
+
428
+ if (mode === 'fail') {
429
+ throw new BudgetExceededError(
430
+ `${budgetSummary.failed} of ${budgetSummary.total} budget(s) exceeded`,
431
+ budgetSummary,
432
+ );
433
+ } else if (mode === 'warn') {
434
+ console.warn(
435
+ `Warning: ${budgetSummary.failed} of ${budgetSummary.total} budget(s) exceeded`,
436
+ );
437
+ }
438
+ // mode === 'report': just include in output, don't fail
439
+ }
440
+ }
305
441
 
306
442
  const endTime = new Date();
307
443
  const finalRun: BenchmarkRun = {
308
444
  ...initialRun,
445
+ budgetSummary,
309
446
  duration: endTime.getTime() - startTime.getTime(),
310
447
  endTime,
311
448
  files: fileResults,
@@ -489,6 +626,7 @@ export abstract class ModestBenchEngine implements BenchmarkEngine {
489
626
  private async executeBenchmarkFile(
490
627
  filePath: string,
491
628
  config: ModestBenchConfig,
629
+ cwd: string,
492
630
  reporters: Reporter[] = [],
493
631
  signal?: AbortSignal,
494
632
  ): Promise<FileResult> {
@@ -543,11 +681,14 @@ export abstract class ModestBenchEngine implements BenchmarkEngine {
543
681
 
544
682
  const endTime = new Date();
545
683
 
684
+ // Normalize file path to be relative to cwd
685
+ const relativePath = pathRelative(cwd, filePath);
686
+
546
687
  return {
547
688
  config: benchmarkDef.config,
548
689
  duration: endTime.getTime() - startTime.getTime(),
549
690
  endTime,
550
- filePath,
691
+ filePath: relativePath,
551
692
  startTime,
552
693
  suites: suiteResults,
553
694
  };
@@ -556,11 +697,14 @@ export abstract class ModestBenchEngine implements BenchmarkEngine {
556
697
  const executionError =
557
698
  error instanceof Error ? error : new Error(String(error));
558
699
 
700
+ // Normalize file path to be relative to cwd
701
+ const relativePath = pathRelative(cwd, filePath);
702
+
559
703
  return {
560
704
  duration: endTime.getTime() - startTime.getTime(),
561
705
  endTime,
562
706
  error: executionError,
563
- filePath,
707
+ filePath: relativePath,
564
708
  startTime,
565
709
  suites: [],
566
710
  };
@@ -697,19 +841,6 @@ export abstract class ModestBenchEngine implements BenchmarkEngine {
697
841
  }
698
842
  }
699
843
 
700
- /**
701
- * Generate a unique run ID
702
- *
703
- * Uses crypto.randomBytes for cryptographically random 7-character IDs.
704
- * Format: 7 lowercase alphanumeric characters (e.g., "k3m9x2p")
705
- */
706
- private generateRunId(): string {
707
- // Generate random bytes, convert to hex, then to base36, take first 7 chars
708
- const hex = randomBytes(4).toString('hex');
709
- const num = parseInt(hex, 16);
710
- return num.toString(36).padStart(7, '0').substring(0, 7);
711
- }
712
-
713
844
  /**
714
845
  * Get CI/CD information if available
715
846
  */
@@ -1,4 +1,4 @@
1
- import { isAbsolute, join, resolve } from 'node:path';
1
+ import { extname, isAbsolute, join, resolve } from 'node:path';
2
2
 
3
3
  /**
4
4
  * Resolves the final output path for a reporter
@@ -29,7 +29,15 @@ export const resolveOutputPath = (
29
29
  return resolve(process.cwd(), outputFile);
30
30
  }
31
31
 
32
- // Fall back to default behavior
32
+ // If outputDir looks like a file (has extension), treat it as a file path
33
+ // This handles cases like: --output results.csv
34
+ if (outputDir && extname(outputDir)) {
35
+ return isAbsolute(outputDir)
36
+ ? outputDir
37
+ : resolve(process.cwd(), outputDir);
38
+ }
39
+
40
+ // Fall back to default behavior (outputDir is a directory)
33
41
  if (outputDir && defaultFilename) {
34
42
  return join(outputDir, defaultFilename);
35
43
  }
@@ -143,10 +143,19 @@ export const isModestBenchError = (
143
143
  error: unknown,
144
144
  ): error is ModestBenchError => {
145
145
  return (
146
- typeof error === 'object' &&
147
- error !== null &&
146
+ isError(error) &&
148
147
  'code' in error &&
149
148
  typeof (error as { code: unknown }).code === 'string' &&
150
149
  (error as { code: string }).code.startsWith('ERR_MB_')
151
150
  );
152
151
  };
152
+
153
+ /**
154
+ * Type guard to check if an error is a standard Error
155
+ *
156
+ * @param error - The error to check
157
+ * @returns `true` if the error is an `Error`
158
+ */
159
+ export const isError = (error: unknown): error is Error => {
160
+ return error instanceof Error;
161
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Budget-related errors
3
+ *
4
+ * Errors that occur during budget evaluation and enforcement.
5
+ */
6
+
7
+ import type { BudgetSummary } from '../types/core.js';
8
+
9
+ import { ModestBenchError } from './base.js';
10
+
11
+ /**
12
+ * Error thrown when performance budgets are exceeded
13
+ *
14
+ * Thrown when budget evaluation fails and budgetMode is set to 'fail'. Contains
15
+ * the full budget summary for detailed reporting.
16
+ */
17
+ export class BudgetExceededError extends ModestBenchError {
18
+ /**
19
+ * Budget summary containing details of all violations
20
+ */
21
+ public readonly budgetSummary: BudgetSummary;
22
+
23
+ /**
24
+ * Error code for budget exceeded errors
25
+ */
26
+ readonly code = 'ERR_MB_BUDGET_EXCEEDED';
27
+
28
+ /**
29
+ * Create a new budget exceeded error
30
+ *
31
+ * @param message - Human-readable error message
32
+ * @param budgetSummary - Budget evaluation results
33
+ */
34
+ constructor(message: string, budgetSummary: BudgetSummary) {
35
+ super(message);
36
+ this.budgetSummary = budgetSummary;
37
+ }
38
+ }
@@ -13,6 +13,9 @@ export {
13
13
  ModestBenchError,
14
14
  } from './base.js';
15
15
 
16
+ // Budget errors
17
+ export { BudgetExceededError } from './budget.js';
18
+
16
19
  // CLI errors
17
20
  export {
18
21
  InvalidArgumentError,
package/src/index.ts CHANGED
@@ -26,11 +26,17 @@ export * from './errors/index.js';
26
26
  export { CsvReporter } from './reporters/csv.js';
27
27
  export { HumanReporter } from './reporters/human.js';
28
28
  export { JsonReporter } from './reporters/json.js';
29
+ export { ProfileHumanReporter } from './reporters/profile-human.js';
29
30
 
30
31
  // Services
31
32
  export { ModestBenchConfigurationManager } from './services/config-manager.js';
32
33
  export { BenchmarkFileLoader } from './services/file-loader.js';
33
34
  export { FileHistoryStorage } from './services/history-storage.js';
35
+ // Profiler services
36
+ export { filterProfile } from './services/profiler/profile-filter.js';
37
+ export { parseProfile } from './services/profiler/profile-parser.js';
38
+
39
+ export { runWithProfiling } from './services/profiler/profile-runner.js';
34
40
  export { ModestBenchProgressManager } from './services/progress-manager.js';
35
41
  export {
36
42
  BaseReporter,
@@ -40,3 +46,6 @@ export {
40
46
 
41
47
  // Export all types
42
48
  export * from './types/index.js';
49
+
50
+ // Utilities
51
+ export { findPackageRoot } from './utils/package.js';
@@ -11,20 +11,24 @@ import { dirname } from 'node:path';
11
11
 
12
12
  import type {
13
13
  BenchmarkRun,
14
- FileResult,
15
- ProgressState,
16
- SuiteResult,
14
+ BudgetResult,
15
+ BudgetSummary,
16
+ TaskId,
17
17
  TaskResult,
18
18
  } from '../types/index.js';
19
19
 
20
20
  import { ReporterOutputError } from '../errors/index.js';
21
21
  import { BaseReporter } from '../services/reporter-registry.js';
22
+ import { createTaskId } from '../types/index.js';
22
23
 
23
24
  /**
24
25
  * CSV column definitions for task results
25
26
  */
26
27
  interface CsvRow {
27
28
  readonly arch: string;
29
+ /** Budget passed: 1 (pass), 0 (fail), undefined (no budget) */
30
+ readonly budgetPassed?: number | undefined;
31
+ readonly budgetViolations?: string | undefined;
28
32
  readonly ciProvider?: string | undefined;
29
33
  readonly cpuCores: number;
30
34
  readonly cpuModel: string;
@@ -54,6 +58,8 @@ interface CsvRow {
54
58
  * CSV reporter for structured tabular output
55
59
  */
56
60
  export class CsvReporter extends BaseReporter {
61
+ private budgetResults: Map<TaskId, BudgetResult> = new Map();
62
+
57
63
  private currentFile = '';
58
64
 
59
65
  private currentRun?: BenchmarkRun;
@@ -68,8 +74,6 @@ export class CsvReporter extends BaseReporter {
68
74
 
69
75
  private readonly outputPath?: string | undefined;
70
76
 
71
- private readonly quiet: boolean;
72
-
73
77
  private readonly quote: string;
74
78
 
75
79
  private rows: CsvRow[] = [];
@@ -92,7 +96,6 @@ export class CsvReporter extends BaseReporter {
92
96
  this.includeMetadata = options.includeMetadata ?? true;
93
97
  this.delimiter = options.delimiter ?? ',';
94
98
  this.quote = options.quote ?? '"';
95
- this.quiet = options.quiet ?? false;
96
99
  }
97
100
 
98
101
  /**
@@ -137,6 +140,31 @@ export class CsvReporter extends BaseReporter {
137
140
  return this.includeMetadata;
138
141
  }
139
142
 
143
+ onBudgetResult(summary: BudgetSummary): void {
144
+ // Store budget results indexed by taskId
145
+ for (const result of summary.results) {
146
+ this.budgetResults.set(result.taskId, result);
147
+ }
148
+
149
+ // Update existing rows with budget data (since onTaskResult is called before onBudgetResult)
150
+ for (const row of this.rows) {
151
+ // row.file is already relative to cwd
152
+ const taskId = createTaskId(row.file, row.suite, row.task);
153
+ const budgetResult = this.budgetResults.get(taskId);
154
+ if (budgetResult) {
155
+ // Need to cast to mutable to update readonly properties
156
+ const mutableRow = row as {
157
+ budgetPassed?: number;
158
+ budgetViolations?: string;
159
+ };
160
+ mutableRow.budgetPassed = budgetResult.passed ? 1 : 0;
161
+ mutableRow.budgetViolations = budgetResult.violations
162
+ .map((v) => v.type)
163
+ .join('; ');
164
+ }
165
+ }
166
+ }
167
+
140
168
  async onEnd(_run: BenchmarkRun): Promise<void> {
141
169
  const csvContent = this.generateCsv();
142
170
 
@@ -151,27 +179,15 @@ export class CsvReporter extends BaseReporter {
151
179
  console.error('CSV Reporter Error:', error.message);
152
180
  }
153
181
 
154
- onFileEnd(_result: FileResult): void {
155
- // No-op for CSV reporter
156
- }
157
-
158
182
  onFileStart(file: string): void {
159
183
  this.currentFile = file;
160
184
  }
161
185
 
162
- onProgress(_state: ProgressState): void {
163
- // No-op for CSV reporter
164
- }
165
-
166
186
  onStart(run: BenchmarkRun): void {
167
187
  this.currentRun = run;
168
188
  this.rows = [];
169
189
  }
170
190
 
171
- onSuiteEnd(_result: SuiteResult): void {
172
- // No-op for CSV reporter
173
- }
174
-
175
191
  onSuiteStart(suite: string): void {
176
192
  this.currentSuite = suite;
177
193
  }
@@ -181,8 +197,21 @@ export class CsvReporter extends BaseReporter {
181
197
  return;
182
198
  }
183
199
 
200
+ // Look up budget result for this task
201
+ // this.currentFile is already relative to cwd (comes from engine)
202
+ const taskId = createTaskId(
203
+ this.currentFile,
204
+ this.currentSuite,
205
+ result.name,
206
+ );
207
+ const budgetResult = this.budgetResults.get(taskId);
208
+
184
209
  const row: CsvRow = {
185
210
  arch: this.currentRun.environment.arch,
211
+ budgetPassed: budgetResult ? (budgetResult.passed ? 1 : 0) : undefined,
212
+ budgetViolations: budgetResult
213
+ ? budgetResult.violations.map((v) => v.type).join('; ')
214
+ : undefined,
186
215
  ciProvider: this.currentRun.ci?.provider,
187
216
  cpuCores: this.currentRun.environment.cpu.cores,
188
217
  cpuModel: this.currentRun.environment.cpu.model,
@@ -211,10 +240,6 @@ export class CsvReporter extends BaseReporter {
211
240
  this.rows.push(row);
212
241
  }
213
242
 
214
- onTaskStart(_task: string): void {
215
- // No-op for CSV reporter
216
- }
217
-
218
243
  /**
219
244
  * Escape a field value for CSV format
220
245
  */
@@ -277,6 +302,8 @@ export class CsvReporter extends BaseReporter {
277
302
  'p95',
278
303
  'p99',
279
304
  'error',
305
+ 'budgetPassed',
306
+ 'budgetViolations',
280
307
  'timestamp',
281
308
  ];
282
309
 
@@ -302,9 +329,9 @@ export class CsvReporter extends BaseReporter {
302
329
  */
303
330
  private generateRow(row: CsvRow): string {
304
331
  const values = [
305
- row.file || '',
306
- row.suite || '',
307
- row.task || '',
332
+ row.file,
333
+ row.suite,
334
+ row.task,
308
335
  (row.mean ?? 0).toString(),
309
336
  (row.stdDev ?? 0).toString(),
310
337
  (row.min ?? 0).toString(),
@@ -316,6 +343,8 @@ export class CsvReporter extends BaseReporter {
316
343
  (row.p95 ?? 0).toString(),
317
344
  (row.p99 ?? 0).toString(),
318
345
  row.error || '',
346
+ row.budgetPassed !== undefined ? row.budgetPassed.toString() : '',
347
+ row.budgetViolations || '',
319
348
  row.timestamp || '',
320
349
  ];
321
350