@reicek/neataptic-ts 0.1.25 → 0.1.26

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 (210) hide show
  1. package/.github/copilot-instructions.md +11 -0
  2. package/.github/skills/trace-analyzer-extension/SKILL.md +3 -3
  3. package/.github/skills/trace-analyzer-extension/assets/extension-checklist.md +1 -1
  4. package/.github/skills/trace-analyzer-extension/references/analyzer-extension-workflow.md +1 -1
  5. package/.github/skills/trace-audit-reporting/SKILL.md +3 -3
  6. package/.github/skills/trace-audit-reporting/references/trace-analysis-workflow.md +1 -1
  7. package/package.json +19 -13
  8. package/plans/Flappy_Bird_Folder_Documentation_Pass.md +4 -4
  9. package/plans/README.md +24 -0
  10. package/plans/Roadmap.md +62 -40
  11. package/plans/analyze-trace-solid-split.plans.md +66 -0
  12. package/plans/architecture-solid-split.plans.md +9 -15
  13. package/plans/asciiMaze-typescript-repair.plans.md +1 -1
  14. package/plans/generate-docs-solid-split.plans.md +87 -0
  15. package/plans/methods-docs.plans.md +25 -1
  16. package/plans/methods-solid-split.plans.md +14 -14
  17. package/plans/neat-docs.plans.md +9 -1
  18. package/plans/neat-test-surface-repair.plans.md +1 -1
  19. package/plans/render-docs-html-solid-split.plans.md +68 -0
  20. package/plans/src-no-explicit-any-cleanup.plans.md +1 -1
  21. package/plans/utils-docs.plans.md +6 -1
  22. package/scripts/analyze-trace/analyze-trace.analysis.ts +479 -0
  23. package/scripts/analyze-trace/analyze-trace.constants.ts +35 -0
  24. package/scripts/analyze-trace/analyze-trace.io.ts +69 -0
  25. package/scripts/analyze-trace/analyze-trace.report.ts +100 -0
  26. package/scripts/analyze-trace/analyze-trace.shared.ts +116 -0
  27. package/scripts/analyze-trace/analyze-trace.ts +45 -0
  28. package/scripts/analyze-trace/analyze-trace.types.ts +72 -0
  29. package/scripts/assets/theme.css +80 -23
  30. package/scripts/copy-examples.ts +239 -0
  31. package/scripts/export-onnx.ts +223 -0
  32. package/scripts/generate-bench-tables.ts +378 -37
  33. package/scripts/generate-docs/generate-docs.constants.ts +107 -0
  34. package/scripts/generate-docs/generate-docs.order.ts +355 -0
  35. package/scripts/generate-docs/generate-docs.state.ts +31 -0
  36. package/scripts/generate-docs/generate-docs.targets.ts +165 -0
  37. package/scripts/generate-docs/generate-docs.ts +63 -0
  38. package/scripts/generate-docs/generate-docs.types.ts +112 -0
  39. package/scripts/generate-docs/output/generate-docs.output.folder-index.utils.ts +167 -0
  40. package/scripts/generate-docs/output/generate-docs.output.ordering.utils.ts +353 -0
  41. package/scripts/generate-docs/output/generate-docs.output.readme.utils.ts +420 -0
  42. package/scripts/generate-docs/output/generate-docs.output.ts +123 -0
  43. package/scripts/generate-docs/output/generate-docs.output.warnings.utils.ts +219 -0
  44. package/scripts/generate-docs/symbols/generate-docs.symbols.collection.utils.ts +365 -0
  45. package/scripts/generate-docs/symbols/generate-docs.symbols.jsdoc.utils.ts +373 -0
  46. package/scripts/generate-docs/symbols/generate-docs.symbols.normalize.utils.ts +155 -0
  47. package/scripts/generate-docs/symbols/generate-docs.symbols.render.utils.ts +149 -0
  48. package/scripts/generate-docs/symbols/generate-docs.symbols.signature.utils.ts +289 -0
  49. package/scripts/generate-docs/symbols/generate-docs.symbols.ts +11 -0
  50. package/scripts/mermaid-cli.mjs +102 -22
  51. package/scripts/mermaid-cli.ts +736 -0
  52. package/scripts/render-docs-html/render-docs-html.assets.ts +54 -0
  53. package/scripts/render-docs-html/render-docs-html.mermaid.ts +245 -0
  54. package/scripts/{render-docs-html.sidebar.ts → render-docs-html/render-docs-html.navigation.ts} +141 -144
  55. package/scripts/render-docs-html/render-docs-html.pages.ts +333 -0
  56. package/scripts/render-docs-html/render-docs-html.shared.ts +333 -0
  57. package/scripts/render-docs-html/render-docs-html.types.ts +42 -0
  58. package/scripts/render-docs-html.ts +23 -587
  59. package/scripts/run-docs.ts +238 -0
  60. package/scripts/write-dist-docs-pkg.ts +40 -0
  61. package/src/README.md +75 -75
  62. package/src/architecture/connection/README.md +5 -5
  63. package/src/architecture/layer/README.md +508 -508
  64. package/src/architecture/network/README.md +1458 -1458
  65. package/src/architecture/network/activate/README.md +694 -694
  66. package/src/architecture/network/bootstrap/README.md +77 -77
  67. package/src/architecture/network/connect/README.md +74 -74
  68. package/src/architecture/network/deterministic/README.md +135 -135
  69. package/src/architecture/network/evolve/README.md +364 -364
  70. package/src/architecture/network/gating/README.md +130 -130
  71. package/src/architecture/network/genetic/README.md +399 -399
  72. package/src/architecture/network/mutate/README.md +897 -897
  73. package/src/architecture/network/onnx/README.md +720 -720
  74. package/src/architecture/network/onnx/export/README.md +728 -728
  75. package/src/architecture/network/onnx/export/layers/README.md +450 -450
  76. package/src/architecture/network/onnx/import/README.md +618 -618
  77. package/src/architecture/network/onnx/schema/README.md +32 -32
  78. package/src/architecture/network/prune/README.md +245 -245
  79. package/src/architecture/network/remove/README.md +135 -135
  80. package/src/architecture/network/runtime/README.md +106 -106
  81. package/src/architecture/network/serialize/README.md +542 -542
  82. package/src/architecture/network/slab/README.md +608 -608
  83. package/src/architecture/network/standalone/README.md +212 -212
  84. package/src/architecture/network/stats/README.md +84 -84
  85. package/src/architecture/network/topology/README.md +465 -465
  86. package/src/architecture/network/training/README.md +200 -200
  87. package/src/architecture/node/README.md +5 -5
  88. package/src/architecture/nodePool/README.md +14 -14
  89. package/src/methods/README.md +99 -99
  90. package/src/methods/activation/README.md +189 -189
  91. package/src/methods/cost/README.md +131 -131
  92. package/src/methods/rate/README.md +86 -86
  93. package/src/multithreading/README.md +77 -77
  94. package/src/multithreading/workers/browser/README.md +8 -8
  95. package/src/multithreading/workers/node/README.md +8 -8
  96. package/src/neat/README.md +148 -148
  97. package/src/neat/adaptive/README.md +120 -120
  98. package/src/neat/adaptive/acceptance/README.md +40 -40
  99. package/src/neat/adaptive/complexity/README.md +137 -137
  100. package/src/neat/adaptive/core/README.md +197 -197
  101. package/src/neat/adaptive/lineage/README.md +90 -90
  102. package/src/neat/adaptive/mutation/README.md +284 -284
  103. package/src/neat/compat/README.md +43 -43
  104. package/src/neat/compat/core/README.md +90 -90
  105. package/src/neat/diversity/README.md +35 -35
  106. package/src/neat/diversity/core/README.md +88 -88
  107. package/src/neat/evaluate/README.md +85 -85
  108. package/src/neat/evaluate/auto-distance/README.md +75 -75
  109. package/src/neat/evaluate/entropy-compat/README.md +37 -37
  110. package/src/neat/evaluate/entropy-sharing/README.md +43 -43
  111. package/src/neat/evaluate/fitness/README.md +23 -23
  112. package/src/neat/evaluate/novelty/README.md +120 -120
  113. package/src/neat/evaluate/objectives/README.md +17 -17
  114. package/src/neat/evaluate/shared/README.md +94 -94
  115. package/src/neat/evolve/README.md +96 -96
  116. package/src/neat/evolve/adaptive/README.md +60 -60
  117. package/src/neat/evolve/objectives/README.md +63 -63
  118. package/src/neat/evolve/offspring/README.md +56 -56
  119. package/src/neat/evolve/population/README.md +171 -171
  120. package/src/neat/evolve/runtime/README.md +79 -79
  121. package/src/neat/evolve/speciation/README.md +74 -74
  122. package/src/neat/evolve/warnings/README.md +10 -10
  123. package/src/neat/export/README.md +114 -114
  124. package/src/neat/helpers/README.md +50 -50
  125. package/src/neat/init/README.md +9 -9
  126. package/src/neat/lineage/core/README.md +101 -101
  127. package/src/neat/multiobjective/category/README.md +74 -74
  128. package/src/neat/multiobjective/crowding/README.md +272 -272
  129. package/src/neat/multiobjective/dominance/README.md +171 -171
  130. package/src/neat/multiobjective/fronts/README.md +68 -68
  131. package/src/neat/multiobjective/metrics/README.md +43 -43
  132. package/src/neat/multiobjective/objectives/README.md +31 -31
  133. package/src/neat/multiobjective/shared/README.md +27 -27
  134. package/src/neat/mutation/README.md +97 -97
  135. package/src/neat/mutation/add-conn/README.md +115 -115
  136. package/src/neat/mutation/add-node/README.md +126 -126
  137. package/src/neat/mutation/flow/README.md +149 -149
  138. package/src/neat/mutation/repair/README.md +185 -185
  139. package/src/neat/mutation/select/README.md +117 -117
  140. package/src/neat/mutation/shared/README.md +32 -32
  141. package/src/neat/objectives/README.md +25 -25
  142. package/src/neat/objectives/core/README.md +67 -67
  143. package/src/neat/pruning/README.md +40 -40
  144. package/src/neat/pruning/core/README.md +171 -171
  145. package/src/neat/pruning/facade/README.md +32 -32
  146. package/src/neat/rng/README.md +104 -104
  147. package/src/neat/rng/core/README.md +137 -137
  148. package/src/neat/rng/facade/README.md +50 -50
  149. package/src/neat/selection/README.md +111 -111
  150. package/src/neat/selection/core/README.md +227 -227
  151. package/src/neat/selection/facade/README.md +61 -61
  152. package/src/neat/shared/README.md +163 -163
  153. package/src/neat/speciation/README.md +31 -31
  154. package/src/neat/speciation/threshold/README.md +35 -35
  155. package/src/neat/species/README.md +25 -25
  156. package/src/neat/species/core/README.md +20 -20
  157. package/src/neat/species/core/shared/README.md +18 -18
  158. package/src/neat/species/history/context/README.md +22 -22
  159. package/src/neat/telemetry/accessors/README.md +58 -58
  160. package/src/neat/telemetry/exports/README.md +233 -233
  161. package/src/neat/telemetry/facade/README.md +252 -252
  162. package/src/neat/telemetry/facade/archive/README.md +57 -57
  163. package/src/neat/telemetry/facade/buffer/README.md +43 -43
  164. package/src/neat/telemetry/facade/lineage/README.md +12 -12
  165. package/src/neat/telemetry/facade/objectives/README.md +44 -44
  166. package/src/neat/telemetry/facade/runtime/README.md +26 -26
  167. package/src/neat/telemetry/facade/species/README.md +27 -27
  168. package/src/neat/telemetry/metrics/README.md +696 -696
  169. package/src/neat/telemetry/recorder/README.md +57 -57
  170. package/src/neat/telemetry/types/README.md +32 -32
  171. package/src/neat/topology-intent/README.md +75 -75
  172. package/src/utils/README.md +193 -193
  173. package/test/examples/asciiMaze/browser-entry/README.md +92 -92
  174. package/test/examples/asciiMaze/dashboardManager/README.md +109 -109
  175. package/test/examples/asciiMaze/dashboardManager/telemetry/README.md +28 -28
  176. package/test/examples/asciiMaze/evolutionEngine/README.md +1527 -1527
  177. package/test/examples/asciiMaze/mazeMovement/README.md +105 -105
  178. package/test/examples/asciiMaze/mazeMovement/finalization/README.md +16 -16
  179. package/test/examples/asciiMaze/mazeMovement/policy/README.md +57 -57
  180. package/test/examples/asciiMaze/mazeMovement/runtime/README.md +52 -52
  181. package/test/examples/asciiMaze/mazeMovement/shaping/README.md +46 -46
  182. package/test/examples/flappy_bird/browser-entry/README.md +508 -508
  183. package/test/examples/flappy_bird/browser-entry/host/README.md +101 -101
  184. package/test/examples/flappy_bird/browser-entry/host/resize/README.md +144 -144
  185. package/test/examples/flappy_bird/browser-entry/network-view/README.md +194 -194
  186. package/test/examples/flappy_bird/browser-entry/playback/README.md +278 -278
  187. package/test/examples/flappy_bird/browser-entry/playback/background/README.md +129 -129
  188. package/test/examples/flappy_bird/browser-entry/playback/background/ground-grid/README.md +502 -502
  189. package/test/examples/flappy_bird/browser-entry/playback/frame-render/README.md +139 -139
  190. package/test/examples/flappy_bird/browser-entry/playback/snapshot/README.md +10 -10
  191. package/test/examples/flappy_bird/browser-entry/playback/trail/README.md +43 -43
  192. package/test/examples/flappy_bird/browser-entry/playback/worker-channel/README.md +30 -30
  193. package/test/examples/flappy_bird/browser-entry/runtime/README.md +59 -59
  194. package/test/examples/flappy_bird/browser-entry/visualization/README.md +276 -276
  195. package/test/examples/flappy_bird/browser-entry/worker-channel/README.md +16 -16
  196. package/test/examples/flappy_bird/constants/README.md +1070 -1070
  197. package/test/examples/flappy_bird/environment/README.md +22 -22
  198. package/test/examples/flappy_bird/evaluation/README.md +32 -32
  199. package/test/examples/flappy_bird/evaluation/rollout/README.md +141 -141
  200. package/test/examples/flappy_bird/flappy-evolution-worker/README.md +425 -425
  201. package/test/examples/flappy_bird/simulation-shared/README.md +170 -170
  202. package/test/examples/flappy_bird/simulation-shared/observation/README.md +109 -109
  203. package/test/examples/flappy_bird/trainer/README.md +325 -325
  204. package/test/examples/flappy_bird/trainer/evaluation/README.md +74 -74
  205. package/scripts/analyze-trace.ts +0 -590
  206. package/scripts/copy-examples.mjs +0 -114
  207. package/scripts/export-onnx.mjs +0 -86
  208. package/scripts/generate-bench-tables.mjs +0 -182
  209. package/scripts/generate-docs.ts +0 -2900
  210. package/scripts/write-dist-docs-pkg.mjs +0 -16
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Minimal CLI to export a JSON ONNX model from a serialized network state.
3
+ *
4
+ * Usage:
5
+ * npm run onnx:export -- --in network.json --out model.onnx.json [--metadata] [--batch] [--legacy] [--partial] [--mixed]
6
+ *
7
+ * This stays intentionally lightweight. For larger automation flows, prefer
8
+ * calling the ONNX export API directly from application code.
9
+ */
10
+
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { pathToFileURL } from 'node:url';
14
+
15
+ const PRIMARY_ONNX_MODULE_PATH = path.resolve(
16
+ 'dist',
17
+ 'architecture',
18
+ 'onnx.js',
19
+ );
20
+ const FALLBACK_ONNX_MODULE_PATH = path.resolve(
21
+ 'dist',
22
+ 'architecture',
23
+ 'network',
24
+ 'network.onnx.js',
25
+ );
26
+ const NETWORK_MODULE_PATH = path.resolve('dist', 'architecture', 'network.js');
27
+
28
+ interface ExportOnnxOptions {
29
+ includeMetadata: boolean;
30
+ batchDimension: boolean;
31
+ legacyNodeOrdering: boolean;
32
+ allowPartialConnectivity: boolean;
33
+ allowMixedActivations: boolean;
34
+ }
35
+
36
+ interface NetworkLike {
37
+ toJSON?: () => unknown;
38
+ }
39
+
40
+ interface NetworkFactory<TNetwork extends NetworkLike> {
41
+ fromJSON(raw: unknown): TNetwork;
42
+ }
43
+
44
+ interface OnnxExportModule<TNetwork extends NetworkLike> {
45
+ exportToONNX: (network: TNetwork, options: ExportOnnxOptions) => unknown;
46
+ }
47
+
48
+ interface DistDependencies<TNetwork extends NetworkLike> {
49
+ Network: NetworkFactory<TNetwork>;
50
+ exportToONNX: OnnxExportModule<TNetwork>['exportToONNX'];
51
+ }
52
+
53
+ interface ParsedCliOptions extends ExportOnnxOptions {
54
+ inputFile: string;
55
+ outputFile: string;
56
+ }
57
+
58
+ /**
59
+ * Runs the ONNX export CLI.
60
+ *
61
+ * @returns Promise resolved when export succeeds.
62
+ */
63
+ async function main(): Promise<void> {
64
+ const rawArguments = process.argv.slice(2);
65
+ if (hasFlag(rawArguments, '--help') || hasFlag(rawArguments, '-h')) {
66
+ printUsage();
67
+ return;
68
+ }
69
+
70
+ const cliOptions = parseCliOptions(rawArguments);
71
+ const { Network, exportToONNX } = await loadDistDependencies<NetworkLike>();
72
+ const networkJson = readJsonFile(cliOptions.inputFile);
73
+ const network = Network.fromJSON(networkJson);
74
+ const onnxJson = exportToONNX(network, cliOptions);
75
+
76
+ fs.writeFileSync(
77
+ path.resolve(cliOptions.outputFile),
78
+ `${JSON.stringify(onnxJson, null, 2)}\n`,
79
+ 'utf8',
80
+ );
81
+ console.log(`ONNX JSON written to ${cliOptions.outputFile}`);
82
+ }
83
+
84
+ /**
85
+ * Parses CLI options from raw process arguments.
86
+ *
87
+ * @param rawArguments - CLI arguments after the script path.
88
+ * @returns Parsed and validated CLI options.
89
+ */
90
+ function parseCliOptions(rawArguments: readonly string[]): ParsedCliOptions {
91
+ const inputFile = resolveOptionValue(rawArguments, '--in');
92
+ const outputFile = resolveOptionValue(rawArguments, '--out');
93
+
94
+ if (!inputFile || !outputFile) {
95
+ printUsageAndExit('Error: --in and --out are required.');
96
+ }
97
+
98
+ return {
99
+ inputFile,
100
+ outputFile,
101
+ includeMetadata: hasFlag(rawArguments, '--metadata'),
102
+ batchDimension: hasFlag(rawArguments, '--batch'),
103
+ legacyNodeOrdering: hasFlag(rawArguments, '--legacy'),
104
+ allowPartialConnectivity: hasFlag(rawArguments, '--partial'),
105
+ allowMixedActivations: hasFlag(rawArguments, '--mixed'),
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Resolves one option value from `--name value` or `--name=value` syntax.
111
+ *
112
+ * @param rawArguments - CLI arguments after the script path.
113
+ * @param optionName - Long option name including leading dashes.
114
+ * @returns Option value when present.
115
+ */
116
+ function resolveOptionValue(
117
+ rawArguments: readonly string[],
118
+ optionName: string,
119
+ ): string | undefined {
120
+ const exactIndex = rawArguments.indexOf(optionName);
121
+ if (exactIndex >= 0) {
122
+ return rawArguments[exactIndex + 1];
123
+ }
124
+
125
+ const inlineArgument = rawArguments.find((argument) =>
126
+ argument.startsWith(`${optionName}=`),
127
+ );
128
+ return inlineArgument?.slice(optionName.length + 1);
129
+ }
130
+
131
+ /**
132
+ * Checks whether a boolean flag is present.
133
+ *
134
+ * @param rawArguments - CLI arguments after the script path.
135
+ * @param optionName - Flag name including leading dashes.
136
+ * @returns `true` when the flag is present.
137
+ */
138
+ function hasFlag(rawArguments: readonly string[], optionName: string): boolean {
139
+ return rawArguments.includes(optionName);
140
+ }
141
+
142
+ /**
143
+ * Loads the built network and ONNX export modules from `dist/`.
144
+ *
145
+ * @returns The built network constructor and ONNX export function.
146
+ */
147
+ async function loadDistDependencies<TNetwork extends NetworkLike>(): Promise<
148
+ DistDependencies<TNetwork>
149
+ > {
150
+ const onnxModule = (await importModuleWithFallback<
151
+ Partial<OnnxExportModule<TNetwork>>
152
+ >(PRIMARY_ONNX_MODULE_PATH, FALLBACK_ONNX_MODULE_PATH)) as Partial<
153
+ OnnxExportModule<TNetwork>
154
+ >;
155
+ const networkModule = (await import(
156
+ pathToFileURL(NETWORK_MODULE_PATH).href
157
+ )) as { default?: NetworkFactory<TNetwork> };
158
+
159
+ if (!networkModule.default || typeof onnxModule.exportToONNX !== 'function') {
160
+ throw new Error(
161
+ 'Failed to load built ONNX export dependencies from dist/.',
162
+ );
163
+ }
164
+
165
+ return {
166
+ Network: networkModule.default,
167
+ exportToONNX: onnxModule.exportToONNX,
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Imports the primary built module, falling back to a secondary path when the
173
+ * public layout has shifted.
174
+ *
175
+ * @param primaryModulePath - Preferred built module path.
176
+ * @param fallbackModulePath - Fallback built module path.
177
+ * @returns Imported module namespace.
178
+ */
179
+ async function importModuleWithFallback<TModule>(
180
+ primaryModulePath: string,
181
+ fallbackModulePath: string,
182
+ ): Promise<TModule> {
183
+ try {
184
+ return (await import(pathToFileURL(primaryModulePath).href)) as TModule;
185
+ } catch {
186
+ return (await import(pathToFileURL(fallbackModulePath).href)) as TModule;
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Reads and parses one JSON file.
192
+ *
193
+ * @param filePath - Source JSON file path.
194
+ * @returns Parsed JSON value.
195
+ */
196
+ function readJsonFile(filePath: string): unknown {
197
+ return JSON.parse(fs.readFileSync(path.resolve(filePath), 'utf8')) as unknown;
198
+ }
199
+
200
+ /** Prints CLI usage text. */
201
+ function printUsage(): void {
202
+ console.log(
203
+ 'Usage: npm run onnx:export -- --in network.json --out model.onnx.json [--metadata] [--batch] [--legacy] [--partial] [--mixed]',
204
+ );
205
+ }
206
+
207
+ /**
208
+ * Prints CLI usage text and exits the process with failure.
209
+ *
210
+ * @param message - Error message shown before usage text.
211
+ * @returns Never returns.
212
+ */
213
+ function printUsageAndExit(message: string): never {
214
+ console.error(message);
215
+ printUsage();
216
+ process.exit(1);
217
+ }
218
+
219
+ main().catch((error: unknown) => {
220
+ const errorMessage = error instanceof Error ? error.message : String(error);
221
+ console.error('Export failed:', errorMessage);
222
+ process.exit(1);
223
+ });
@@ -14,46 +14,336 @@
14
14
  * - Gracefully degrades when fields missing (older artifact schema).
15
15
  * - Extend later for variance/regression annotation summaries.
16
16
  */
17
- import fs from 'fs';
18
- import path from 'path';
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
19
 
20
+ /** Canonical benchmark artifact consumed by the table generator. */
21
+ const BENCHMARK_ARTIFACT_PATH = path.resolve(
22
+ 'test/benchmarks/benchmark.results.json',
23
+ );
24
+
25
+ /** Metrics shown in the variant delta table. */
26
+ const DELTA_METRICS = ['buildMs', 'fwdAvgMs', 'bytesPerConn'] as const;
27
+
28
+ /** Metrics shown in the heap and resident-set table. */
29
+ const HEAP_METRICS = ['heapUsed', 'rss'] as const;
30
+
31
+ /** Full metric set recognized across both current and legacy artifact schemas. */
32
+ const AGGREGATED_METRICS = [...DELTA_METRICS, ...HEAP_METRICS] as const;
33
+
34
+ /** Benchmark variant identifiers emitted by the build-variant suite. */
35
+ type BenchmarkMode = 'src' | 'dist';
36
+
37
+ /** Metric names that participate in src-vs-dist regression comparison. */
38
+ type DeltaMetricName = (typeof DELTA_METRICS)[number];
39
+
40
+ /** Metric names that participate in the heap summary table. */
41
+ type HeapMetricName = (typeof HEAP_METRICS)[number];
42
+
43
+ /** All metric names recognized by the script. */
44
+ type BenchmarkMetricName = (typeof AGGREGATED_METRICS)[number];
45
+
46
+ /** Legacy flat-artifact key names such as `buildMsMean`. */
47
+ type LegacyMeanMetricKey = `${BenchmarkMetricName}Mean`;
48
+
49
+ /**
50
+ * Aggregate statistics for one benchmark metric.
51
+ *
52
+ * The current table generator only consumes `mean`, but the wider schema can
53
+ * also carry percentile and spread values that may be used by later reports.
54
+ */
20
55
  interface AggregatedMetric {
21
- mean: number;
22
- p50: number;
23
- p95: number;
24
- std: number;
56
+ mean?: number;
57
+ p50?: number;
58
+ p95?: number;
59
+ std?: number;
25
60
  }
26
61
 
27
- /** Pad / truncate value to fixed width for monospace block. */
28
- function cell(v: any, w: number): string {
62
+ /**
63
+ * Scenario-level metrics under one size bucket.
64
+ *
65
+ * The script currently reads the `all` scenario, but the normalized shape keeps
66
+ * the scenario layer so future reporting can distinguish specialized runs.
67
+ */
68
+ type AggregatedScenarioBucket = Partial<
69
+ Record<BenchmarkMetricName, AggregatedMetric>
70
+ >;
71
+
72
+ /**
73
+ * Size bucket keyed by scenario name.
74
+ *
75
+ * Example: `1024 -> all -> buildMs -> { mean: ... }`.
76
+ */
77
+ type AggregatedSizeBucket = Record<
78
+ string,
79
+ AggregatedScenarioBucket | undefined
80
+ > & {
81
+ all?: AggregatedScenarioBucket;
82
+ };
83
+
84
+ /** Aggregated metrics keyed by benchmark size. */
85
+ type AggregatedModeBucket = Record<string, AggregatedSizeBucket>;
86
+
87
+ /** Fully normalized aggregated results keyed by build variant. */
88
+ interface NormalizedAggregatedResults {
89
+ src: AggregatedModeBucket;
90
+ dist: AggregatedModeBucket;
91
+ }
92
+
93
+ /** Minimal artifact contract needed by this script. */
94
+ interface BenchmarkArtifact {
95
+ aggregated?: unknown;
96
+ }
97
+
98
+ /**
99
+ * Legacy array-based aggregated entry shape.
100
+ *
101
+ * Older benchmark artifacts stored one flat record per `(mode, size)` pair
102
+ * using `fooMean` properties instead of the nested scenario bucket structure.
103
+ */
104
+ interface LegacyAggregatedEntry extends Partial<
105
+ Record<LegacyMeanMetricKey, number>
106
+ > {
107
+ mode: BenchmarkMode;
108
+ size: number | string;
109
+ }
110
+
111
+ /**
112
+ * Pads or truncates a value to fixed width for the fenced monospace tables.
113
+ *
114
+ * @param v - Cell value to render.
115
+ * @param w - Target cell width.
116
+ * @returns Fixed-width cell content.
117
+ */
118
+ function cell(v: string | number | undefined | null, w: number): string {
29
119
  const s = String(v ?? '');
30
120
  return s.length >= w ? s.slice(0, w) : s + ' '.repeat(w - s.length);
31
121
  }
32
122
 
33
- /** Format number with trimmed trailing zeros. */
34
- function fmtNum(n: any, digits = 4): string {
35
- if (typeof n !== 'number' || !isFinite(n)) return '';
123
+ /**
124
+ * Formats a numeric value with trimmed trailing zeros.
125
+ *
126
+ * This keeps markdown tables compact without losing the ability to request
127
+ * higher precision for smaller benchmark values.
128
+ *
129
+ * @param n - Candidate numeric value.
130
+ * @param digits - Maximum fixed decimal precision.
131
+ * @returns Formatted number or an empty string when the input is not finite.
132
+ */
133
+ function fmtNum(n: number | undefined, digits = 4): string {
134
+ if (typeof n !== 'number' || !Number.isFinite(n)) return '';
36
135
  return n.toFixed(digits).replace(/0+$/, '').replace(/\.$/, '');
37
136
  }
38
137
 
39
- function loadArtifact(): any | null {
40
- const file = path.resolve('test/benchmarks/benchmark.results.json');
41
- if (!fs.existsSync(file)) {
42
- console.error('[bench:tables] Artifact not found:', file);
138
+ /**
139
+ * Loads the benchmark artifact from disk.
140
+ *
141
+ * @returns Parsed artifact when present and readable, otherwise `null`.
142
+ */
143
+ function loadArtifact(): BenchmarkArtifact | null {
144
+ if (!fs.existsSync(BENCHMARK_ARTIFACT_PATH)) {
145
+ console.error(
146
+ '[bench:tables] Artifact not found:',
147
+ BENCHMARK_ARTIFACT_PATH,
148
+ );
43
149
  return null;
44
150
  }
151
+
45
152
  try {
46
- return JSON.parse(fs.readFileSync(file, 'utf8'));
47
- } catch (e) {
48
- console.error('[bench:tables] Parse error', e);
153
+ return JSON.parse(
154
+ fs.readFileSync(BENCHMARK_ARTIFACT_PATH, 'utf8'),
155
+ ) as BenchmarkArtifact;
156
+ } catch (error) {
157
+ console.error('[bench:tables] Parse error', error);
49
158
  return null;
50
159
  }
51
160
  }
52
161
 
53
- function buildVariantDeltaTable(artifact: any): string {
54
- const agg = artifact.aggregated || {};
55
- const sizes = new Set<string>(Object.keys(agg.src || {}));
56
- const ordered = Array.from(sizes).sort((a, b) => parseInt(a) - parseInt(b));
162
+ /**
163
+ * Normalizes the artifact's aggregated payload into one stable nested shape.
164
+ *
165
+ * The benchmark pipeline has emitted both nested and legacy flat array schemas.
166
+ * Folding both into the same mode/size/scenario/metric structure keeps the
167
+ * table builders simple and resilient.
168
+ *
169
+ * @param artifact - Parsed benchmark artifact.
170
+ * @returns Normalized aggregated result buckets.
171
+ */
172
+ function normalizeAggregated(
173
+ artifact: BenchmarkArtifact,
174
+ ): NormalizedAggregatedResults {
175
+ if (Array.isArray(artifact.aggregated)) {
176
+ return normalizeLegacyAggregated(artifact.aggregated);
177
+ }
178
+
179
+ if (!isRecord(artifact.aggregated)) {
180
+ return createEmptyAggregatedResults();
181
+ }
182
+
183
+ return {
184
+ src: normalizeModeBucket(artifact.aggregated.src),
185
+ dist: normalizeModeBucket(artifact.aggregated.dist),
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Converts the older flat array schema into the normalized nested shape.
191
+ *
192
+ * @param rawEntries - Legacy aggregated entries.
193
+ * @returns Normalized aggregated result buckets.
194
+ */
195
+ function normalizeLegacyAggregated(
196
+ rawEntries: readonly unknown[],
197
+ ): NormalizedAggregatedResults {
198
+ const aggregatedResults = createEmptyAggregatedResults();
199
+
200
+ for (const rawEntry of rawEntries) {
201
+ if (!isLegacyAggregatedEntry(rawEntry)) {
202
+ continue;
203
+ }
204
+
205
+ const sizeKey = String(rawEntry.size);
206
+ const scenarioBucket = ensureAllScenarioBucket(
207
+ aggregatedResults[rawEntry.mode],
208
+ sizeKey,
209
+ );
210
+
211
+ for (const metricName of AGGREGATED_METRICS) {
212
+ const meanKey = `${metricName}Mean` as const;
213
+ const meanValue = rawEntry[meanKey];
214
+ if (typeof meanValue === 'number' && Number.isFinite(meanValue)) {
215
+ scenarioBucket[metricName] = { mean: meanValue };
216
+ }
217
+ }
218
+ }
219
+
220
+ return aggregatedResults;
221
+ }
222
+
223
+ /**
224
+ * Normalizes one top-level mode bucket such as `src` or `dist`.
225
+ *
226
+ * @param rawModeBucket - Unknown raw mode payload.
227
+ * @returns Normalized size buckets.
228
+ */
229
+ function normalizeModeBucket(rawModeBucket: unknown): AggregatedModeBucket {
230
+ if (!isRecord(rawModeBucket)) {
231
+ return {};
232
+ }
233
+
234
+ return Object.fromEntries(
235
+ Object.entries(rawModeBucket).map(([sizeKey, rawSizeBucket]) => [
236
+ sizeKey,
237
+ normalizeSizeBucket(rawSizeBucket),
238
+ ]),
239
+ );
240
+ }
241
+
242
+ /**
243
+ * Normalizes one size bucket keyed by scenario names.
244
+ *
245
+ * @param rawSizeBucket - Unknown raw size payload.
246
+ * @returns Normalized scenario buckets.
247
+ */
248
+ function normalizeSizeBucket(rawSizeBucket: unknown): AggregatedSizeBucket {
249
+ if (!isRecord(rawSizeBucket)) {
250
+ return {};
251
+ }
252
+
253
+ return Object.fromEntries(
254
+ Object.entries(rawSizeBucket).map(([scenarioName, rawScenarioBucket]) => [
255
+ scenarioName,
256
+ normalizeScenarioBucket(rawScenarioBucket),
257
+ ]),
258
+ ) as AggregatedSizeBucket;
259
+ }
260
+
261
+ /**
262
+ * Normalizes one scenario bucket keyed by metric name.
263
+ *
264
+ * Invalid or incomplete metric objects are dropped rather than guessed.
265
+ *
266
+ * @param rawScenarioBucket - Unknown raw scenario payload.
267
+ * @returns Normalized metric bucket.
268
+ */
269
+ function normalizeScenarioBucket(
270
+ rawScenarioBucket: unknown,
271
+ ): AggregatedScenarioBucket {
272
+ if (!isRecord(rawScenarioBucket)) {
273
+ return {};
274
+ }
275
+
276
+ const normalizedEntries = AGGREGATED_METRICS.flatMap((metricName) => {
277
+ const rawMetric = rawScenarioBucket[metricName];
278
+ if (!isRecord(rawMetric)) {
279
+ return [];
280
+ }
281
+
282
+ const meanValue = rawMetric.mean;
283
+ if (typeof meanValue !== 'number' || !Number.isFinite(meanValue)) {
284
+ return [];
285
+ }
286
+
287
+ return [[metricName, { mean: meanValue }] as const];
288
+ });
289
+
290
+ return Object.fromEntries(normalizedEntries) as AggregatedScenarioBucket;
291
+ }
292
+
293
+ /**
294
+ * Ensures the `all` scenario exists for one `(mode, size)` pair.
295
+ *
296
+ * @param modeBucket - Normalized mode bucket.
297
+ * @param sizeKey - Size identifier.
298
+ * @returns Mutable `all` scenario bucket.
299
+ */
300
+ function ensureAllScenarioBucket(
301
+ modeBucket: AggregatedModeBucket,
302
+ sizeKey: string,
303
+ ): AggregatedScenarioBucket {
304
+ modeBucket[sizeKey] ??= {};
305
+ modeBucket[sizeKey].all ??= {};
306
+ return modeBucket[sizeKey].all!;
307
+ }
308
+
309
+ /**
310
+ * Creates an empty normalized results shell.
311
+ *
312
+ * @returns Empty `src` and `dist` mode buckets.
313
+ */
314
+ function createEmptyAggregatedResults(): NormalizedAggregatedResults {
315
+ return {
316
+ src: {},
317
+ dist: {},
318
+ };
319
+ }
320
+
321
+ /**
322
+ * Resolves size keys in ascending numeric order.
323
+ *
324
+ * @param aggregatedResults - Normalized aggregated result buckets.
325
+ * @returns Ordered size keys.
326
+ */
327
+ function resolveOrderedSizes(
328
+ aggregatedResults: NormalizedAggregatedResults,
329
+ ): string[] {
330
+ return [...new Set(Object.keys(aggregatedResults.src))].toSorted(
331
+ compareNumericSizeKeys,
332
+ );
333
+ }
334
+
335
+ /**
336
+ * Builds the variant delta table comparing `src` and `dist` means.
337
+ *
338
+ * The result is tuned for markdown fenced blocks rather than GFM pipe tables,
339
+ * which keeps wide benchmark rows easier to scan in docs and pull requests.
340
+ *
341
+ * @param artifact - Parsed benchmark artifact.
342
+ * @returns Monospace markdown table body.
343
+ */
344
+ function buildVariantDeltaTable(artifact: BenchmarkArtifact): string {
345
+ const aggregatedResults = normalizeAggregated(artifact);
346
+ const orderedSizes = resolveOrderedSizes(aggregatedResults);
57
347
  const rows: string[] = [];
58
348
  rows.push(
59
349
  'Size Metric Src Mean Dist Mean Δ Abs Δ % Flag Result',
@@ -61,13 +351,10 @@ function buildVariantDeltaTable(artifact: any): string {
61
351
  rows.push(
62
352
  '------ ----------------- ------------- ------------- ------------- ---------- ------------- ------------------------------------------------------------',
63
353
  );
64
- const metrics = ['buildMs', 'fwdAvgMs', 'bytesPerConn'];
65
- for (const size of ordered) {
66
- for (const metric of metrics) {
67
- const srcAgg: AggregatedMetric | undefined =
68
- agg.src?.[size]?.all?.[metric];
69
- const distAgg: AggregatedMetric | undefined =
70
- agg.dist?.[size]?.all?.[metric];
354
+ for (const size of orderedSizes) {
355
+ for (const metric of DELTA_METRICS) {
356
+ const srcAgg = aggregatedResults.src[size]?.all?.[metric];
357
+ const distAgg = aggregatedResults.dist[size]?.all?.[metric];
71
358
  const sMean = srcAgg?.mean;
72
359
  const dMean = distAgg?.mean;
73
360
  let dAbs: number | undefined;
@@ -123,10 +410,15 @@ function buildVariantDeltaTable(artifact: any): string {
123
410
  return rows.join('\n');
124
411
  }
125
412
 
126
- function buildHeapTable(artifact: any): string {
127
- const agg = artifact.aggregated || {};
128
- const sizes = new Set<string>(Object.keys(agg.src || {}));
129
- const ordered = Array.from(sizes).sort((a, b) => parseInt(a) - parseInt(b));
413
+ /**
414
+ * Builds the heap and RSS comparison table.
415
+ *
416
+ * @param artifact - Parsed benchmark artifact.
417
+ * @returns Monospace markdown table body.
418
+ */
419
+ function buildHeapTable(artifact: BenchmarkArtifact): string {
420
+ const aggregatedResults = normalizeAggregated(artifact);
421
+ const orderedSizes = resolveOrderedSizes(aggregatedResults);
130
422
  const rows: string[] = [];
131
423
  rows.push(
132
424
  'Size Metric Src Bytes Dist Bytes Src MB Dist MB Δ Bytes Δ % Note',
@@ -134,10 +426,10 @@ function buildHeapTable(artifact: any): string {
134
426
  rows.push(
135
427
  '------ -------------- ------------ ------------ --------- --------- ----------- ------- -------------------------------------------------',
136
428
  );
137
- for (const size of ordered) {
138
- for (const metric of ['heapUsed', 'rss']) {
139
- const s = agg.src?.[size]?.all?.[metric]?.mean;
140
- const d = agg.dist?.[size]?.all?.[metric]?.mean;
429
+ for (const size of orderedSizes) {
430
+ for (const metric of HEAP_METRICS) {
431
+ const s = aggregatedResults.src[size]?.all?.[metric]?.mean;
432
+ const d = aggregatedResults.dist[size]?.all?.[metric]?.mean;
141
433
  if (typeof s !== 'number' || typeof d !== 'number') continue;
142
434
  const dBytes = d - s;
143
435
  const dPct = s === 0 ? 0 : (dBytes / s) * 100;
@@ -165,6 +457,55 @@ function buildHeapTable(artifact: any): string {
165
457
  return rows.join('\n');
166
458
  }
167
459
 
460
+ /**
461
+ * Compares size keys numerically, falling back to lexical comparison.
462
+ *
463
+ * @param left - Left size key.
464
+ * @param right - Right size key.
465
+ * @returns Standard comparator delta.
466
+ */
467
+ function compareNumericSizeKeys(left: string, right: string): number {
468
+ const leftValue = Number.parseInt(left, 10);
469
+ const rightValue = Number.parseInt(right, 10);
470
+
471
+ if (Number.isNaN(leftValue) || Number.isNaN(rightValue)) {
472
+ return left.localeCompare(right);
473
+ }
474
+
475
+ return leftValue - rightValue || left.localeCompare(right);
476
+ }
477
+
478
+ /**
479
+ * Narrows an unknown value to the legacy flat aggregated entry shape.
480
+ *
481
+ * @param value - Candidate legacy entry.
482
+ * @returns `true` when the value matches the legacy schema.
483
+ */
484
+ function isLegacyAggregatedEntry(
485
+ value: unknown,
486
+ ): value is LegacyAggregatedEntry {
487
+ return (
488
+ isRecord(value) &&
489
+ (value.mode === 'src' || value.mode === 'dist') &&
490
+ (typeof value.size === 'number' || typeof value.size === 'string')
491
+ );
492
+ }
493
+
494
+ /**
495
+ * Narrows an unknown value to a plain object record.
496
+ *
497
+ * @param value - Candidate value.
498
+ * @returns `true` when the value is a non-array object.
499
+ */
500
+ function isRecord(value: unknown): value is Record<string, unknown> {
501
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
502
+ }
503
+
504
+ /**
505
+ * Runs the artifact load and emits both markdown tables to standard output.
506
+ *
507
+ * @returns Nothing.
508
+ */
168
509
  function main() {
169
510
  const artifact = loadArtifact();
170
511
  if (!artifact) process.exit(1);