modestbench 0.0.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 (275) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/LICENSE.md +55 -0
  3. package/README.md +699 -0
  4. package/dist/bootstrap.cjs +37 -0
  5. package/dist/bootstrap.cjs.map +1 -0
  6. package/dist/bootstrap.d.cts +17 -0
  7. package/dist/bootstrap.d.cts.map +1 -0
  8. package/dist/bootstrap.d.ts +17 -0
  9. package/dist/bootstrap.d.ts.map +1 -0
  10. package/dist/bootstrap.js +33 -0
  11. package/dist/bootstrap.js.map +1 -0
  12. package/dist/cli/commands/history.cjs +459 -0
  13. package/dist/cli/commands/history.cjs.map +1 -0
  14. package/dist/cli/commands/history.d.cts +34 -0
  15. package/dist/cli/commands/history.d.cts.map +1 -0
  16. package/dist/cli/commands/history.d.ts +34 -0
  17. package/dist/cli/commands/history.d.ts.map +1 -0
  18. package/dist/cli/commands/history.js +422 -0
  19. package/dist/cli/commands/history.js.map +1 -0
  20. package/dist/cli/commands/init.cjs +566 -0
  21. package/dist/cli/commands/init.cjs.map +1 -0
  22. package/dist/cli/commands/init.d.cts +26 -0
  23. package/dist/cli/commands/init.d.cts.map +1 -0
  24. package/dist/cli/commands/init.d.ts +26 -0
  25. package/dist/cli/commands/init.d.ts.map +1 -0
  26. package/dist/cli/commands/init.js +562 -0
  27. package/dist/cli/commands/init.js.map +1 -0
  28. package/dist/cli/commands/run.cjs +285 -0
  29. package/dist/cli/commands/run.cjs.map +1 -0
  30. package/dist/cli/commands/run.d.cts +37 -0
  31. package/dist/cli/commands/run.d.cts.map +1 -0
  32. package/dist/cli/commands/run.d.ts +37 -0
  33. package/dist/cli/commands/run.d.ts.map +1 -0
  34. package/dist/cli/commands/run.js +248 -0
  35. package/dist/cli/commands/run.js.map +1 -0
  36. package/dist/cli/index.cjs +523 -0
  37. package/dist/cli/index.cjs.map +1 -0
  38. package/dist/cli/index.d.cts +58 -0
  39. package/dist/cli/index.d.cts.map +1 -0
  40. package/dist/cli/index.d.ts +58 -0
  41. package/dist/cli/index.d.ts.map +1 -0
  42. package/dist/cli/index.js +515 -0
  43. package/dist/cli/index.js.map +1 -0
  44. package/dist/config/manager.cjs +370 -0
  45. package/dist/config/manager.cjs.map +1 -0
  46. package/dist/config/manager.d.cts +46 -0
  47. package/dist/config/manager.d.cts.map +1 -0
  48. package/dist/config/manager.d.ts +46 -0
  49. package/dist/config/manager.d.ts.map +1 -0
  50. package/dist/config/manager.js +333 -0
  51. package/dist/config/manager.js.map +1 -0
  52. package/dist/config/schema.cjs +182 -0
  53. package/dist/config/schema.cjs.map +1 -0
  54. package/dist/config/schema.d.cts +51 -0
  55. package/dist/config/schema.d.cts.map +1 -0
  56. package/dist/config/schema.d.ts +51 -0
  57. package/dist/config/schema.d.ts.map +1 -0
  58. package/dist/config/schema.js +145 -0
  59. package/dist/config/schema.js.map +1 -0
  60. package/dist/constants.cjs +22 -0
  61. package/dist/constants.cjs.map +1 -0
  62. package/dist/constants.d.cts +10 -0
  63. package/dist/constants.d.cts.map +1 -0
  64. package/dist/constants.d.ts +10 -0
  65. package/dist/constants.d.ts.map +1 -0
  66. package/dist/constants.js +19 -0
  67. package/dist/constants.js.map +1 -0
  68. package/dist/core/benchmark-schema.cjs +135 -0
  69. package/dist/core/benchmark-schema.cjs.map +1 -0
  70. package/dist/core/benchmark-schema.d.cts +139 -0
  71. package/dist/core/benchmark-schema.d.cts.map +1 -0
  72. package/dist/core/benchmark-schema.d.ts +139 -0
  73. package/dist/core/benchmark-schema.d.ts.map +1 -0
  74. package/dist/core/benchmark-schema.js +132 -0
  75. package/dist/core/benchmark-schema.js.map +1 -0
  76. package/dist/core/engine.cjs +669 -0
  77. package/dist/core/engine.cjs.map +1 -0
  78. package/dist/core/engine.d.cts +128 -0
  79. package/dist/core/engine.d.cts.map +1 -0
  80. package/dist/core/engine.d.ts +128 -0
  81. package/dist/core/engine.d.ts.map +1 -0
  82. package/dist/core/engine.js +632 -0
  83. package/dist/core/engine.js.map +1 -0
  84. package/dist/core/engines/accurate-engine.cjs +292 -0
  85. package/dist/core/engines/accurate-engine.cjs.map +1 -0
  86. package/dist/core/engines/accurate-engine.d.cts +63 -0
  87. package/dist/core/engines/accurate-engine.d.cts.map +1 -0
  88. package/dist/core/engines/accurate-engine.d.ts +63 -0
  89. package/dist/core/engines/accurate-engine.d.ts.map +1 -0
  90. package/dist/core/engines/accurate-engine.js +288 -0
  91. package/dist/core/engines/accurate-engine.js.map +1 -0
  92. package/dist/core/engines/index.cjs +21 -0
  93. package/dist/core/engines/index.cjs.map +1 -0
  94. package/dist/core/engines/index.d.cts +16 -0
  95. package/dist/core/engines/index.d.cts.map +1 -0
  96. package/dist/core/engines/index.d.ts +16 -0
  97. package/dist/core/engines/index.d.ts.map +1 -0
  98. package/dist/core/engines/index.js +16 -0
  99. package/dist/core/engines/index.js.map +1 -0
  100. package/dist/core/engines/tinybench-engine.cjs +286 -0
  101. package/dist/core/engines/tinybench-engine.cjs.map +1 -0
  102. package/dist/core/engines/tinybench-engine.d.cts +18 -0
  103. package/dist/core/engines/tinybench-engine.d.cts.map +1 -0
  104. package/dist/core/engines/tinybench-engine.d.ts +18 -0
  105. package/dist/core/engines/tinybench-engine.d.ts.map +1 -0
  106. package/dist/core/engines/tinybench-engine.js +282 -0
  107. package/dist/core/engines/tinybench-engine.js.map +1 -0
  108. package/dist/core/error-manager.cjs +303 -0
  109. package/dist/core/error-manager.cjs.map +1 -0
  110. package/dist/core/error-manager.d.cts +77 -0
  111. package/dist/core/error-manager.d.cts.map +1 -0
  112. package/dist/core/error-manager.d.ts +77 -0
  113. package/dist/core/error-manager.d.ts.map +1 -0
  114. package/dist/core/error-manager.js +299 -0
  115. package/dist/core/error-manager.js.map +1 -0
  116. package/dist/core/loader.cjs +287 -0
  117. package/dist/core/loader.cjs.map +1 -0
  118. package/dist/core/loader.d.cts +55 -0
  119. package/dist/core/loader.d.cts.map +1 -0
  120. package/dist/core/loader.d.ts +55 -0
  121. package/dist/core/loader.d.ts.map +1 -0
  122. package/dist/core/loader.js +250 -0
  123. package/dist/core/loader.js.map +1 -0
  124. package/dist/core/stats-utils.cjs +99 -0
  125. package/dist/core/stats-utils.cjs.map +1 -0
  126. package/dist/core/stats-utils.d.cts +50 -0
  127. package/dist/core/stats-utils.d.cts.map +1 -0
  128. package/dist/core/stats-utils.d.ts +50 -0
  129. package/dist/core/stats-utils.d.ts.map +1 -0
  130. package/dist/core/stats-utils.js +94 -0
  131. package/dist/core/stats-utils.js.map +1 -0
  132. package/dist/index.cjs +64 -0
  133. package/dist/index.cjs.map +1 -0
  134. package/dist/index.d.cts +22 -0
  135. package/dist/index.d.cts.map +1 -0
  136. package/dist/index.d.ts +22 -0
  137. package/dist/index.d.ts.map +1 -0
  138. package/dist/index.js +30 -0
  139. package/dist/index.js.map +1 -0
  140. package/dist/progress/manager.cjs +325 -0
  141. package/dist/progress/manager.cjs.map +1 -0
  142. package/dist/progress/manager.d.cts +125 -0
  143. package/dist/progress/manager.d.cts.map +1 -0
  144. package/dist/progress/manager.d.ts +125 -0
  145. package/dist/progress/manager.d.ts.map +1 -0
  146. package/dist/progress/manager.js +321 -0
  147. package/dist/progress/manager.js.map +1 -0
  148. package/dist/reporters/csv.cjs +250 -0
  149. package/dist/reporters/csv.cjs.map +1 -0
  150. package/dist/reporters/csv.d.cts +92 -0
  151. package/dist/reporters/csv.d.cts.map +1 -0
  152. package/dist/reporters/csv.d.ts +92 -0
  153. package/dist/reporters/csv.d.ts.map +1 -0
  154. package/dist/reporters/csv.js +246 -0
  155. package/dist/reporters/csv.js.map +1 -0
  156. package/dist/reporters/human.cjs +516 -0
  157. package/dist/reporters/human.cjs.map +1 -0
  158. package/dist/reporters/human.d.cts +86 -0
  159. package/dist/reporters/human.d.cts.map +1 -0
  160. package/dist/reporters/human.d.ts +86 -0
  161. package/dist/reporters/human.d.ts.map +1 -0
  162. package/dist/reporters/human.js +509 -0
  163. package/dist/reporters/human.js.map +1 -0
  164. package/dist/reporters/index.cjs +17 -0
  165. package/dist/reporters/index.cjs.map +1 -0
  166. package/dist/reporters/index.d.cts +10 -0
  167. package/dist/reporters/index.d.cts.map +1 -0
  168. package/dist/reporters/index.d.ts +10 -0
  169. package/dist/reporters/index.d.ts.map +1 -0
  170. package/dist/reporters/index.js +10 -0
  171. package/dist/reporters/index.js.map +1 -0
  172. package/dist/reporters/json.cjs +215 -0
  173. package/dist/reporters/json.cjs.map +1 -0
  174. package/dist/reporters/json.d.cts +79 -0
  175. package/dist/reporters/json.d.cts.map +1 -0
  176. package/dist/reporters/json.d.ts +79 -0
  177. package/dist/reporters/json.d.ts.map +1 -0
  178. package/dist/reporters/json.js +211 -0
  179. package/dist/reporters/json.js.map +1 -0
  180. package/dist/reporters/registry.cjs +255 -0
  181. package/dist/reporters/registry.cjs.map +1 -0
  182. package/dist/reporters/registry.d.cts +155 -0
  183. package/dist/reporters/registry.d.cts.map +1 -0
  184. package/dist/reporters/registry.d.ts +155 -0
  185. package/dist/reporters/registry.d.ts.map +1 -0
  186. package/dist/reporters/registry.js +249 -0
  187. package/dist/reporters/registry.js.map +1 -0
  188. package/dist/reporters/simple.cjs +328 -0
  189. package/dist/reporters/simple.cjs.map +1 -0
  190. package/dist/reporters/simple.d.cts +51 -0
  191. package/dist/reporters/simple.d.cts.map +1 -0
  192. package/dist/reporters/simple.d.ts +51 -0
  193. package/dist/reporters/simple.d.ts.map +1 -0
  194. package/dist/reporters/simple.js +321 -0
  195. package/dist/reporters/simple.js.map +1 -0
  196. package/dist/schema/modestbench-config.schema.json +162 -0
  197. package/dist/storage/history.cjs +456 -0
  198. package/dist/storage/history.cjs.map +1 -0
  199. package/dist/storage/history.d.cts +99 -0
  200. package/dist/storage/history.d.cts.map +1 -0
  201. package/dist/storage/history.d.ts +99 -0
  202. package/dist/storage/history.d.ts.map +1 -0
  203. package/dist/storage/history.js +452 -0
  204. package/dist/storage/history.js.map +1 -0
  205. package/dist/types/cli.cjs +21 -0
  206. package/dist/types/cli.cjs.map +1 -0
  207. package/dist/types/cli.d.cts +296 -0
  208. package/dist/types/cli.d.cts.map +1 -0
  209. package/dist/types/cli.d.ts +296 -0
  210. package/dist/types/cli.d.ts.map +1 -0
  211. package/dist/types/cli.js +18 -0
  212. package/dist/types/cli.js.map +1 -0
  213. package/dist/types/core.cjs +14 -0
  214. package/dist/types/core.cjs.map +1 -0
  215. package/dist/types/core.d.cts +380 -0
  216. package/dist/types/core.d.cts.map +1 -0
  217. package/dist/types/core.d.ts +380 -0
  218. package/dist/types/core.d.ts.map +1 -0
  219. package/dist/types/core.js +13 -0
  220. package/dist/types/core.js.map +1 -0
  221. package/dist/types/index.cjs +27 -0
  222. package/dist/types/index.cjs.map +1 -0
  223. package/dist/types/index.d.cts +11 -0
  224. package/dist/types/index.d.cts.map +1 -0
  225. package/dist/types/index.d.ts +11 -0
  226. package/dist/types/index.d.ts.map +1 -0
  227. package/dist/types/index.js +11 -0
  228. package/dist/types/index.js.map +1 -0
  229. package/dist/types/interfaces.cjs +10 -0
  230. package/dist/types/interfaces.cjs.map +1 -0
  231. package/dist/types/interfaces.d.cts +381 -0
  232. package/dist/types/interfaces.d.cts.map +1 -0
  233. package/dist/types/interfaces.d.ts +381 -0
  234. package/dist/types/interfaces.d.ts.map +1 -0
  235. package/dist/types/interfaces.js +9 -0
  236. package/dist/types/interfaces.js.map +1 -0
  237. package/dist/types/utility.cjs +92 -0
  238. package/dist/types/utility.cjs.map +1 -0
  239. package/dist/types/utility.d.cts +330 -0
  240. package/dist/types/utility.d.cts.map +1 -0
  241. package/dist/types/utility.d.ts +330 -0
  242. package/dist/types/utility.d.ts.map +1 -0
  243. package/dist/types/utility.js +78 -0
  244. package/dist/types/utility.js.map +1 -0
  245. package/package.json +211 -0
  246. package/src/bootstrap.ts +35 -0
  247. package/src/cli/commands/history.ts +569 -0
  248. package/src/cli/commands/init.ts +658 -0
  249. package/src/cli/commands/run.ts +346 -0
  250. package/src/cli/index.ts +642 -0
  251. package/src/config/manager.ts +387 -0
  252. package/src/config/schema.ts +188 -0
  253. package/src/constants.ts +21 -0
  254. package/src/core/benchmark-schema.ts +185 -0
  255. package/src/core/engine.ts +888 -0
  256. package/src/core/engines/accurate-engine.ts +408 -0
  257. package/src/core/engines/index.ts +16 -0
  258. package/src/core/engines/tinybench-engine.ts +335 -0
  259. package/src/core/error-manager.ts +372 -0
  260. package/src/core/loader.ts +324 -0
  261. package/src/core/stats-utils.ts +135 -0
  262. package/src/index.ts +46 -0
  263. package/src/progress/manager.ts +415 -0
  264. package/src/reporters/csv.ts +368 -0
  265. package/src/reporters/human.ts +707 -0
  266. package/src/reporters/index.ts +10 -0
  267. package/src/reporters/json.ts +302 -0
  268. package/src/reporters/registry.ts +349 -0
  269. package/src/reporters/simple.ts +459 -0
  270. package/src/storage/history.ts +600 -0
  271. package/src/types/cli.ts +312 -0
  272. package/src/types/core.ts +414 -0
  273. package/src/types/index.ts +18 -0
  274. package/src/types/interfaces.ts +451 -0
  275. package/src/types/utility.ts +446 -0
@@ -0,0 +1,600 @@
1
+ /**
2
+ * ModestBench History Storage
3
+ *
4
+ * File-based storage system for benchmark run history and results. Provides
5
+ * querying, cleanup, and export capabilities for historical data.
6
+ */
7
+
8
+ import { createHash } from 'node:crypto';
9
+ import {
10
+ existsSync,
11
+ mkdirSync,
12
+ readFileSync,
13
+ unlinkSync,
14
+ writeFileSync,
15
+ } from 'node:fs';
16
+ import { join } from 'node:path';
17
+
18
+ import type {
19
+ BenchmarkRun,
20
+ CleanupResult,
21
+ HistoryQuery,
22
+ HistoryStorage,
23
+ RetentionPolicy,
24
+ } from '../types/index.js';
25
+
26
+ /**
27
+ * Index entry for stored benchmark runs
28
+ */
29
+ interface IndexEntry {
30
+ readonly date: Date;
31
+ readonly filename: string;
32
+ readonly id: string;
33
+ readonly sizeBytes: number;
34
+ readonly summary: string;
35
+ readonly tags: string[];
36
+ }
37
+
38
+ /**
39
+ * Storage index structure
40
+ */
41
+ interface StorageIndex {
42
+ readonly created: Date;
43
+ readonly entries: IndexEntry[];
44
+ readonly lastModified: Date;
45
+ readonly version: string;
46
+ }
47
+
48
+ /**
49
+ * File-based history storage implementation
50
+ */
51
+ export class FileHistoryStorage implements HistoryStorage {
52
+ private index: null | StorageIndex = null;
53
+
54
+ private readonly indexFile: string;
55
+
56
+ private readonly maxFileSize: number;
57
+
58
+ private readonly storageDir: string;
59
+
60
+ constructor(
61
+ options: {
62
+ maxFileSize?: number;
63
+ storageDir?: string;
64
+ } = {},
65
+ ) {
66
+ this.storageDir =
67
+ options.storageDir || join(process.cwd(), '.modestbench', 'history');
68
+ this.indexFile = join(this.storageDir, 'index.json');
69
+ this.maxFileSize = options.maxFileSize || 10 * 1024 * 1024; // 10MB default
70
+
71
+ this.ensureStorageDir();
72
+ }
73
+
74
+ public static isValidBenchmarkRun(obj: unknown): obj is BenchmarkRun {
75
+ // Type guard function that checks properties on unknown object
76
+ return (
77
+ !!obj &&
78
+ typeof obj === 'object' &&
79
+ 'id' in obj &&
80
+ typeof obj.id === 'string' &&
81
+ 'files' in obj &&
82
+ Array.isArray(obj.files) &&
83
+ 'startTime' in obj &&
84
+ !!obj.startTime &&
85
+ 'endTime' in obj &&
86
+ !!obj.endTime &&
87
+ 'environment' in obj &&
88
+ !!obj.environment &&
89
+ 'summary' in obj &&
90
+ !!obj.summary
91
+ );
92
+ }
93
+
94
+ /**
95
+ * Clean up old data according to retention policy
96
+ */
97
+ async cleanup(policy: RetentionPolicy): Promise<CleanupResult> {
98
+ try {
99
+ const index = await this.loadIndex();
100
+ const entriesToRemove: IndexEntry[] = [];
101
+ let totalSize = 0;
102
+
103
+ // Calculate current storage metrics
104
+ for (const entry of index.entries) {
105
+ totalSize += entry.sizeBytes;
106
+ }
107
+
108
+ // Sort entries by date (oldest first) for cleanup
109
+ const sortedEntries = [...index.entries].sort(
110
+ (a, b) => a.date.getTime() - b.date.getTime(),
111
+ );
112
+
113
+ // Apply retention policies
114
+ for (const entry of sortedEntries) {
115
+ let shouldRemove = false;
116
+
117
+ // Check max age
118
+ if (
119
+ policy.maxAge &&
120
+ Date.now() - entry.date.getTime() > policy.maxAge
121
+ ) {
122
+ shouldRemove = true;
123
+ }
124
+
125
+ // Check max runs count (remove oldest)
126
+ if (
127
+ policy.maxRuns &&
128
+ index.entries.length - entriesToRemove.length > policy.maxRuns
129
+ ) {
130
+ shouldRemove = true;
131
+ }
132
+
133
+ // Check max size (remove oldest until under limit)
134
+ if (policy.maxSize && totalSize > policy.maxSize) {
135
+ shouldRemove = true;
136
+ totalSize -= entry.sizeBytes;
137
+ }
138
+
139
+ if (shouldRemove) {
140
+ entriesToRemove.push(entry);
141
+ }
142
+ }
143
+
144
+ // Remove files and update index
145
+ const removedFiles: string[] = [];
146
+ let freedBytes = 0;
147
+
148
+ for (const entry of entriesToRemove) {
149
+ const filePath = join(this.storageDir, entry.filename);
150
+ try {
151
+ if (existsSync(filePath)) {
152
+ unlinkSync(filePath);
153
+ removedFiles.push(entry.filename);
154
+ freedBytes += entry.sizeBytes;
155
+ }
156
+ await this.removeFromIndex(entry.id);
157
+ } catch (error) {
158
+ // Log but continue with other deletions
159
+ console.warn(`Failed to remove file ${entry.filename}: ${error}`);
160
+ }
161
+ }
162
+
163
+ return {
164
+ freedBytes,
165
+ removedFiles,
166
+ removedRuns: entriesToRemove.length,
167
+ };
168
+ } catch (error) {
169
+ throw new Error(
170
+ `Failed to cleanup storage: ${error instanceof Error ? error.message : String(error)}`,
171
+ );
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Export historical data
177
+ */
178
+ async export(format: 'csv' | 'json', query?: HistoryQuery): Promise<string> {
179
+ try {
180
+ const runs = await this.queryRuns(query || {});
181
+
182
+ if (format === 'json') {
183
+ return JSON.stringify(runs, null, 2);
184
+ } else if (format === 'csv') {
185
+ return this.exportToCsv(runs);
186
+ } else {
187
+ throw new Error(`Unsupported export format: ${format}`);
188
+ }
189
+ } catch (error) {
190
+ throw new Error(
191
+ `Failed to export data: ${error instanceof Error ? error.message : String(error)}`,
192
+ );
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Get index of all stored runs
198
+ */
199
+ async getIndex(): Promise<
200
+ Array<{ date: Date; id: string; summary: string }>
201
+ > {
202
+ try {
203
+ const index = await this.loadIndex();
204
+ return index.entries.map((entry) => ({
205
+ date: entry.date,
206
+ id: entry.id,
207
+ summary: entry.summary,
208
+ }));
209
+ } catch (error) {
210
+ throw new Error(
211
+ `Failed to get storage index: ${error instanceof Error ? error.message : String(error)}`,
212
+ );
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Get storage statistics
218
+ */
219
+ async getStats(): Promise<{
220
+ newestRun?: Date | undefined;
221
+ oldestRun?: Date | undefined;
222
+ totalRuns: number;
223
+ totalSize: number;
224
+ }> {
225
+ try {
226
+ const index = await this.loadIndex();
227
+ const dates = index.entries
228
+ .map((e) => e.date)
229
+ .sort((a, b) => a.getTime() - b.getTime());
230
+
231
+ return {
232
+ newestRun: dates[dates.length - 1],
233
+ oldestRun: dates[0],
234
+ totalRuns: index.entries.length,
235
+ totalSize: index.entries.reduce(
236
+ (total, entry) => total + entry.sizeBytes,
237
+ 0,
238
+ ),
239
+ };
240
+ } catch {
241
+ return { totalRuns: 0, totalSize: 0 };
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Get storage directory path
247
+ */
248
+ getStorageDir(): string {
249
+ return this.storageDir;
250
+ }
251
+
252
+ /**
253
+ * Get total storage size in bytes
254
+ */
255
+ async getStorageSize(): Promise<number> {
256
+ try {
257
+ const index = await this.loadIndex();
258
+ return index.entries.reduce((total, entry) => total + entry.sizeBytes, 0);
259
+ } catch {
260
+ return 0;
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Load a specific benchmark run
266
+ */
267
+ async loadRun(id: string): Promise<BenchmarkRun | null> {
268
+ try {
269
+ const index = await this.loadIndex();
270
+ const entry = index.entries.find((e) => e.id === id);
271
+
272
+ if (!entry) {
273
+ return null;
274
+ }
275
+
276
+ const filePath = join(this.storageDir, entry.filename);
277
+
278
+ if (!existsSync(filePath)) {
279
+ // File missing, clean up index
280
+ await this.removeFromIndex(id);
281
+ return null;
282
+ }
283
+
284
+ const data = readFileSync(filePath, 'utf8');
285
+ const run = JSON.parse(data) as BenchmarkRun;
286
+
287
+ // Validate the loaded run
288
+ if (!FileHistoryStorage.isValidBenchmarkRun(run)) {
289
+ throw new Error(`Invalid benchmark run data in file ${entry.filename}`);
290
+ }
291
+
292
+ return run;
293
+ } catch (error) {
294
+ throw new Error(
295
+ `Failed to load benchmark run ${id}: ${error instanceof Error ? error.message : String(error)}`,
296
+ );
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Query historical runs
302
+ */
303
+ async queryRuns(query: HistoryQuery): Promise<BenchmarkRun[]> {
304
+ try {
305
+ const index = await this.loadIndex();
306
+ let filteredEntries = [...index.entries];
307
+
308
+ // Apply filters
309
+ if (query.since) {
310
+ filteredEntries = filteredEntries.filter((e) => e.date >= query.since!);
311
+ }
312
+
313
+ if (query.until) {
314
+ filteredEntries = filteredEntries.filter((e) => e.date <= query.until!);
315
+ }
316
+
317
+ if (query.pattern) {
318
+ const regex = new RegExp(query.pattern, 'i');
319
+ filteredEntries = filteredEntries.filter((e) => regex.test(e.summary));
320
+ }
321
+
322
+ if (query.tags && query.tags.length > 0) {
323
+ filteredEntries = filteredEntries.filter((e) =>
324
+ query.tags!.some((tag) => e.tags.includes(tag)),
325
+ );
326
+ }
327
+
328
+ // Apply sorting
329
+ const sortBy = query.sortBy || 'date';
330
+ const sort = query.sort || 'desc';
331
+
332
+ filteredEntries.sort((a, b) => {
333
+ let comparison = 0;
334
+
335
+ switch (sortBy) {
336
+ case 'date':
337
+ comparison = a.date.getTime() - b.date.getTime();
338
+ break;
339
+ case 'name':
340
+ comparison = a.summary.localeCompare(b.summary);
341
+ break;
342
+ default:
343
+ comparison = a.date.getTime() - b.date.getTime();
344
+ }
345
+
346
+ return sort === 'desc' ? -comparison : comparison;
347
+ });
348
+
349
+ // Apply pagination
350
+ const offset = query.offset || 0;
351
+ const limit = query.limit || filteredEntries.length;
352
+ const paginatedEntries = filteredEntries.slice(offset, offset + limit);
353
+
354
+ // Load the actual runs
355
+ const runs: BenchmarkRun[] = [];
356
+ for (const entry of paginatedEntries) {
357
+ const run = await this.loadRun(entry.id);
358
+ if (run) {
359
+ runs.push(run);
360
+ }
361
+ }
362
+
363
+ return runs;
364
+ } catch (error) {
365
+ throw new Error(
366
+ `Failed to query benchmark runs: ${error instanceof Error ? error.message : String(error)}`,
367
+ );
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Save a benchmark run to storage
373
+ */
374
+ async saveRun(run: BenchmarkRun): Promise<void> {
375
+ try {
376
+ this.ensureStorageDir();
377
+
378
+ // Generate filename based on run ID and timestamp
379
+ const filename = this.generateFilename(run);
380
+ const filePath = join(this.storageDir, filename);
381
+
382
+ // Serialize the run data
383
+ const data = JSON.stringify(run, null, 2);
384
+
385
+ // Check file size limit
386
+ if (Buffer.byteLength(data, 'utf8') > this.maxFileSize) {
387
+ throw new Error(
388
+ `Benchmark run data exceeds maximum file size of ${this.maxFileSize} bytes`,
389
+ );
390
+ }
391
+
392
+ // Write the run data
393
+ writeFileSync(filePath, data, 'utf8');
394
+
395
+ // Update the index
396
+ await this.updateIndex(run, filename, Buffer.byteLength(data, 'utf8'));
397
+ } catch (error) {
398
+ throw new Error(
399
+ `Failed to save benchmark run: ${error instanceof Error ? error.message : String(error)}`,
400
+ );
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Ensure storage directory exists
406
+ */
407
+ private ensureStorageDir(): void {
408
+ if (!existsSync(this.storageDir)) {
409
+ mkdirSync(this.storageDir, { recursive: true });
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Export runs to CSV format
415
+ */
416
+ private exportToCsv(runs: BenchmarkRun[]): string {
417
+ const headers = [
418
+ 'runId',
419
+ 'startTime',
420
+ 'endTime',
421
+ 'duration',
422
+ 'files',
423
+ 'suites',
424
+ 'tasks',
425
+ 'passed',
426
+ 'failed',
427
+ 'nodeVersion',
428
+ 'platform',
429
+ 'arch',
430
+ 'gitCommit',
431
+ 'gitBranch',
432
+ ];
433
+
434
+ const rows = runs.map((run) => [
435
+ run.id,
436
+ run.startTime.toISOString(),
437
+ run.endTime.toISOString(),
438
+ run.duration.toString(),
439
+ run.summary.totalFiles.toString(),
440
+ run.summary.totalSuites.toString(),
441
+ run.summary.totalTasks.toString(),
442
+ run.summary.passedTasks.toString(),
443
+ run.summary.failedTasks.toString(),
444
+ run.environment.nodeVersion,
445
+ run.environment.platform,
446
+ run.environment.arch,
447
+ run.git?.commit || '',
448
+ run.git?.branch || '',
449
+ ]);
450
+
451
+ const csvLines = [headers, ...rows];
452
+ return csvLines
453
+ .map((row) =>
454
+ row.map((cell) => `"${cell.replace(/"/g, '""')}"`).join(','),
455
+ )
456
+ .join('\n');
457
+ }
458
+
459
+ /**
460
+ * Generate filename for a benchmark run
461
+ */
462
+ private generateFilename(run: BenchmarkRun): string {
463
+ const timestamp = run.startTime.toISOString().replace(/[:.]/g, '-');
464
+ const hash = createHash('md5').update(run.id).digest('hex').substring(0, 8);
465
+ return `run-${timestamp}-${hash}.json`;
466
+ }
467
+
468
+ /**
469
+ * Generate a human-readable summary for a run
470
+ */
471
+ private generateSummary(run: BenchmarkRun): string {
472
+ const fileCount = run.files.length;
473
+ const taskCount = run.summary.totalTasks;
474
+ const failedCount = run.summary.failedTasks;
475
+
476
+ if (failedCount > 0) {
477
+ return `${fileCount} files, ${taskCount} tasks (${failedCount} failed)`;
478
+ } else {
479
+ return `${fileCount} files, ${taskCount} tasks`;
480
+ }
481
+ }
482
+
483
+ /**
484
+ * Load the storage index
485
+ */
486
+ private async loadIndex(): Promise<StorageIndex> {
487
+ if (this.index) {
488
+ return this.index;
489
+ }
490
+
491
+ if (!existsSync(this.indexFile)) {
492
+ this.index = {
493
+ created: new Date(),
494
+ entries: [],
495
+ lastModified: new Date(),
496
+ version: '1.0.0',
497
+ };
498
+ await this.saveIndex();
499
+ return this.index; // We just assigned it, so it's not null
500
+ }
501
+
502
+ try {
503
+ const data = readFileSync(this.indexFile, 'utf8');
504
+ const parsed = JSON.parse(data) as {
505
+ [key: string]: unknown;
506
+ created: string;
507
+ entries: Array<{ [key: string]: unknown; date: string }>;
508
+ lastModified: string;
509
+ };
510
+
511
+ // Convert date strings back to Date objects
512
+ this.index = {
513
+ created: new Date(parsed.created),
514
+ entries: parsed.entries.map((entry) => ({
515
+ ...entry,
516
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
517
+ date: new Date((entry as any).date),
518
+ })) as IndexEntry[],
519
+ lastModified: new Date(parsed.lastModified),
520
+ version: '1.0.0',
521
+ };
522
+
523
+ return this.index; // We just assigned it, so it's not null
524
+ } catch (error) {
525
+ throw new Error(
526
+ `Failed to load storage index: ${error instanceof Error ? error.message : String(error)}`,
527
+ );
528
+ }
529
+ }
530
+
531
+ /**
532
+ * Remove an entry from the index
533
+ */
534
+ private async removeFromIndex(id: string): Promise<void> {
535
+ const index = await this.loadIndex();
536
+ const entryIndex = index.entries.findIndex((e) => e.id === id);
537
+
538
+ if (entryIndex >= 0) {
539
+ index.entries.splice(entryIndex, 1);
540
+ this.index = index;
541
+ await this.saveIndex();
542
+ }
543
+ }
544
+
545
+ /**
546
+ * Save the storage index
547
+ */
548
+ private async saveIndex(): Promise<void> {
549
+ if (!this.index) {
550
+ return;
551
+ }
552
+
553
+ try {
554
+ this.index = {
555
+ ...this.index,
556
+ lastModified: new Date(),
557
+ };
558
+
559
+ const data = JSON.stringify(this.index, null, 2);
560
+ writeFileSync(this.indexFile, data, 'utf8');
561
+ } catch (error) {
562
+ throw new Error(
563
+ `Failed to save storage index: ${error instanceof Error ? error.message : String(error)}`,
564
+ );
565
+ }
566
+ }
567
+
568
+ /**
569
+ * Update index with a new run
570
+ */
571
+ private async updateIndex(
572
+ run: BenchmarkRun,
573
+ filename: string,
574
+ sizeBytes: number,
575
+ ): Promise<void> {
576
+ const index = await this.loadIndex();
577
+
578
+ const entry: IndexEntry = {
579
+ date: run.startTime,
580
+ filename,
581
+ id: run.id,
582
+ sizeBytes,
583
+ summary: this.generateSummary(run),
584
+ tags: run.tags || [],
585
+ };
586
+
587
+ // Remove existing entry if it exists
588
+ const existingIndex = index.entries.findIndex((e) => e.id === run.id);
589
+ if (existingIndex >= 0) {
590
+ index.entries.splice(existingIndex, 1);
591
+ }
592
+
593
+ // Add new entry
594
+ index.entries.push(entry);
595
+
596
+ // Update index
597
+ this.index = index;
598
+ await this.saveIndex();
599
+ }
600
+ }