@7h3/protocol 0.4.0 → 0.5.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 (248) hide show
  1. package/README.md +6 -323
  2. package/agentAdapter.d.ts +26 -0
  3. package/auditLog.d.ts +54 -0
  4. package/capability.d.ts +67 -0
  5. package/cborCodec.d.ts +34 -0
  6. package/conformanceVectors.d.ts +20 -0
  7. package/encryption.d.ts +85 -0
  8. package/envelopeCbor.d.ts +33 -0
  9. package/frameworkAdapters.d.ts +72 -0
  10. package/gateway.d.ts +53 -0
  11. package/grpcBinding.d.ts +29 -0
  12. package/httpBinding.d.ts +50 -0
  13. package/index.d.ts +39 -0
  14. package/index.js +3662 -0
  15. package/keyInfra.d.ts +64 -0
  16. package/keyRegistry.d.ts +9 -0
  17. package/keyRotation.d.ts +20 -0
  18. package/mcpGateway.d.ts +37 -0
  19. package/mcpTransports.d.ts +62 -0
  20. package/mcpWrapper.d.ts +83 -0
  21. package/otel.d.ts +65 -0
  22. package/package.json +13 -79
  23. package/policyEnforcer.d.ts +50 -0
  24. package/policyTelemetryFeedback.d.ts +11 -0
  25. package/protocol.d.ts +66 -0
  26. package/protocolAgent.d.ts +58 -0
  27. package/protocolBinary.d.ts +8 -0
  28. package/protocolCapabilities.d.ts +24 -0
  29. package/protocolReplay.d.ts +35 -0
  30. package/protocolTransport.d.ts +73 -0
  31. package/queueBinding.d.ts +43 -0
  32. package/rateLimiter.d.ts +18 -0
  33. package/{src/redisClient.ts → redisClient.d.ts} +25 -75
  34. package/replayStores.d.ts +98 -0
  35. package/revocation.d.ts +71 -0
  36. package/routePolicy.d.ts +30 -0
  37. package/runtimePolicy.d.ts +24 -0
  38. package/runtimePolicyManager.d.ts +15 -0
  39. package/runtimePolicyPresets.d.ts +11 -0
  40. package/signedResponse.d.ts +21 -0
  41. package/stream.d.ts +59 -0
  42. package/telemetry.d.ts +79 -0
  43. package/webhookBinding.d.ts +30 -0
  44. package/wsBinding.d.ts +52 -0
  45. package/.dockerignore +0 -19
  46. package/.github/dependabot.yml +0 -32
  47. package/.github/workflows/ci.yml +0 -31
  48. package/.github/workflows/publish.yml +0 -59
  49. package/.github/workflows/scorecard.yml +0 -37
  50. package/7h3.example.yaml +0 -125
  51. package/CHANGELOG.md +0 -92
  52. package/CONTRIBUTING.md +0 -82
  53. package/Dockerfile +0 -73
  54. package/GOVERNANCE.md +0 -62
  55. package/SECURITY.md +0 -70
  56. package/bench-results/replay-cache-full-1777891033256.json +0 -10
  57. package/bench-results/replay-cache-full-1777896317488.json +0 -10
  58. package/bench-results/replay-cache-full-1777900993184.json +0 -10
  59. package/bench-results/replay-cache-full-1777901019285.json +0 -10
  60. package/bench-results/replay-cache-quick-1777870170126.json +0 -10
  61. package/bench-results/signature-profiles-quick-1775875160079.json +0 -85
  62. package/bench-results/signature-profiles-quick-1775983539716.json +0 -85
  63. package/bench-results/signature-profiles-quick-1776237913190.json +0 -85
  64. package/bench-results/wire-codecs-full-1777891019803.json +0 -93
  65. package/bench-results/wire-codecs-full-1777896260964.json +0 -93
  66. package/bench-results/wire-codecs-full-1777901004247.json +0 -93
  67. package/bench-results/wire-codecs-quick-1775972879056.json +0 -93
  68. package/bench-results/wire-codecs-quick-1775983541111.json +0 -93
  69. package/bench-results/wire-codecs-quick-1776237914299.json +0 -93
  70. package/bench-results/wire-codecs-quick-1777841285236.json +0 -93
  71. package/bench-results/wire-codecs-quick-1777841321772.json +0 -93
  72. package/bench-results/wire-codecs-quick-1777841330408.json +0 -93
  73. package/bench-results/wire-codecs-quick-1777852886082.json +0 -93
  74. package/bench-results/wire-codecs-quick-1777852988773.json +0 -93
  75. package/bench-results/wire-codecs-quick-1777870188095.json +0 -93
  76. package/bench-results/wire-codecs-quick-1777870263918.json +0 -93
  77. package/bench-results/wire-codecs-quick-1777870455034.json +0 -93
  78. package/bench-results/wire-codecs-quick-1778816163081.json +0 -93
  79. package/bench-results/wire-codecs-quick-1778843936130.json +0 -93
  80. package/bin/7h3.ts +0 -385
  81. package/conformance/7h3_v0_1.json +0 -77
  82. package/conformance/7h3_v0_1_binary.json +0 -20
  83. package/conformance/aip_v0_1_binary.json +0 -20
  84. package/docker-compose.yaml +0 -77
  85. package/docs/ADOPTION_PLAN.md +0 -120
  86. package/docs/AGENTS.md +0 -77
  87. package/docs/AIP_RFC_v0.1.md +0 -97
  88. package/docs/AI_DECISION_CARD.md +0 -122
  89. package/docs/AI_RUNTIME_POLICY.json +0 -126
  90. package/docs/AI_RUNTIME_POLICY.yaml +0 -110
  91. package/docs/BACKPRESSURE_TUNING.md +0 -65
  92. package/docs/BENCHMARK_CLAIM_MATRIX.md +0 -42
  93. package/docs/BENCHMARK_REPORT_TEMPLATE.md +0 -169
  94. package/docs/BINARY_CODEC_BENCH.md +0 -23
  95. package/docs/CLEAN_CLONE_RUNBOOK.md +0 -36
  96. package/docs/CLOCK_SKEW_POLICY.md +0 -30
  97. package/docs/DISTRIBUTED_REPLAY.md +0 -142
  98. package/docs/FUZZ_CAMPAIGN.md +0 -121
  99. package/docs/GATEWAY.md +0 -195
  100. package/docs/KEY_MANAGEMENT_POLICY.md +0 -53
  101. package/docs/KEY_REVOCATION.md +0 -69
  102. package/docs/MCP_WRAPPER.md +0 -159
  103. package/docs/MIGRATION_GUIDE.md +0 -40
  104. package/docs/OPERATORS.md +0 -184
  105. package/docs/PERF_REGRESSION_POLICY.md +0 -34
  106. package/docs/PROJECT_EXAMINATION_2026-05-31.md +0 -219
  107. package/docs/RELEASE_BENCHMARK_REPORT_2026-05-15.md +0 -135
  108. package/docs/RELEASE_GATE.md +0 -25
  109. package/docs/RELEASE_NOTES_v0.1.0.md +0 -54
  110. package/docs/SECURITY_REVIEW_2026-06-05.md +0 -165
  111. package/docs/TELEMETRY.md +0 -41
  112. package/docs/THREAT_MODEL.md +0 -89
  113. package/docs/VERSIONING_POLICY.md +0 -30
  114. package/docs/assets/banner.png +0 -0
  115. package/eslint.config.js +0 -15
  116. package/fuzz/ts/harness-decode.ts +0 -136
  117. package/fuzz/ts/harness-verify.ts +0 -121
  118. package/fuzz/ts/run.ts +0 -35
  119. package/mcp-server/README.md +0 -38
  120. package/mcp-server/package-lock.json +0 -1187
  121. package/mcp-server/package.json +0 -35
  122. package/mcp-server/src/index.ts +0 -236
  123. package/mcp-server/tsconfig.json +0 -14
  124. package/scripts/aip-framework-quickstart.ts +0 -110
  125. package/scripts/aip-mcp-gateway.ts +0 -38
  126. package/scripts/aip-mcp-wrap-demo.ts +0 -72
  127. package/scripts/aip-quickstart.ts +0 -60
  128. package/scripts/bench-diff.ts +0 -118
  129. package/scripts/bench-protocol-e2e.ts +0 -937
  130. package/scripts/bench-protocol-openloop.ts +0 -1397
  131. package/scripts/bench-replay-cache.ts +0 -76
  132. package/scripts/bench-signature-profiles.ts +0 -180
  133. package/scripts/bench-wire-codecs.ts +0 -161
  134. package/scripts/build-binary-conformance.ts +0 -36
  135. package/scripts/build-release-dashboard.ts +0 -175
  136. package/scripts/canary-rollout.ts +0 -38
  137. package/scripts/mcpGatewayCli.test.ts +0 -116
  138. package/scripts/prepare-aip-package.ts +0 -88
  139. package/scripts/regen-conformance-sigs.ts +0 -18
  140. package/scripts/release-gate.ts +0 -19
  141. package/scripts/validate-runtime-policy.ts +0 -18
  142. package/sdk/browser/index.test.ts +0 -162
  143. package/sdk/browser/index.ts +0 -257
  144. package/sdk/browser/package.json +0 -13
  145. package/sdk/go/go.mod +0 -3
  146. package/sdk/go/http.go +0 -135
  147. package/sdk/go/protocol.go +0 -324
  148. package/sdk/go/protocol_test.go +0 -334
  149. package/sdk/go/webhook.go +0 -136
  150. package/sdk/python/README.md +0 -18
  151. package/sdk/python/protocol_7h3/__init__.py +0 -46
  152. package/sdk/python/protocol_7h3/http.py +0 -212
  153. package/sdk/python/protocol_7h3/keys.py +0 -149
  154. package/sdk/python/protocol_7h3/protocol.py +0 -525
  155. package/sdk/python/protocol_7h3/queue.py +0 -118
  156. package/sdk/python/protocol_7h3/webhook.py +0 -116
  157. package/sdk/python/pyproject.toml +0 -40
  158. package/sdk/python/tests/test_conformance.py +0 -110
  159. package/sdk/python/tests/test_http.py +0 -305
  160. package/sdk/python/tests/test_keys.py +0 -417
  161. package/sdk/python/tests/test_queue.py +0 -120
  162. package/sdk/python/tests/test_webhook.py +0 -345
  163. package/sdk/rust/Cargo.lock +0 -371
  164. package/sdk/rust/Cargo.toml +0 -25
  165. package/sdk/rust/README.md +0 -31
  166. package/sdk/rust/fuzz/Cargo.toml +0 -29
  167. package/sdk/rust/fuzz/fuzz_targets/fuzz_canonicalize.rs +0 -46
  168. package/sdk/rust/fuzz/fuzz_targets/fuzz_decode.rs +0 -11
  169. package/sdk/rust/src/bin/aip_mcp_gateway.rs +0 -59
  170. package/sdk/rust/src/http.rs +0 -145
  171. package/sdk/rust/src/keys.rs +0 -161
  172. package/sdk/rust/src/lib.rs +0 -688
  173. package/sdk/rust/src/queue.rs +0 -79
  174. package/sdk/rust/src/webhook.rs +0 -86
  175. package/sdk/rust/tests/conformance.rs +0 -148
  176. package/sdk/rust/tests/gateway.rs +0 -130
  177. package/sdk/rust/tests/http_webhook_queue.rs +0 -201
  178. package/sdk/rust/tests/keys.rs +0 -189
  179. package/src/agentAdapter.test.ts +0 -48
  180. package/src/agentAdapter.ts +0 -56
  181. package/src/auditLog.test.ts +0 -145
  182. package/src/auditLog.ts +0 -147
  183. package/src/conformance.test.ts +0 -136
  184. package/src/conformanceVectors.ts +0 -99
  185. package/src/frameworkAdapters.test.ts +0 -290
  186. package/src/frameworkAdapters.ts +0 -261
  187. package/src/gateway.test.ts +0 -343
  188. package/src/gateway.ts +0 -171
  189. package/src/grpcBinding.test.ts +0 -211
  190. package/src/grpcBinding.ts +0 -103
  191. package/src/httpBinding.test.ts +0 -376
  192. package/src/httpBinding.ts +0 -163
  193. package/src/index.ts +0 -32
  194. package/src/keyInfra.test.ts +0 -278
  195. package/src/keyInfra.ts +0 -228
  196. package/src/keyRegistry.ts +0 -59
  197. package/src/keyRotation.test.ts +0 -78
  198. package/src/keyRotation.ts +0 -72
  199. package/src/mcpGateway.test.ts +0 -129
  200. package/src/mcpGateway.ts +0 -250
  201. package/src/mcpTransports.test.ts +0 -92
  202. package/src/mcpTransports.ts +0 -169
  203. package/src/mcpWrapper.test.ts +0 -179
  204. package/src/mcpWrapper.ts +0 -206
  205. package/src/policyEnforcer.test.ts +0 -99
  206. package/src/policyEnforcer.ts +0 -169
  207. package/src/policyTelemetryFeedback.test.ts +0 -25
  208. package/src/policyTelemetryFeedback.ts +0 -38
  209. package/src/protocol.bench.ts +0 -37
  210. package/src/protocol.test.ts +0 -155
  211. package/src/protocol.ts +0 -413
  212. package/src/protocolAgent.test.ts +0 -105
  213. package/src/protocolAgent.ts +0 -169
  214. package/src/protocolBinary.test.ts +0 -165
  215. package/src/protocolBinary.ts +0 -312
  216. package/src/protocolCapabilities.ts +0 -70
  217. package/src/protocolFuzz.advanced.test.ts +0 -235
  218. package/src/protocolFuzz.test.ts +0 -111
  219. package/src/protocolNegative.test.ts +0 -97
  220. package/src/protocolReplay.test.ts +0 -71
  221. package/src/protocolReplay.ts +0 -194
  222. package/src/protocolTransport.test.ts +0 -556
  223. package/src/protocolTransport.ts +0 -483
  224. package/src/queueBinding.test.ts +0 -130
  225. package/src/queueBinding.ts +0 -102
  226. package/src/rateLimiter.test.ts +0 -96
  227. package/src/rateLimiter.ts +0 -46
  228. package/src/redisIntegration.test.ts +0 -134
  229. package/src/replayStores.test.ts +0 -141
  230. package/src/replayStores.ts +0 -82
  231. package/src/revocation.test.ts +0 -98
  232. package/src/revocation.ts +0 -0
  233. package/src/routePolicy.test.ts +0 -87
  234. package/src/routePolicy.ts +0 -72
  235. package/src/runtimePolicy.test.ts +0 -49
  236. package/src/runtimePolicy.ts +0 -81
  237. package/src/runtimePolicyManager.test.ts +0 -29
  238. package/src/runtimePolicyManager.ts +0 -50
  239. package/src/runtimePolicyPresets.ts +0 -43
  240. package/src/signedResponse.test.ts +0 -111
  241. package/src/signedResponse.ts +0 -83
  242. package/src/webhookBinding.test.ts +0 -144
  243. package/src/webhookBinding.ts +0 -115
  244. package/src/wsBinding.test.ts +0 -221
  245. package/src/wsBinding.ts +0 -100
  246. package/tsconfig.json +0 -15
  247. package/tsconfig.lib.json +0 -23
  248. package/vite.lib.config.ts +0 -16
@@ -1,937 +0,0 @@
1
- import { mkdir, writeFile } from 'node:fs/promises'
2
- import http2 from 'node:http2'
3
- import { performance } from 'node:perf_hooks'
4
- import process from 'node:process'
5
- import { WebSocket, WebSocketServer } from 'ws'
6
- import {
7
- canonicalizeEnvelope,
8
- signCanonicalPayloadHmac,
9
- type ProtocolEnvelope,
10
- type ProtocolHeader,
11
- } from '../src/protocol'
12
- import { InMemoryReplayCache } from '../src/protocolReplay'
13
- import { InMemoryVerificationMaterialCache, decodeEnvelope, encodeEnvelope, receiveEnvelope } from '../src/protocolTransport'
14
-
15
- type ProfileName = 'quick' | 'full'
16
- type Mode = 'inproc' | 'http' | 'ws' | 'agent-loop'
17
-
18
- interface Profile {
19
- warmupMs: number
20
- measureMs: number
21
- }
22
-
23
- interface Scenario {
24
- mode: Mode
25
- payloadBytes: number
26
- concurrency: number
27
- }
28
-
29
- interface StageTotals {
30
- buildMs: number
31
- canonicalizeMs: number
32
- macMs: number
33
- queueMs: number
34
- transportMs: number
35
- verifyMs: number
36
- decodeMs: number
37
- }
38
-
39
- interface ScenarioResult {
40
- mode: Mode
41
- payloadBytes: number
42
- concurrency: number
43
- ops: number
44
- opsPerSecond: number
45
- p50Ms: number
46
- p95Ms: number
47
- p99Ms: number
48
- cpuMsPer10kOps: number
49
- heapDeltaMb: number
50
- rssDeltaMb: number
51
- allocBytesPerOp: number
52
- stageBuildMsPerOp: number
53
- stageCanonicalizeMsPerOp: number
54
- stageMacMsPerOp: number
55
- stageQueueMsPerOp: number
56
- stageTransportMsPerOp: number
57
- stageVerifyMsPerOp: number
58
- stageDecodeMsPerOp: number
59
- }
60
-
61
- interface TransportTrace {
62
- queueMs: number
63
- transportMs: number
64
- verifyMs: number
65
- decodeMs: number
66
- }
67
-
68
- interface TransportAdapter {
69
- start(): Promise<void>
70
- send(rawEnvelope: string): Promise<TransportTrace>
71
- stop(): Promise<void>
72
- }
73
-
74
- interface BenchEnvelopeTemplate {
75
- headerBase: Omit<ProtocolHeader, 'messageId' | 'timestampMs' | 'nonce'>
76
- intent: 'TASK'
77
- capability: string
78
- }
79
-
80
- interface ServerAck {
81
- ok: boolean
82
- queueMs: number
83
- verifyMs: number
84
- decodeMs: number
85
- error?: string
86
- }
87
-
88
- const ACK_HEADER_OK = 'x-aip-ok'
89
- const ACK_HEADER_QUEUE_MS = 'x-aip-queue-ms'
90
- const ACK_HEADER_VERIFY_MS = 'x-aip-verify-ms'
91
- const ACK_HEADER_DECODE_MS = 'x-aip-decode-ms'
92
-
93
- const SHARED_SECRET = 'bench-shared-secret'
94
- const SHARED_SECRET_RESOLVER = async (): Promise<string> => SHARED_SECRET
95
- const PROFILES: Record<ProfileName, Profile> = {
96
- quick: {
97
- warmupMs: 300,
98
- measureMs: 1500,
99
- },
100
- full: {
101
- warmupMs: 1200,
102
- measureMs: 8000,
103
- },
104
- }
105
-
106
- const PAYLOAD_SIZES = [256, 1024, 4096, 16384]
107
- const CONCURRENCY_LEVELS = [1, 10, 100, 1000]
108
- const MAX_LATENCY_SAMPLES = 200_000
109
-
110
- function parseProfile(argv: string[]): ProfileName {
111
- const idx = argv.indexOf('--profile')
112
- if (idx >= 0) {
113
- const value = argv[idx + 1]
114
- if (value === 'quick' || value === 'full') return value
115
- }
116
- return 'quick'
117
- }
118
-
119
- function parseCsvArg(flag: string, argv: string[]): string[] {
120
- const idx = argv.indexOf(flag)
121
- if (idx < 0) return []
122
- const raw = argv[idx + 1]
123
- if (!raw) return []
124
- return raw
125
- .split(',')
126
- .map((part) => part.trim())
127
- .filter((part) => part.length > 0)
128
- }
129
-
130
- function parseModeSelection(argv: string[], fallback: Mode[]): Mode[] {
131
- const rawModes = parseCsvArg('--modes', argv)
132
- if (rawModes.length === 0) return fallback
133
- const allowed = new Set<Mode>(['inproc', 'http', 'ws', 'agent-loop'])
134
- const parsed = rawModes.filter((raw): raw is Mode => allowed.has(raw as Mode))
135
- return parsed.length > 0 ? parsed : fallback
136
- }
137
-
138
- function parseNumberSelection(argv: string[], flag: string, fallback: number[]): number[] {
139
- const rawValues = parseCsvArg(flag, argv)
140
- if (rawValues.length === 0) return fallback
141
- const parsed = rawValues
142
- .map((raw) => Number(raw))
143
- .filter((value) => Number.isFinite(value) && value > 0)
144
- .map((value) => Math.floor(value))
145
- return parsed.length > 0 ? parsed : fallback
146
- }
147
-
148
- function payloadOfSize(bytes: number): string {
149
- if (bytes <= 0) return ''
150
- return 'x'.repeat(bytes)
151
- }
152
-
153
- function percentile(sorted: number[], p: number): number {
154
- if (sorted.length === 0) return 0
155
- const index = Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length))
156
- return sorted[index] ?? 0
157
- }
158
-
159
- function round3(value: number): number {
160
- return Number(value.toFixed(3))
161
- }
162
-
163
- function nowMs(): number {
164
- return Date.now()
165
- }
166
-
167
- function maybeGc(): void {
168
- const withGc = globalThis as unknown as { gc?: () => void }
169
- withGc.gc?.()
170
- }
171
-
172
- function emptyStageTotals(): StageTotals {
173
- return {
174
- buildMs: 0,
175
- canonicalizeMs: 0,
176
- macMs: 0,
177
- queueMs: 0,
178
- transportMs: 0,
179
- verifyMs: 0,
180
- decodeMs: 0,
181
- }
182
- }
183
-
184
- function addStageTotals(target: StageTotals, delta: TransportTrace & { buildMs: number; canonicalizeMs: number; macMs: number }): void {
185
- target.buildMs += delta.buildMs
186
- target.canonicalizeMs += delta.canonicalizeMs
187
- target.macMs += delta.macMs
188
- target.queueMs += delta.queueMs
189
- target.transportMs += delta.transportMs
190
- target.verifyMs += delta.verifyMs
191
- target.decodeMs += delta.decodeMs
192
- }
193
-
194
- function buildEnvelope(template: BenchEnvelopeTemplate, payload: string, sequence: number): Omit<ProtocolEnvelope, 'signature'> {
195
- return {
196
- header: {
197
- ...template.headerBase,
198
- messageId: `m-${sequence}`,
199
- timestampMs: nowMs(),
200
- nonce: `n-${sequence}`,
201
- },
202
- body: {
203
- intent: template.intent,
204
- content: payload,
205
- capability: template.capability,
206
- correlationId: `corr-${sequence}`,
207
- },
208
- }
209
- }
210
-
211
- function parseServerAck(raw: string): ServerAck {
212
- const parsed = JSON.parse(raw) as Partial<ServerAck>
213
- return {
214
- ok: parsed.ok === true,
215
- queueMs: parsed.queueMs ?? 0,
216
- verifyMs: parsed.verifyMs ?? 0,
217
- decodeMs: parsed.decodeMs ?? 0,
218
- error: parsed.error,
219
- }
220
- }
221
-
222
- class InProcessAdapter implements TransportAdapter {
223
- private readonly replayCache = new InMemoryReplayCache(1_000_000)
224
- private readonly verificationMaterialCache = new InMemoryVerificationMaterialCache()
225
-
226
- async start(): Promise<void> {}
227
-
228
- async send(rawEnvelope: string): Promise<TransportTrace> {
229
- const queueStart = performance.now()
230
- await new Promise<void>((resolve) => setImmediate(resolve))
231
- const queueMs = performance.now() - queueStart
232
-
233
- const decodeStart = performance.now()
234
- const decoded = decodeEnvelope(rawEnvelope)
235
- const decodeMs = performance.now() - decodeStart
236
- if (!decoded.ok || !decoded.envelope) {
237
- throw new Error(decoded.diagnostics.map((d) => d.message).join('; '))
238
- }
239
-
240
- const verifyStart = performance.now()
241
- const result = await receiveEnvelope(decoded.envelope, {
242
- nowMs: nowMs(),
243
- replayCache: this.replayCache,
244
- verificationMaterialCache: this.verificationMaterialCache,
245
- secretResolver: SHARED_SECRET_RESOLVER,
246
- })
247
- const verifyMs = performance.now() - verifyStart
248
- if (!result.ok) {
249
- throw new Error(result.diagnostics.map((d) => d.message).join('; '))
250
- }
251
-
252
- return {
253
- queueMs,
254
- transportMs: 0,
255
- verifyMs,
256
- decodeMs,
257
- }
258
- }
259
-
260
- async stop(): Promise<void> {}
261
- }
262
-
263
- class HttpAdapter implements TransportAdapter {
264
- private readonly replayCache = new InMemoryReplayCache(1_000_000)
265
- private readonly verificationMaterialCache = new InMemoryVerificationMaterialCache()
266
- private server: http2.Http2Server | null = null
267
- private client: http2.ClientHttp2Session | null = null
268
- private port = 0
269
- private readonly fastAck: boolean
270
-
271
- constructor(fastAck = true) {
272
- this.fastAck = fastAck
273
- }
274
-
275
- async start(): Promise<void> {
276
- this.server = http2.createServer({
277
- settings: {
278
- maxConcurrentStreams: 4096,
279
- },
280
- })
281
- this.server.on('stream', (stream, headers) => {
282
- if (headers[':method'] !== 'POST') {
283
- stream.respond({ ':status': 405 })
284
- stream.end()
285
- return
286
- }
287
-
288
- const queueStart = performance.now()
289
- const chunks: Buffer[] = []
290
- stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)))
291
- stream.on('end', async () => {
292
- const queueMs = performance.now() - queueStart
293
- const raw = Buffer.concat(chunks).toString('utf8')
294
-
295
- try {
296
- const decodeStart = performance.now()
297
- const decoded = decodeEnvelope(raw)
298
- const decodeMs = performance.now() - decodeStart
299
- if (!decoded.ok || !decoded.envelope) {
300
- stream.respond({ ':status': 400, 'content-type': 'application/json' })
301
- stream.end(JSON.stringify({ ok: false, queueMs, decodeMs, verifyMs: 0, error: 'decode failed' }))
302
- return
303
- }
304
-
305
- const verifyStart = performance.now()
306
- const result = await receiveEnvelope(decoded.envelope, {
307
- nowMs: nowMs(),
308
- replayCache: this.replayCache,
309
- verificationMaterialCache: this.verificationMaterialCache,
310
- secretResolver: SHARED_SECRET_RESOLVER,
311
- })
312
- const verifyMs = performance.now() - verifyStart
313
- const ack: ServerAck = {
314
- ok: result.ok,
315
- queueMs,
316
- decodeMs,
317
- verifyMs,
318
- error: result.ok ? undefined : result.diagnostics.map((d) => d.message).join('; '),
319
- }
320
-
321
- if (result.ok && this.fastAck) {
322
- stream.respond({
323
- ':status': 204,
324
- [ACK_HEADER_OK]: '1',
325
- [ACK_HEADER_QUEUE_MS]: String(queueMs),
326
- [ACK_HEADER_VERIFY_MS]: String(verifyMs),
327
- [ACK_HEADER_DECODE_MS]: String(decodeMs),
328
- })
329
- stream.end()
330
- return
331
- }
332
-
333
- stream.respond({ ':status': result.ok ? 200 : 400, 'content-type': 'application/json' })
334
- stream.end(JSON.stringify(ack))
335
- } catch {
336
- stream.respond({ ':status': 500, 'content-type': 'application/json' })
337
- stream.end(JSON.stringify({ ok: false, queueMs, decodeMs: 0, verifyMs: 0, error: 'internal http receive error' }))
338
- }
339
- })
340
- })
341
-
342
- await new Promise<void>((resolve, reject) => {
343
- this.server?.once('error', reject)
344
- this.server?.listen(0, '127.0.0.1', () => resolve())
345
- })
346
-
347
- const address = this.server.address()
348
- if (!address || typeof address === 'string') {
349
- throw new Error('Could not resolve HTTP server address')
350
- }
351
- this.port = address.port
352
-
353
- this.client = http2.connect(`http://127.0.0.1:${this.port}`)
354
- await new Promise<void>((resolve, reject) => {
355
- const client = this.client
356
- if (!client) {
357
- reject(new Error('Failed to initialize HTTP/2 client'))
358
- return
359
- }
360
- client.once('error', reject)
361
- client.once('connect', () => {
362
- client.off('error', reject)
363
- resolve()
364
- })
365
- })
366
- }
367
-
368
- async send(rawEnvelope: string): Promise<TransportTrace> {
369
- const transportStart = performance.now()
370
-
371
- if (!this.client) {
372
- throw new Error('HTTP/2 client not started')
373
- }
374
-
375
- const response = await new Promise<{ body: string; headers: http2.IncomingHttpHeaders }>((resolve, reject) => {
376
- const request = this.client!.request({
377
- ':method': 'POST',
378
- ':path': '/envelope',
379
- 'content-type': 'application/json',
380
- })
381
-
382
- let statusCode = 500
383
- let responseHeaders: http2.IncomingHttpHeaders = {}
384
- const chunks: Buffer[] = []
385
-
386
- request.on('response', (headers) => {
387
- const code = headers[':status']
388
- statusCode = typeof code === 'number' ? code : Number(code ?? 500)
389
- responseHeaders = headers
390
- })
391
- request.on('data', (chunk) => chunks.push(Buffer.from(chunk)))
392
- request.on('end', () => {
393
- const body = Buffer.concat(chunks).toString('utf8')
394
- if (statusCode >= 400) {
395
- reject(new Error(`HTTP status ${statusCode}: ${body}`))
396
- return
397
- }
398
- resolve({ body, headers: responseHeaders })
399
- })
400
-
401
- request.on('error', reject)
402
- request.end(rawEnvelope)
403
- })
404
- const transportMs = performance.now() - transportStart
405
-
406
- const ack = this.fastAck && readHeaderString(response.headers, ACK_HEADER_OK) === '1' ? ackFromHeaders(response.headers) : parseServerAck(response.body)
407
- if (!ack.ok) {
408
- throw new Error(ack.error ?? 'HTTP adapter received non-ok response')
409
- }
410
-
411
- return {
412
- queueMs: ack.queueMs,
413
- transportMs,
414
- verifyMs: ack.verifyMs,
415
- decodeMs: ack.decodeMs,
416
- }
417
- }
418
-
419
- async stop(): Promise<void> {
420
- if (this.client) {
421
- await new Promise<void>((resolve) => {
422
- const client = this.client
423
- if (!client) {
424
- resolve()
425
- return
426
- }
427
- client.once('close', () => resolve())
428
- client.close()
429
- })
430
- this.client = null
431
- }
432
- if (!this.server) return
433
- await new Promise<void>((resolve, reject) => {
434
- this.server?.close((error) => (error ? reject(error) : resolve()))
435
- })
436
- this.server = null
437
- }
438
- }
439
-
440
- function readHeaderString(headers: http2.IncomingHttpHeaders, key: string): string | undefined {
441
- const value = headers[key]
442
- if (typeof value === 'string') return value
443
- if (typeof value === 'number') return String(value)
444
- if (Array.isArray(value)) {
445
- const first = value[0]
446
- return typeof first === 'string' ? first : undefined
447
- }
448
- return undefined
449
- }
450
-
451
- function readHeaderNumber(headers: http2.IncomingHttpHeaders, key: string): number {
452
- const raw = readHeaderString(headers, key)
453
- if (!raw) return 0
454
- const parsed = Number(raw)
455
- return Number.isFinite(parsed) ? parsed : 0
456
- }
457
-
458
- function ackFromHeaders(headers: http2.IncomingHttpHeaders): ServerAck {
459
- return {
460
- ok: readHeaderString(headers, ACK_HEADER_OK) === '1',
461
- queueMs: readHeaderNumber(headers, ACK_HEADER_QUEUE_MS),
462
- verifyMs: readHeaderNumber(headers, ACK_HEADER_VERIFY_MS),
463
- decodeMs: readHeaderNumber(headers, ACK_HEADER_DECODE_MS),
464
- }
465
- }
466
-
467
- class WsAdapter implements TransportAdapter {
468
- private readonly replayCache = new InMemoryReplayCache(1_000_000)
469
- private readonly verificationMaterialCache = new InMemoryVerificationMaterialCache()
470
- private server: WebSocketServer | null = null
471
- private client: WebSocket | null = null
472
- private nextId = 1
473
- private readonly pending = new Map<number, { resolve: (trace: TransportTrace) => void; reject: (error: Error) => void }>()
474
-
475
- async start(): Promise<void> {
476
- this.server = new WebSocketServer({ port: 0, host: '127.0.0.1' })
477
-
478
- this.server.on('connection', (socket) => {
479
- socket.on('message', async (data) => {
480
- const queueStart = performance.now()
481
- let message: { id: number; payload: string }
482
- try {
483
- message = JSON.parse(Buffer.isBuffer(data) ? data.toString('utf8') : String(data)) as {
484
- id: number
485
- payload: string
486
- }
487
- } catch {
488
- socket.send(Buffer.from(JSON.stringify({ id: 0, ok: false, error: 'invalid frame' }), 'utf8'))
489
- return
490
- }
491
-
492
- const queueMs = performance.now() - queueStart
493
- try {
494
- const decodeStart = performance.now()
495
- const decoded = decodeEnvelope(message.payload)
496
- const decodeMs = performance.now() - decodeStart
497
- if (!decoded.ok || !decoded.envelope) {
498
- socket.send(
499
- Buffer.from(
500
- JSON.stringify({ id: message.id, ok: false, queueMs, decodeMs, verifyMs: 0, error: 'decode failed' }),
501
- 'utf8',
502
- ),
503
- )
504
- return
505
- }
506
-
507
- const verifyStart = performance.now()
508
- const result = await receiveEnvelope(decoded.envelope, {
509
- nowMs: nowMs(),
510
- replayCache: this.replayCache,
511
- verificationMaterialCache: this.verificationMaterialCache,
512
- secretResolver: SHARED_SECRET_RESOLVER,
513
- })
514
- const verifyMs = performance.now() - verifyStart
515
-
516
- socket.send(
517
- Buffer.from(
518
- JSON.stringify({
519
- id: message.id,
520
- ok: result.ok,
521
- queueMs,
522
- decodeMs,
523
- verifyMs,
524
- error: result.ok ? undefined : result.diagnostics.map((d) => d.message).join('; '),
525
- }),
526
- 'utf8',
527
- ),
528
- )
529
- } catch {
530
- socket.send(
531
- Buffer.from(
532
- JSON.stringify({ id: message.id, ok: false, queueMs, decodeMs: 0, verifyMs: 0, error: 'internal ws receive error' }),
533
- 'utf8',
534
- ),
535
- )
536
- }
537
- })
538
- })
539
-
540
- const port = await new Promise<number>((resolve, reject) => {
541
- this.server?.once('error', reject)
542
- this.server?.once('listening', () => {
543
- const address = this.server?.address()
544
- if (!address || typeof address === 'string') {
545
- reject(new Error('Could not resolve WS server address'))
546
- return
547
- }
548
- resolve(address.port)
549
- })
550
- })
551
-
552
- this.client = await new Promise<WebSocket>((resolve, reject) => {
553
- const socket = new WebSocket(`ws://127.0.0.1:${port}`)
554
- socket.once('open', () => resolve(socket))
555
- socket.once('error', reject)
556
- })
557
-
558
- this.client.on('message', (data) => {
559
- let frame: { id: number; ok: boolean; queueMs: number; verifyMs: number; decodeMs: number; error?: string }
560
- try {
561
- frame = JSON.parse(Buffer.isBuffer(data) ? data.toString('utf8') : String(data)) as {
562
- id: number
563
- ok: boolean
564
- queueMs: number
565
- verifyMs: number
566
- decodeMs: number
567
- error?: string
568
- }
569
- } catch {
570
- return
571
- }
572
- const pending = this.pending.get(frame.id)
573
- if (!pending) return
574
- this.pending.delete(frame.id)
575
- if (!frame.ok) {
576
- pending.reject(new Error(frame.error ?? 'ws adapter received non-ok response'))
577
- return
578
- }
579
- pending.resolve({
580
- queueMs: frame.queueMs,
581
- transportMs: 0,
582
- verifyMs: frame.verifyMs,
583
- decodeMs: frame.decodeMs,
584
- })
585
- })
586
- }
587
-
588
- async send(rawEnvelope: string): Promise<TransportTrace> {
589
- if (!this.client || this.client.readyState !== WebSocket.OPEN) {
590
- throw new Error('WebSocket client is not open')
591
- }
592
-
593
- const id = this.nextId
594
- this.nextId += 1
595
- const sendStart = performance.now()
596
-
597
- const completion = new Promise<TransportTrace>((resolve, reject) => {
598
- this.pending.set(id, { resolve, reject })
599
- })
600
-
601
- this.client.send(Buffer.from(JSON.stringify({ id, payload: rawEnvelope }), 'utf8'))
602
- const trace = await completion
603
- return {
604
- ...trace,
605
- transportMs: performance.now() - sendStart,
606
- }
607
- }
608
-
609
- async stop(): Promise<void> {
610
- if (this.client) {
611
- await new Promise<void>((resolve) => {
612
- this.client?.once('close', () => resolve())
613
- this.client?.close()
614
- })
615
- this.client = null
616
- }
617
-
618
- if (this.server) {
619
- await new Promise<void>((resolve, reject) => {
620
- this.server?.close((error) => (error ? reject(error) : resolve()))
621
- })
622
- this.server = null
623
- }
624
- }
625
- }
626
-
627
- async function runAgentLoop(payload: string): Promise<void> {
628
- const input = payload
629
- const plan = `plan:${input.length}`
630
- const toolResult = await Promise.resolve(plan.length + input.length)
631
- const memoryUpdate = `${plan}:${toolResult}`
632
- const response = `ok:${memoryUpdate}`
633
- if (response.length === 0) {
634
- throw new Error('unexpected empty response')
635
- }
636
- }
637
-
638
- async function buildSignAndEncodeEnvelope(
639
- template: BenchEnvelopeTemplate,
640
- payload: string,
641
- sequence: number,
642
- ): Promise<{
643
- raw: string
644
- buildMs: number
645
- canonicalizeMs: number
646
- macMs: number
647
- }> {
648
- const buildStart = performance.now()
649
- const unsigned = buildEnvelope(template, payload, sequence)
650
- const buildMs = performance.now() - buildStart
651
-
652
- const canonicalizeStart = performance.now()
653
- const canonicalPayload = canonicalizeEnvelope(unsigned)
654
- const canonicalizeMs = performance.now() - canonicalizeStart
655
-
656
- const macStart = performance.now()
657
- const signature = await signCanonicalPayloadHmac(canonicalPayload, SHARED_SECRET)
658
- const macMs = performance.now() - macStart
659
-
660
- const signedEnvelope: ProtocolEnvelope = {
661
- ...unsigned,
662
- signature: {
663
- alg: 'HS256',
664
- keyId: 'bench-k1',
665
- value: signature,
666
- },
667
- }
668
-
669
- return {
670
- raw: encodeEnvelope(signedEnvelope, 'compact'),
671
- buildMs,
672
- canonicalizeMs,
673
- macMs,
674
- }
675
- }
676
-
677
- async function runPhase(options: {
678
- durationMs: number
679
- concurrency: number
680
- payload: string
681
- execute: (payload: string) => Promise<TransportTrace & { buildMs: number; canonicalizeMs: number; macMs: number }>
682
- collectLatencies: boolean
683
- }): Promise<{ latencies: number[]; completed: number; stages: StageTotals }> {
684
- const latencies: number[] = []
685
- const stages = emptyStageTotals()
686
- const deadline = performance.now() + options.durationMs
687
- let inFlight = 0
688
- let completed = 0
689
-
690
- return new Promise((resolve, reject) => {
691
- let settled = false
692
-
693
- const finish = () => {
694
- if (settled) return
695
- settled = true
696
- resolve({ latencies, completed, stages })
697
- }
698
-
699
- const maybeLaunch = () => {
700
- while (inFlight < options.concurrency && performance.now() < deadline) {
701
- inFlight += 1
702
- const opStart = performance.now()
703
-
704
- options
705
- .execute(options.payload)
706
- .then((trace) => {
707
- completed += 1
708
- addStageTotals(stages, trace)
709
- if (options.collectLatencies) {
710
- const elapsed = performance.now() - opStart
711
- if (latencies.length < MAX_LATENCY_SAMPLES) {
712
- latencies.push(elapsed)
713
- } else {
714
- const sampleIndex = Math.floor(Math.random() * completed)
715
- if (sampleIndex < MAX_LATENCY_SAMPLES) {
716
- latencies[sampleIndex] = elapsed
717
- }
718
- }
719
- }
720
- })
721
- .catch((error: unknown) => {
722
- if (settled) return
723
- settled = true
724
- reject(error)
725
- })
726
- .finally(() => {
727
- inFlight -= 1
728
- if (settled) return
729
- if (performance.now() < deadline) {
730
- maybeLaunch()
731
- return
732
- }
733
- if (inFlight === 0) {
734
- finish()
735
- }
736
- })
737
- }
738
- }
739
-
740
- maybeLaunch()
741
- if (inFlight === 0) {
742
- finish()
743
- }
744
- })
745
- }
746
-
747
- async function runScenario(profile: Profile, scenario: Scenario): Promise<ScenarioResult> {
748
- const payload = payloadOfSize(scenario.payloadBytes)
749
- const template: BenchEnvelopeTemplate = {
750
- headerBase: {
751
- version: '7h3/0.1',
752
- ttlMs: 120_000,
753
- sender: 'bench.sender',
754
- recipient: 'bench.receiver',
755
- },
756
- intent: 'TASK',
757
- capability: 'task.plan',
758
- }
759
-
760
- const adapter: TransportAdapter | null =
761
- scenario.mode === 'inproc' ? new InProcessAdapter() : scenario.mode === 'http' ? new HttpAdapter() : scenario.mode === 'ws' ? new WsAdapter() : null
762
-
763
- try {
764
- if (adapter) await adapter.start()
765
-
766
- let sequence = 0
767
- const execute = async (content: string): Promise<TransportTrace & { buildMs: number; canonicalizeMs: number; macMs: number }> => {
768
- if (scenario.mode === 'agent-loop') {
769
- const buildStart = performance.now()
770
- await runAgentLoop(content)
771
- return {
772
- buildMs: performance.now() - buildStart,
773
- canonicalizeMs: 0,
774
- macMs: 0,
775
- queueMs: 0,
776
- transportMs: 0,
777
- verifyMs: 0,
778
- decodeMs: 0,
779
- }
780
- }
781
-
782
- sequence += 1
783
- const built = await buildSignAndEncodeEnvelope(template, content, sequence)
784
- const trace = await adapter!.send(built.raw)
785
- return {
786
- ...trace,
787
- buildMs: built.buildMs,
788
- canonicalizeMs: built.canonicalizeMs,
789
- macMs: built.macMs,
790
- }
791
- }
792
-
793
- await runPhase({
794
- durationMs: profile.warmupMs,
795
- concurrency: scenario.concurrency,
796
- payload,
797
- execute,
798
- collectLatencies: false,
799
- })
800
-
801
- maybeGc()
802
- const cpuStart = process.cpuUsage()
803
- const memStart = process.memoryUsage()
804
-
805
- const measured = await runPhase({
806
- durationMs: profile.measureMs,
807
- concurrency: scenario.concurrency,
808
- payload,
809
- execute,
810
- collectLatencies: true,
811
- })
812
-
813
- const cpuEnd = process.cpuUsage(cpuStart)
814
- const memEnd = process.memoryUsage()
815
- const elapsedSeconds = profile.measureMs / 1000
816
-
817
- const sortedLatencies = measured.latencies.slice().sort((a, b) => a - b)
818
- const completed = measured.completed
819
- const opsPerSecond = completed / elapsedSeconds
820
- const cpuMs = (cpuEnd.user + cpuEnd.system) / 1000
821
- const stageScale = completed > 0 ? 1 / completed : 0
822
-
823
- const cpuMsPer10kOps = completed > 0 ? cpuMs / (completed / 10_000) : 0
824
- const heapDeltaBytes = memEnd.heapUsed - memStart.heapUsed
825
- const rssDeltaBytes = memEnd.rss - memStart.rss
826
-
827
- return {
828
- mode: scenario.mode,
829
- payloadBytes: scenario.payloadBytes,
830
- concurrency: scenario.concurrency,
831
- ops: completed,
832
- opsPerSecond: round3(opsPerSecond),
833
- p50Ms: round3(percentile(sortedLatencies, 50)),
834
- p95Ms: round3(percentile(sortedLatencies, 95)),
835
- p99Ms: round3(percentile(sortedLatencies, 99)),
836
- cpuMsPer10kOps: round3(cpuMsPer10kOps),
837
- heapDeltaMb: round3(heapDeltaBytes / (1024 * 1024)),
838
- rssDeltaMb: round3(rssDeltaBytes / (1024 * 1024)),
839
- allocBytesPerOp: round3(completed > 0 ? heapDeltaBytes / completed : 0),
840
- stageBuildMsPerOp: round3(measured.stages.buildMs * stageScale),
841
- stageCanonicalizeMsPerOp: round3(measured.stages.canonicalizeMs * stageScale),
842
- stageMacMsPerOp: round3(measured.stages.macMs * stageScale),
843
- stageQueueMsPerOp: round3(measured.stages.queueMs * stageScale),
844
- stageTransportMsPerOp: round3(measured.stages.transportMs * stageScale),
845
- stageVerifyMsPerOp: round3(measured.stages.verifyMs * stageScale),
846
- stageDecodeMsPerOp: round3(measured.stages.decodeMs * stageScale),
847
- }
848
- } finally {
849
- if (adapter) await adapter.stop()
850
- }
851
- }
852
-
853
- function renderMarkdown(profileName: ProfileName, profile: Profile, rows: ScenarioResult[]): string {
854
- const lines: string[] = []
855
- lines.push(`# Protocol E2E Benchmark (${profileName})`)
856
- lines.push('')
857
- lines.push(`- Warmup: ${profile.warmupMs}ms`)
858
- lines.push(`- Measure: ${profile.measureMs}ms`)
859
- lines.push('- Pipeline: build -> canonicalize -> MAC -> queue -> transport -> verify -> decode')
860
- lines.push('')
861
- lines.push('| mode | payload | concurrency | ops/s | p50 ms | p95 ms | p99 ms | cpu/10k ms | build ms/op | canon ms/op | mac ms/op | queue ms/op | transport ms/op | verify ms/op | decode ms/op |')
862
- lines.push('| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |')
863
-
864
- for (const row of rows) {
865
- lines.push(
866
- `| ${row.mode} | ${row.payloadBytes} | ${row.concurrency} | ${row.opsPerSecond} | ${row.p50Ms} | ${row.p95Ms} | ${row.p99Ms} | ${row.cpuMsPer10kOps} | ${row.stageBuildMsPerOp} | ${row.stageCanonicalizeMsPerOp} | ${row.stageMacMsPerOp} | ${row.stageQueueMsPerOp} | ${row.stageTransportMsPerOp} | ${row.stageVerifyMsPerOp} | ${row.stageDecodeMsPerOp} |`,
867
- )
868
- }
869
-
870
- return `${lines.join('\n')}\n`
871
- }
872
-
873
- async function main(): Promise<void> {
874
- const profileName = parseProfile(process.argv)
875
- const profile = PROFILES[profileName]
876
- if (!profile) {
877
- throw new Error(`Unknown profile '${profileName}'`)
878
- }
879
-
880
- const modes = parseModeSelection(process.argv, ['inproc', 'http', 'ws', 'agent-loop'])
881
- const payloadSizes = parseNumberSelection(process.argv, '--payloads', PAYLOAD_SIZES)
882
- const concurrencyLevels = parseNumberSelection(process.argv, '--concurrency', CONCURRENCY_LEVELS)
883
- const scenarios: Scenario[] = []
884
- for (const mode of modes) {
885
- for (const payloadBytes of payloadSizes) {
886
- for (const concurrency of concurrencyLevels) {
887
- scenarios.push({ mode, payloadBytes, concurrency })
888
- }
889
- }
890
- }
891
-
892
- const rows: ScenarioResult[] = []
893
- for (const scenario of scenarios) {
894
- const label = `[${scenario.mode}] payload=${scenario.payloadBytes}B concurrency=${scenario.concurrency}`
895
- console.log(`Running ${label}`)
896
- const result = await runScenario(profile, scenario)
897
- rows.push(result)
898
- console.log(
899
- ` ops/s=${result.opsPerSecond} p99=${result.p99Ms}ms build=${result.stageBuildMsPerOp}ms canon=${result.stageCanonicalizeMsPerOp}ms mac=${result.stageMacMsPerOp}ms transport=${result.stageTransportMsPerOp}ms verify=${result.stageVerifyMsPerOp}ms`,
900
- )
901
- }
902
-
903
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
904
- const outputDir = 'dist/bench'
905
- await mkdir(outputDir, { recursive: true })
906
-
907
- const jsonPath = `${outputDir}/protocol-e2e.${profileName}.${timestamp}.json`
908
- const mdPath = `${outputDir}/protocol-e2e.${profileName}.${timestamp}.md`
909
- const latestJsonPath = `${outputDir}/protocol-e2e.${profileName}.latest.json`
910
- const latestMdPath = `${outputDir}/protocol-e2e.${profileName}.latest.md`
911
-
912
- const payload = {
913
- profile: profileName,
914
- warmupMs: profile.warmupMs,
915
- measureMs: profile.measureMs,
916
- results: rows,
917
- }
918
-
919
- await writeFile(jsonPath, JSON.stringify(payload, null, 2), 'utf8')
920
- await writeFile(latestJsonPath, JSON.stringify(payload, null, 2), 'utf8')
921
-
922
- const markdown = renderMarkdown(profileName, profile, rows)
923
- await writeFile(mdPath, markdown, 'utf8')
924
- await writeFile(latestMdPath, markdown, 'utf8')
925
-
926
- console.log('')
927
- console.log(`Wrote ${jsonPath}`)
928
- console.log(`Wrote ${mdPath}`)
929
- console.log(`Updated ${latestJsonPath}`)
930
- console.log(`Updated ${latestMdPath}`)
931
- }
932
-
933
- main().catch((error: unknown) => {
934
- const message = error instanceof Error ? error.stack ?? error.message : String(error)
935
- console.error(message)
936
- process.exitCode = 1
937
- })