castrum 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -59
  3. package/castrum.linux-x64-gnu.node +0 -0
  4. package/dist/index.d.ts +3161 -0
  5. package/dist/index.js +7306 -0
  6. package/docs/ARCHITECTURE.md +415 -0
  7. package/docs/BENCHMARKS.md +307 -0
  8. package/docs/ENVIRONMENT.md +79 -0
  9. package/docs/GETTING_STARTED.md +209 -0
  10. package/docs/INGRESS.md +745 -0
  11. package/docs/REPO_MAP.md +259 -0
  12. package/docs/adr/0001-two-wire-formats.md +39 -0
  13. package/docs/adr/0002-zero-dom-fast-schema.md +38 -0
  14. package/docs/adr/0003-bun-builtins-delegation.md +42 -0
  15. package/docs/bun-builtins-decision-matrix.md +108 -0
  16. package/index.ts +51 -8
  17. package/package.json +73 -20
  18. package/src/data/json-rows.ts +13 -13
  19. package/src/ingress/body.ts +149 -0
  20. package/src/ingress/constants.ts +220 -48
  21. package/src/ingress/context.ts +136 -0
  22. package/src/ingress/decode/baked-result.ts +121 -0
  23. package/src/ingress/decode/fast-result.ts +160 -0
  24. package/src/ingress/decode/packed-sections.ts +56 -0
  25. package/src/ingress/decode/result-base.ts +172 -0
  26. package/src/ingress/errors.ts +72 -0
  27. package/src/ingress/fast.ts +152 -1021
  28. package/src/ingress/handlers.ts +563 -1001
  29. package/src/ingress/headers/baked-templates.ts +102 -0
  30. package/src/ingress/headers/cors.ts +44 -0
  31. package/src/ingress/headers/fast-templates.ts +238 -0
  32. package/src/ingress/headers/hsts.ts +83 -0
  33. package/src/ingress/health.ts +67 -0
  34. package/src/ingress/index.ts +130 -405
  35. package/src/ingress/metrics.ts +132 -0
  36. package/src/ingress/options.ts +197 -0
  37. package/src/ingress/packing/gather-raw-headers.ts +75 -0
  38. package/src/ingress/packing/header-packing.ts +75 -0
  39. package/src/ingress/packing/input-packer.ts +150 -0
  40. package/src/ingress/packing/scratch.ts +50 -0
  41. package/src/ingress/packing/select-headers.ts +96 -0
  42. package/src/ingress/response/error-bodies.ts +75 -0
  43. package/src/ingress/response/terminal.ts +59 -0
  44. package/src/ingress/routes/common.ts +38 -0
  45. package/src/ingress/routes/delete.ts +14 -0
  46. package/src/ingress/routes/echo.ts +107 -0
  47. package/src/ingress/routes/fallback.ts +23 -0
  48. package/src/ingress/routes/head.ts +37 -0
  49. package/src/ingress/routes/index.ts +10 -0
  50. package/src/ingress/routes/json-write.ts +153 -0
  51. package/src/ingress/routes/options.ts +36 -0
  52. package/src/ingress/routes/read.ts +45 -0
  53. package/src/ingress/server-node.ts +376 -0
  54. package/src/ingress/server.ts +443 -0
  55. package/src/ingress/shared.ts +132 -0
  56. package/src/ingress/status.ts +97 -0
  57. package/src/ingress/types.ts +157 -0
  58. package/src/integration/batch.ts +58 -0
  59. package/src/integration/index.ts +29 -0
  60. package/src/integration/pipeline.ts +217 -0
  61. package/src/integration/streaming.ts +79 -0
  62. package/src/integration/websocket.ts +92 -0
  63. package/src/loader/batch.ts +133 -0
  64. package/src/loader/cost.ts +104 -0
  65. package/src/loader/index.ts +930 -0
  66. package/src/loader/ops.ts +657 -0
  67. package/src/native/ffi.ts +1541 -0
  68. package/src/native/index.ts +8 -207
  69. package/src/native/loader.ts +194 -0
  70. package/src/native/types.ts +598 -0
  71. package/src/rust-ffi/addon.ts +10 -0
  72. package/src/rust-ffi/batch.ts +431 -0
  73. package/src/rust-ffi/client.ts +88 -0
  74. package/src/rust-ffi/context.ts +164 -0
  75. package/src/rust-ffi/index.ts +40 -541
  76. package/src/rust-ffi/options.ts +113 -0
  77. package/src/rust-ffi/packed.ts +253 -0
  78. package/src/rust-ffi/proven.ts +42 -0
  79. package/src/rust-ffi/scalar/crypto.ts +265 -0
  80. package/src/rust-ffi/scalar/factories.ts +233 -0
  81. package/src/rust-ffi/scalar/hashing.ts +72 -0
  82. package/src/rust-ffi/scalar/http.ts +206 -0
  83. package/src/rust-ffi/scalar/index.ts +31 -0
  84. package/src/rust-ffi/scalar/interface.ts +450 -0
  85. package/src/rust-ffi/scalar/json.ts +45 -0
  86. package/src/rust-ffi/scalar/payload.ts +124 -0
  87. package/src/rust-ffi/text.ts +95 -0
  88. package/src/selection.json +194 -0
  89. package/src/selection.ts +77 -0
  90. package/src/shared/adaptive.ts +68 -0
  91. package/src/shared/bench-classify.ts +209 -0
  92. package/src/shared/buffer-pool.ts +209 -0
  93. package/src/shared/bytes.ts +35 -5
  94. package/src/shared/env.ts +20 -0
  95. package/src/shared/log.ts +74 -0
  96. package/src/shared/metrics.ts +247 -0
  97. package/src/shared/packed.ts +350 -154
  98. package/src/shared/proven.ts +596 -0
  99. package/src/shared/request-id.ts +67 -0
  100. package/src/shared/response.ts +104 -0
  101. package/src/shared/runtime.ts +34 -0
  102. package/src/shared/trace.ts +81 -0
  103. package/src/shared/uuid.ts +31 -0
  104. package/Cargo.toml +0 -73
  105. package/bench.ts +0 -3
  106. package/rust/batch.rs +0 -195
  107. package/rust/cookie_parser.rs +0 -70
  108. package/rust/cors.rs +0 -263
  109. package/rust/hashing.rs +0 -164
  110. package/rust/headers.rs +0 -234
  111. package/rust/hmac_sha256.rs +0 -113
  112. package/rust/http_parser.rs +0 -132
  113. package/rust/ingress.rs +0 -616
  114. package/rust/ingress_constants.rs +0 -89
  115. package/rust/ip_trust.rs +0 -185
  116. package/rust/json_ops.rs +0 -66
  117. package/rust/json_patch_ops.rs +0 -25
  118. package/rust/json_schema.rs +0 -151
  119. package/rust/json_ser.rs +0 -429
  120. package/rust/lib.rs +0 -48
  121. package/rust/method.rs +0 -141
  122. package/rust/mime_lookup.rs +0 -76
  123. package/rust/output.rs +0 -238
  124. package/rust/proxy.rs +0 -90
  125. package/rust/query_parser.rs +0 -141
  126. package/rust/random_token.rs +0 -52
  127. package/rust/rate_limit.rs +0 -203
  128. package/rust/terminal.rs +0 -95
  129. package/rust/unit_tests.rs +0 -843
  130. package/rust/url_codec.rs +0 -133
  131. package/rust/util.rs +0 -537
  132. package/rust/validation.rs +0 -107
  133. package/rust/websocket.rs +0 -23
  134. package/src/baseline/index.ts +0 -12
  135. package/src/baseline/tasks/cookie.ts +0 -22
  136. package/src/baseline/tasks/hashing.ts +0 -18
  137. package/src/baseline/tasks/hmac.ts +0 -38
  138. package/src/baseline/tasks/http.ts +0 -62
  139. package/src/baseline/tasks/json-patch.ts +0 -12
  140. package/src/baseline/tasks/json.ts +0 -50
  141. package/src/baseline/tasks/mime.ts +0 -5
  142. package/src/baseline/tasks/query.ts +0 -14
  143. package/src/baseline/tasks/token.ts +0 -15
  144. package/src/baseline/tasks/url.ts +0 -11
  145. package/src/baseline/tasks/validation.ts +0 -19
  146. package/src/baseline/tasks/websocket.ts +0 -14
  147. package/src/bench/assert.ts +0 -70
  148. package/src/bench/checks.ts +0 -227
  149. package/src/bench/checksum.ts +0 -27
  150. package/src/bench/comparisons.ts +0 -70
  151. package/src/bench/concurrent-worker.ts +0 -135
  152. package/src/bench/fixtures.ts +0 -233
  153. package/src/bench/index.ts +0 -2
  154. package/src/bench/measure.ts +0 -177
  155. package/src/bench/now.ts +0 -3
  156. package/src/bench/report.ts +0 -79
  157. package/src/bench/run.ts +0 -57
  158. package/src/bench/tasks/complex.ts +0 -232
  159. package/src/bench/tasks/concurrent.ts +0 -213
  160. package/src/bench/tasks/cookie.ts +0 -44
  161. package/src/bench/tasks/hashing.ts +0 -33
  162. package/src/bench/tasks/hmac.ts +0 -36
  163. package/src/bench/tasks/http.ts +0 -50
  164. package/src/bench/tasks/index.ts +0 -52
  165. package/src/bench/tasks/json-patch.ts +0 -21
  166. package/src/bench/tasks/json.ts +0 -33
  167. package/src/bench/tasks/mime.ts +0 -21
  168. package/src/bench/tasks/query.ts +0 -44
  169. package/src/bench/tasks/stress.ts +0 -142
  170. package/src/bench/tasks/token.ts +0 -20
  171. package/src/bench/tasks/url.ts +0 -34
  172. package/src/bench/tasks/validation.ts +0 -57
  173. package/src/bench/tasks/websocket.ts +0 -21
  174. package/src/bench/types.ts +0 -60
  175. package/src/ingress/packed-input.ts +0 -59
  176. package/src/shared/json.ts +0 -18
@@ -0,0 +1,95 @@
1
+ // src/rust-ffi/text.ts — String-oriented FFI namespace (text in → text out).
2
+ //
3
+ // `rust.text.*` is the ergonomic string API over the byte-level native
4
+ // functions. Pure wrappers: encode input, call native, decode output.
5
+
6
+ import { decoder, encoder } from '../shared/bytes'
7
+ import { isBun } from '../shared/runtime'
8
+ import { resolveNative, type RustClientContext } from './context'
9
+
10
+ /** String-oriented FFI namespace. */
11
+ export interface RustText {
12
+ /**
13
+ * Resolve a lowercase file extension to a MIME type string (e.g. `"html"` →
14
+ * `"text/html"`). Unknown extensions return `"application/octet-stream"`.
15
+ * @param ext the file extension WITHOUT a leading dot, case-insensitive.
16
+ * @returns the MIME type for `ext`.
17
+ */
18
+ mimeFromExtension(ext: string): string
19
+ /**
20
+ * RFC 3986 percent-encode a string (the `encodeURIComponent` unreserved
21
+ * set). Under Bun this delegates to the native `encodeURIComponent` for
22
+ * ~4x speed with byte-for-byte parity.
23
+ * @param input the string to encode.
24
+ * @returns the percent-encoded string.
25
+ */
26
+ urlEncode(input: string): string
27
+ /**
28
+ * Percent-decode a string. Throws on malformed `%XX` or invalid UTF-8
29
+ * output (parity with `decodeURIComponent`, which is used under Bun).
30
+ * @param input the percent-encoded string to decode.
31
+ * @returns the decoded string.
32
+ */
33
+ urlDecode(input: string): string
34
+ /**
35
+ * Compute the RFC 6455 `Sec-WebSocket-Accept` value for a client key.
36
+ * @param key the base64 `Sec-WebSocket-Key` header value.
37
+ * @returns the 28-byte base64 accept value.
38
+ */
39
+ wsAcceptKey(key: string): string
40
+ /** @param input an email address. @returns whether it is RFC-valid. */
41
+ validateEmail(input: string): boolean
42
+ /** @param input a UUID string. @returns whether it is a valid (v1-v8) UUID. */
43
+ validateUuid(input: string): boolean
44
+ /** @param input an IPv4 address. @returns whether it is valid. */
45
+ validateIpv4(input: string): boolean
46
+ /** @param input an IPv6 address. @returns whether it is valid. */
47
+ validateIpv6(input: string): boolean
48
+ }
49
+
50
+ /** Build the `text` namespace for a client context. */
51
+ export function buildText(ctx: RustClientContext): RustText {
52
+ // String memo: the byte path already caches the native bytes; this avoids the
53
+ // encode → native lookup → slice → decode round-trip for repeated extension
54
+ // strings (the common case) — 0 allocations on a cache hit.
55
+ const mimeStrCache = new Map<string, string>()
56
+
57
+ return {
58
+ mimeFromExtension(ext) {
59
+ let mime = mimeStrCache.get(ext)
60
+ if (mime === undefined) {
61
+ mime = decoder.decode(ctx.cachedMime(encoder.encode(ext)))
62
+ mimeStrCache.set(ext, mime)
63
+ }
64
+ return mime
65
+ },
66
+ urlEncode(input) {
67
+ // Bun's native encodeURIComponent beats the rust+FFI/NAPI string crossing
68
+ // (~4x, measured) with byte-for-byte parity (RFC 3986 unreserved set) —
69
+ // see docs/bun-builtins-decision-matrix.md.
70
+ if (isBun()) return encodeURIComponent(input)
71
+ return resolveNative(ctx, 'urlEncodeStr')(input) as string
72
+ },
73
+ urlDecode(input) {
74
+ // decodeURIComponent matches the rust decoder on valid input and also
75
+ // throws on malformed/invalid UTF-8 (URIError, an Error subclass).
76
+ if (isBun()) return decodeURIComponent(input)
77
+ return resolveNative(ctx, 'urlDecodeStr')(input) as string
78
+ },
79
+ wsAcceptKey(key) {
80
+ return decoder.decode(resolveNative(ctx, 'wsAcceptKey')(encoder.encode(key)) as Uint8Array)
81
+ },
82
+ validateEmail(input) {
83
+ return resolveNative(ctx, 'validateEmail')(encoder.encode(input)) as boolean
84
+ },
85
+ validateUuid(input) {
86
+ return resolveNative(ctx, 'validateUuid')(encoder.encode(input)) as boolean
87
+ },
88
+ validateIpv4(input) {
89
+ return resolveNative(ctx, 'validateIpv4')(encoder.encode(input)) as boolean
90
+ },
91
+ validateIpv6(input) {
92
+ return resolveNative(ctx, 'validateIpv6')(encoder.encode(input)) as boolean
93
+ },
94
+ }
95
+ }
@@ -0,0 +1,194 @@
1
+ {
2
+ "generatedAt": "2026-08-13T09:13:26.107Z",
3
+ "note": "Auto-selected by `bun scripts/select-native.ts --write` from measurements of the castrum addon vs representative pure-TS implementations. Do NOT hand-edit — regenerate with the benchmark script. The public API reads this via `opImpl(op)` and binds each op to a fixed impl (native or js) at load time.",
4
+ "ops": {
5
+ "fnv1a64": {
6
+ "impl": "native",
7
+ "nativeRatio": 12.603
8
+ },
9
+ "crc32": {
10
+ "impl": "js",
11
+ "nativeRatio": 0.659
12
+ },
13
+ "hmacSha256": {
14
+ "impl": "native",
15
+ "nativeRatio": 1.212
16
+ },
17
+ "hmacSha256Verify": {
18
+ "impl": "native",
19
+ "nativeRatio": 9.028
20
+ },
21
+ "signCookie": {
22
+ "impl": "native",
23
+ "nativeRatio": 3.64
24
+ },
25
+ "verifyCookie": {
26
+ "impl": "native",
27
+ "nativeRatio": 1.772
28
+ },
29
+ "csrfToken": {
30
+ "impl": "native",
31
+ "nativeRatio": 12.671
32
+ },
33
+ "csrfVerify": {
34
+ "impl": "native",
35
+ "nativeRatio": 12.458
36
+ },
37
+ "randomToken": {
38
+ "impl": "native",
39
+ "nativeRatio": 9.546
40
+ },
41
+ "passwordHash": {
42
+ "impl": "native",
43
+ "nativeRatio": 1.657
44
+ },
45
+ "aeadEncrypt": {
46
+ "impl": "native",
47
+ "nativeRatio": 1.288
48
+ },
49
+ "aeadDecrypt": {
50
+ "impl": "native",
51
+ "nativeRatio": 1.108
52
+ },
53
+ "queryPairs": {
54
+ "impl": "js",
55
+ "nativeRatio": 0.279
56
+ },
57
+ "cookiePairs": {
58
+ "impl": "js",
59
+ "nativeRatio": 0.105
60
+ },
61
+ "formPairs": {
62
+ "impl": "js",
63
+ "nativeRatio": 0.328
64
+ },
65
+ "etag": {
66
+ "impl": "js",
67
+ "nativeRatio": 0.187
68
+ },
69
+ "parseMediaType": {
70
+ "impl": "js",
71
+ "nativeRatio": 0.378
72
+ },
73
+ "parseAcceptEncoding": {
74
+ "impl": "js",
75
+ "nativeRatio": 0.367
76
+ },
77
+ "createConditionalRequest": {
78
+ "impl": "js",
79
+ "nativeRatio": 0.275
80
+ },
81
+ "createAcceptNegotiator": {
82
+ "impl": "native",
83
+ "nativeRatio": 2.01
84
+ },
85
+ "multipartParse": {
86
+ "impl": "js",
87
+ "nativeRatio": null
88
+ },
89
+ "jsonValid": {
90
+ "impl": "js",
91
+ "nativeRatio": 0.749
92
+ },
93
+ "jsonPatch": {
94
+ "impl": "js",
95
+ "nativeRatio": 0.014
96
+ },
97
+ "gzipCompress": {
98
+ "impl": "native",
99
+ "nativeRatio": 22.132
100
+ },
101
+ "gzipDecompress": {
102
+ "impl": "native",
103
+ "nativeRatio": 61.039
104
+ },
105
+ "brotliCompress": {
106
+ "impl": "native",
107
+ "nativeRatio": 9.983
108
+ },
109
+ "brotliDecompress": {
110
+ "impl": "native",
111
+ "nativeRatio": 33.327
112
+ },
113
+ "sseEncode": {
114
+ "impl": "js",
115
+ "nativeRatio": 0.586
116
+ },
117
+ "wsFrameEncode": {
118
+ "impl": "js",
119
+ "nativeRatio": 0.152
120
+ },
121
+ "wsFrameDecode": {
122
+ "impl": "js",
123
+ "nativeRatio": 0.988
124
+ },
125
+ "wsAcceptKey": {
126
+ "impl": "js",
127
+ "nativeRatio": 0.939
128
+ },
129
+ "validateEmail": {
130
+ "impl": "js",
131
+ "nativeRatio": 0.007
132
+ },
133
+ "validateUuid": {
134
+ "impl": "js",
135
+ "nativeRatio": 0.008
136
+ },
137
+ "validateIpv4": {
138
+ "impl": "js",
139
+ "nativeRatio": 0.012
140
+ },
141
+ "validateIpv6": {
142
+ "impl": "js",
143
+ "nativeRatio": 0.078
144
+ },
145
+ "createRateLimiter": {
146
+ "impl": "js",
147
+ "nativeRatio": 0.061
148
+ },
149
+ "renderTemplate": {
150
+ "impl": "js",
151
+ "nativeRatio": 0.141
152
+ },
153
+ "xxh3": {
154
+ "impl": "native",
155
+ "nativeRatio": null
156
+ },
157
+ "urlEncode": {
158
+ "impl": "js",
159
+ "nativeRatio": 0.214
160
+ },
161
+ "urlDecode": {
162
+ "impl": "js",
163
+ "nativeRatio": 0.092
164
+ },
165
+ "base64Encode": {
166
+ "impl": "js",
167
+ "nativeRatio": 0.376
168
+ },
169
+ "httpDate": {
170
+ "impl": "js",
171
+ "nativeRatio": 0.401
172
+ },
173
+ "jwtSign": {
174
+ "impl": "native",
175
+ "nativeRatio": null
176
+ },
177
+ "jwtVerify": {
178
+ "impl": "native",
179
+ "nativeRatio": null
180
+ },
181
+ "passwordVerify": {
182
+ "impl": "native",
183
+ "nativeRatio": null
184
+ },
185
+ "createTemplate": {
186
+ "impl": "native",
187
+ "nativeRatio": null
188
+ },
189
+ "createSchemaValidator": {
190
+ "impl": "native",
191
+ "nativeRatio": null
192
+ }
193
+ }
194
+ }
@@ -0,0 +1,77 @@
1
+ // src/selection.ts — native-vs-JS selection surface (OWNED BY CASTRUM).
2
+ //
3
+ // The decision of whether an operation should use the Rust addon ("native")
4
+ // or a pure-JS implementation ("js") is a property of this library, not of
5
+ // any consumer. The single source of truth is `rust/selection.rs`
6
+ // (benchmark-driven, audited by `scripts/select-native.ts --check`); this
7
+ // module is its TS projection and the API consumers call ONCE at load time.
8
+ //
9
+ // Consumers bind each operation to a fixed implementation at startup by
10
+ // reading `opImpl(op)` here — they do NOT swap native↔js per call.
11
+
12
+ import { lazyAddon, getAddon } from './native'
13
+ import { isBun } from './shared/runtime'
14
+
15
+ /** Recommended implementation for an operation. */
16
+ export type OpImpl = 'native' | 'js'
17
+
18
+ export interface OpDecision {
19
+ /** The recommended implementation when the addon is available. */
20
+ readonly impl: OpImpl
21
+ /** Why this decision (measured ratio / rationale). */
22
+ readonly note?: string
23
+ }
24
+
25
+ /**
26
+ * Ops where Bun's NATIVE built-in beats the Rust addon (measured in
27
+ * `docs/bun-builtins-decision-matrix.md`: `Bun.gzipSync` ~2.0x, `Bun.hash.crc32`
28
+ * 2.8–8.4x, `Bun.CryptoHasher` ~1.2x, `Bun.hash.xxHash3` ~4.2x, and
29
+ * `crypto.getRandomValues` for token-sized random). Under Bun the fastest
30
+ * choice for these is the pure-JS path that DELEGATES to the Bun built-in
31
+ * (`"js"`), not the Rust addon — so we never ship something slower than what
32
+ * Bun natively provides. Under Node the base benchmark decision stands (Rust
33
+ * wins for gzip/random/hmac there).
34
+ *
35
+ * NOTE: `gzipDecompress` is deliberately NOT in this set — the rust surface
36
+ * keeps the native path (with its 64 MiB decompression-bomb cap) even under
37
+ * Bun, because `Bun.gunzipSync` has no output-size bound.
38
+ */
39
+ const BUN_WINS = new Set([
40
+ 'gzipCompress',
41
+ 'crc32',
42
+ 'randomToken',
43
+ 'hmacSha256',
44
+ 'xxh3',
45
+ 'urlEncode',
46
+ 'urlDecode',
47
+ 'base64Encode',
48
+ 'httpDate',
49
+ ])
50
+
51
+ // Lazy: importing this module does NOT dlopen the addon; `opImpl` reads the
52
+ // baked decision from Rust on first call.
53
+ const addon = lazyAddon(getAddon)
54
+
55
+ /**
56
+ * The benchmark-driven native-vs-JS decision for `op` — runtime-aware.
57
+ *
58
+ * Returns `"native"` when the Rust addon is the recommended implementation,
59
+ * `"js"` when a pure-JS (or Bun built-in) implementation wins, or `null` for
60
+ * unknown ops (or when the addon is unavailable — with no addon there is no
61
+ * "native" choice). Under Bun, ops in {@link BUN_WINS} resolve to `"js"` so the
62
+ * consumer's JS path delegates to the faster Bun built-in.
63
+ */
64
+ export const opImpl = (op: string): OpImpl | null => {
65
+ if (isBun() && BUN_WINS.has(op)) return 'js'
66
+ return addon.opImpl(op)
67
+ }
68
+
69
+ /** `true` when the Rust addon is the recommended impl for `op`. */
70
+ export const isNativeOp = (op: string): boolean => opImpl(op) === 'native'
71
+
72
+ /** The decision record for `op`, or `null` when unknown. */
73
+ export const opDecision = (op: string): OpDecision | null => {
74
+ const impl = opImpl(op)
75
+ if (impl === null) return null
76
+ return { impl }
77
+ }
@@ -0,0 +1,68 @@
1
+ // src/shared/adaptive.ts — Reusable EWMA adaptive-estimate utility.
2
+ //
3
+ // The loader's cost model (src/loader/cost.ts) learns per-op dispatch costs at
4
+ // runtime with an exponential moving average (EWMA). This is that same
5
+ // machinery, extracted as a tiny reusable class so OTHER subsystems can
6
+ // self-optimize without re-implementing the math — e.g. BufferPool learns the
7
+ // observed request size to pre-size future buffers (`adaptive` option), and
8
+ // consumers can drive their own runtime decisions (batch-vs-loop, delegate
9
+ // vs-native, cache-size) with a bounded, noise-smoothed estimate.
10
+
11
+ export interface AdaptiveEstimateOptions {
12
+ /**
13
+ * EWMA smoothing factor in (0, 1]. Higher reacts faster to new samples;
14
+ * lower smooths more. Default: 0.25.
15
+ */
16
+ alpha?: number
17
+ /** Starting estimate. Default 0 (the first sample wins). */
18
+ initial?: number
19
+ /** Clamp the estimate to at least this. Default 0. */
20
+ min?: number
21
+ /** Clamp the estimate to at most this (bounds memory / resources). Default Infinity. */
22
+ max?: number
23
+ }
24
+
25
+ /**
26
+ * A bounded exponential-moving-average estimate of an observed quantity.
27
+ *
28
+ * `sample(x)` folds a new observation into the running estimate; `estimate`
29
+ * reads the current value. The estimate is clamped to `[min, max]`, so it is
30
+ * safe to drive resource sizing (buffer pools, batch thresholds) with it —
31
+ * a single outlier cannot blow up the estimate, and it decays toward recent
32
+ * observations.
33
+ */
34
+ export class AdaptiveEstimate {
35
+ private readonly alpha: number
36
+ private readonly min: number
37
+ private readonly max: number
38
+ private readonly initial: number
39
+ private value: number
40
+
41
+ constructor(options: AdaptiveEstimateOptions = {}) {
42
+ const alpha = options.alpha ?? 0.25
43
+ if (!Number.isFinite(alpha) || alpha <= 0 || alpha > 1) {
44
+ throw new RangeError(`AdaptiveEstimate: alpha must be in (0,1], got ${alpha}`)
45
+ }
46
+ this.alpha = alpha
47
+ this.min = Math.max(0, options.min ?? 0)
48
+ this.max = options.max ?? Number.POSITIVE_INFINITY
49
+ this.initial = options.initial ?? 0
50
+ this.value = this.initial
51
+ }
52
+
53
+ /** Fold a new observation into the running estimate. */
54
+ sample(x: number): void {
55
+ const v = this.value === 0 ? x : this.value * (1 - this.alpha) + x * this.alpha
56
+ this.value = v < this.min ? this.min : v > this.max ? this.max : v
57
+ }
58
+
59
+ /** Current estimate (clamped to `[min, max]`). */
60
+ get estimate(): number {
61
+ return this.value
62
+ }
63
+
64
+ /** Reset the estimate (back to `initial`). */
65
+ reset(): void {
66
+ this.value = this.initial
67
+ }
68
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * src/shared/bench-classify.ts — Pure benchmark classification for PROVEN_SURFACE.
3
+ *
4
+ * Given the CPU report's `comparisons` array and the static PROVEN_SURFACE
5
+ * registry, compute per-function live classification: aggregate all of a
6
+ * function's benchmark tasks (the canonical comparison plus its `_`-suffixed
7
+ * variants that no other registry entry claims), score each task pass/fail,
8
+ * and derive:
9
+ *
10
+ * - `live` status purely from the latest report (majority-fail rule)
11
+ * - `effective` hybrid status the JSDoc annotator and the audit use:
12
+ * a clear MAJORITY of task losses overrides the static
13
+ * status and auto-deprecates the function, while a single
14
+ * good run never auto-promotes a "not-competitive" function
15
+ * (static classifications reflect the shipped release build).
16
+ * - `promotable` static "not-competitive" but live majority WIN (ratio >= 1)
17
+ * - `won`/`failed`/`total` — the score embedded in the generated JSDoc.
18
+ *
19
+ * Task bands: won = ratio >= 1 (rust at least as fast), failed = ratio <
20
+ * 1/LOSS_TOLERANCE (clear loss), middle = tie (counts toward neither).
21
+ *
22
+ * PURE DATA — imports only the registry (no addon, no `rust` client), so it is
23
+ * unit-testable and shared by scripts/annotate-performance.ts and
24
+ * scripts/check-proven.ts (one source of truth for classification).
25
+ */
26
+
27
+ import { PROVEN_SURFACE, type PerformanceStatus, type ProvenEntry } from './proven'
28
+
29
+ /** A "proven"/"parity" function whose baseline is more than this much faster is a clear loss. */
30
+ export const LOSS_TOLERANCE = 1.35
31
+ /** A "not-competitive" function that wins by more than this is a promotion candidate. */
32
+ export const PROMOTE_TOLERANCE = 1.5
33
+
34
+ /** One measured rust-vs-baseline comparison from the CPU report. */
35
+ export interface Comparison {
36
+ label: string
37
+ nativeName: string
38
+ rustName: string
39
+ nativeAvgMs: number
40
+ rustAvgMs: number
41
+ ratio: number
42
+ faster: 'rust' | 'native'
43
+ }
44
+
45
+ /** Pass/fail outcome for one of a function's benchmark tasks. */
46
+ export interface TaskOutcome {
47
+ rustName: string
48
+ label: string
49
+ ratio: number
50
+ /** True when rust is at least as fast (ratio >= 1) — a genuine win. */
51
+ won: boolean
52
+ /** True when rust is a CLEAR loss (ratio < 1 / LOSS_TOLERANCE) — the deprecation signal. */
53
+ failed: boolean
54
+ /** False for the registry's canonical rustTask; true for aggregated variants. */
55
+ isVariant: boolean
56
+ }
57
+
58
+ export interface ClassifiedEntry {
59
+ entry: ProvenEntry
60
+ tasks: TaskOutcome[]
61
+ total: number
62
+ /** Tasks where rust is at least as fast (ratio >= 1). */
63
+ won: number
64
+ /** Tasks where rust is a clear loss (ratio < 1 / LOSS_TOLERANCE). */
65
+ failed: number
66
+ /** True when a clear majority of the function's benchmarks lose (failed > total/2). */
67
+ majorityFail: boolean
68
+ /** True when a clear majority of the function's benchmarks win (won > total/2). */
69
+ majorityWin: boolean
70
+ /**
71
+ * Ratio to display in JSDoc: the worst task when majority-failing (honest
72
+ * "this loses" number), otherwise the canonical comparison, else the median.
73
+ */
74
+ ratio: number | undefined
75
+ /** Status derived purely from the latest report. */
76
+ live: PerformanceStatus
77
+ /** Hybrid status the annotator/audit should act on (majority-fail overrides static). */
78
+ effective: PerformanceStatus
79
+ /** Live results contradict the static classification (majority fail on a proven/parity fn). */
80
+ drifted: boolean
81
+ /** Static "not-competitive" that now majority-wins (keep deprecated, add a note). */
82
+ promotable: boolean
83
+ }
84
+
85
+ function median(values: number[]): number | undefined {
86
+ if (values.length === 0) return undefined
87
+ const sorted = [...values].sort((a, b) => a - b)
88
+ const mid = Math.floor(sorted.length / 2)
89
+ if (sorted.length % 2 === 1) return sorted[mid] ?? 0
90
+ return ((sorted[mid - 1] ?? 0) + (sorted[mid] ?? 0)) / 2
91
+ }
92
+
93
+ /** Every rustTask claimed by any registry entry (prevents cross-entry variant theft). */
94
+ function defaultClaimed(): Set<string> {
95
+ return new Set(PROVEN_SURFACE.map((e) => e.rustTask).filter(Boolean))
96
+ }
97
+
98
+ /**
99
+ * Classify a single registry entry against the report's comparisons.
100
+ *
101
+ * `claimedRustTasks` is the set of rustTasks owned by OTHER registry entries;
102
+ * a variant comparison is aggregated only when no other entry claims it (e.g.
103
+ * `rust:jwt_sign_bytes` belongs to `jwtSignBytes`, not `jwtSign`).
104
+ */
105
+ export function classifyEntry(
106
+ entry: ProvenEntry,
107
+ comparisons: Comparison[],
108
+ claimedRustTasks: ReadonlySet<string> = defaultClaimed(),
109
+ ): ClassifiedEntry {
110
+ const unmeasured: ClassifiedEntry = {
111
+ entry,
112
+ tasks: [],
113
+ total: 0,
114
+ won: 0,
115
+ failed: 0,
116
+ majorityFail: false,
117
+ majorityWin: false,
118
+ ratio: undefined,
119
+ live: 'unmeasured',
120
+ effective: 'unmeasured',
121
+ drifted: false,
122
+ promotable: false,
123
+ }
124
+ if (!entry.rustTask) return unmeasured
125
+
126
+ const tasks: TaskOutcome[] = []
127
+ for (const c of comparisons) {
128
+ const isCanonical = c.rustName === entry.rustTask
129
+ const isVariant =
130
+ !isCanonical &&
131
+ !claimedRustTasks.has(c.rustName) &&
132
+ c.rustName.startsWith(`${entry.rustTask}_`)
133
+ if (isCanonical || isVariant) {
134
+ // Three-band classification (honest, noise-resistant): a task is WON when
135
+ // rust is at least as fast (ratio >= 1), FAILED when rust clearly loses
136
+ // (ratio < 1 / LOSS_TOLERANCE), and a TIE in between. The middle band
137
+ // counts toward neither majority, so marginal losses can never produce a
138
+ // false win (e.g. a stale PROMOTABLE flag for a function that never
139
+ // actually beat its baseline).
140
+ tasks.push({
141
+ rustName: c.rustName,
142
+ label: c.label,
143
+ ratio: c.ratio,
144
+ won: c.ratio >= 1,
145
+ failed: c.ratio < 1 / LOSS_TOLERANCE,
146
+ isVariant: !isCanonical,
147
+ })
148
+ }
149
+ }
150
+
151
+ const total = tasks.length
152
+ if (total === 0) return unmeasured
153
+
154
+ const won = tasks.filter((t) => t.won).length
155
+ const failed = tasks.filter((t) => t.failed).length
156
+ const majorityFail = failed * 2 > total
157
+ const majorityWin = won * 2 > total
158
+
159
+ const canonical = tasks.find((t) => !t.isVariant)
160
+ const ratio = majorityFail
161
+ ? Math.min(...tasks.map((t) => t.ratio))
162
+ : (canonical?.ratio ?? median(tasks.map((t) => t.ratio)))
163
+
164
+ const live: PerformanceStatus = majorityFail
165
+ ? 'not-competitive'
166
+ : majorityWin
167
+ ? 'proven'
168
+ : 'parity'
169
+
170
+ // Hybrid effective status: a clear majority of losses overrides the static
171
+ // registry (auto-deprecate); otherwise the static classification stands —
172
+ // classifications reflect the shipped release build, not one local run.
173
+ const effective: PerformanceStatus =
174
+ entry.status !== 'unmeasured' && majorityFail ? 'not-competitive' : entry.status
175
+
176
+ return {
177
+ entry,
178
+ tasks,
179
+ total,
180
+ won,
181
+ failed,
182
+ majorityFail,
183
+ majorityWin,
184
+ ratio,
185
+ live,
186
+ effective,
187
+ drifted: entry.status !== 'unmeasured' && effective !== entry.status,
188
+ promotable: entry.status === 'not-competitive' && majorityWin,
189
+ }
190
+ }
191
+
192
+ /** Classify every registry entry (single source of truth for the audit + annotator). */
193
+ export function classifySurface(
194
+ entries: readonly ProvenEntry[],
195
+ comparisons: Comparison[],
196
+ ): Map<string, ClassifiedEntry> {
197
+ const claimed = new Set(entries.map((e) => e.rustTask).filter(Boolean))
198
+ const out = new Map<string, ClassifiedEntry>()
199
+ for (const entry of entries) {
200
+ out.set(entry.name, classifyEntry(entry, comparisons, claimed))
201
+ }
202
+ return out
203
+ }
204
+
205
+ /** Human ratio summary: "3.18x rust" / "2.34x baseline". */
206
+ export function formatRatio(ratio: number): string {
207
+ if (ratio >= 1) return `${ratio.toFixed(2)}x rust`
208
+ return `${(1 / ratio).toFixed(2)}x baseline`
209
+ }