castrum 0.9.0 → 0.9.2

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 (120) hide show
  1. package/README.md +153 -630
  2. package/castrum.linux-x64-gnu.node +0 -0
  3. package/dist/index.d.ts +1653 -1817
  4. package/dist/index.js +6172 -4294
  5. package/docs/API.md +337 -0
  6. package/docs/ARCHITECTURE.md +111 -17
  7. package/docs/BENCHMARKS.md +117 -49
  8. package/docs/CASE_STUDY.md +243 -0
  9. package/docs/ENVIRONMENT.md +24 -7
  10. package/docs/FFI_BUN_GUIDE.md +335 -0
  11. package/docs/GETTING_STARTED.md +18 -34
  12. package/docs/HOW_TO_ADD_AN_OP.md +180 -0
  13. package/docs/INGRESS-ROUTER.md +82 -0
  14. package/docs/INGRESS.md +11 -7
  15. package/docs/NATIVE-ROUTE.md +77 -0
  16. package/docs/REPO_MAP.md +103 -53
  17. package/docs/adr/0001-two-wire-formats.md +1 -1
  18. package/docs/adr/0003-bun-builtins-delegation.md +6 -5
  19. package/docs/ai/LOCAL_DEV.md +81 -0
  20. package/docs/ai/TREE.md +1070 -0
  21. package/docs/bun-builtins-decision-matrix.md +25 -18
  22. package/index.ts +9 -16
  23. package/package.json +41 -28
  24. package/src/ingress/body.ts +167 -27
  25. package/src/ingress/decode/result-base.ts +10 -2
  26. package/src/ingress/handlers.ts +116 -285
  27. package/src/ingress/headers/security.ts +41 -0
  28. package/src/ingress/health.ts +1 -0
  29. package/src/ingress/index.ts +45 -169
  30. package/src/ingress/native-route.ts +182 -0
  31. package/src/ingress/packing/gather-raw-headers.ts +110 -22
  32. package/src/ingress/packing/header-packing.ts +5 -5
  33. package/src/ingress/packing/input-packer.ts +17 -3
  34. package/src/ingress/packing/route-wire.ts +197 -0
  35. package/src/ingress/packing/select-headers.ts +20 -7
  36. package/src/ingress/path-matcher.ts +115 -0
  37. package/src/ingress/response/baked-response.ts +302 -0
  38. package/src/ingress/router.ts +277 -0
  39. package/src/ingress/routes/common.ts +50 -1
  40. package/src/ingress/routes/fallback.ts +2 -2
  41. package/src/ingress/routes/head.ts +3 -13
  42. package/src/ingress/routes/index.ts +5 -4
  43. package/src/ingress/routes/json-write.ts +26 -47
  44. package/src/ingress/routes/native.ts +130 -0
  45. package/src/ingress/routes/options.ts +2 -12
  46. package/src/ingress/routes/read.ts +3 -13
  47. package/src/ingress/routes/responder.ts +159 -0
  48. package/src/ingress/server-node.ts +7 -7
  49. package/src/ingress/server.ts +83 -112
  50. package/src/ingress/sync.ts +151 -0
  51. package/src/ingress/types.ts +44 -1
  52. package/src/integration/pipeline.ts +3 -0
  53. package/src/integration/streaming.ts +2 -3
  54. package/src/integration/websocket.ts +4 -3
  55. package/src/loader/batch.ts +4 -0
  56. package/src/loader/cost.ts +1 -0
  57. package/src/loader/index.ts +43 -228
  58. package/src/loader/state.ts +84 -0
  59. package/src/loader/types.ts +145 -0
  60. package/src/native/ffi/build/codecs.ts +736 -0
  61. package/src/native/ffi/build/compress.ts +248 -0
  62. package/src/native/ffi/build/instances.ts +598 -0
  63. package/src/native/ffi/build/parse.ts +539 -0
  64. package/src/native/ffi/build/util.ts +140 -0
  65. package/src/native/ffi/build.ts +94 -0
  66. package/src/native/ffi/constants.ts +58 -0
  67. package/src/native/ffi/selftest.ts +22 -0
  68. package/src/native/ffi/types.ts +499 -0
  69. package/src/native/ffi.ts +307 -1357
  70. package/src/native/loader.ts +52 -6
  71. package/src/native/types/instances.ts +279 -0
  72. package/src/native/types.ts +41 -238
  73. package/src/runtime/builtins.ts +131 -0
  74. package/src/runtime/codec.ts +147 -0
  75. package/src/runtime/detect.ts +36 -0
  76. package/src/runtime/index.ts +28 -0
  77. package/src/runtime/native.ts +35 -0
  78. package/src/runtime/server.ts +31 -0
  79. package/src/runtime/transport.ts +64 -0
  80. package/src/runtime/types.ts +173 -0
  81. package/src/runtime/uuid.ts +45 -0
  82. package/src/runtime/websocket.ts +30 -0
  83. package/src/rust-ffi/addon.ts +1 -1
  84. package/src/rust-ffi/batch/build.ts +155 -0
  85. package/src/rust-ffi/batch/index.ts +8 -0
  86. package/src/rust-ffi/batch/types.ts +92 -0
  87. package/src/rust-ffi/client.ts +5 -6
  88. package/src/rust-ffi/context.ts +39 -10
  89. package/src/rust-ffi/index.ts +28 -17
  90. package/src/rust-ffi/into.ts +21 -0
  91. package/src/rust-ffi/options.ts +1 -1
  92. package/src/rust-ffi/packed.ts +40 -2
  93. package/src/rust-ffi/proven.ts +16 -30
  94. package/src/rust-ffi/scalar/crypto.ts +188 -107
  95. package/src/rust-ffi/scalar/factories.ts +257 -27
  96. package/src/rust-ffi/scalar/hashing.ts +43 -41
  97. package/src/rust-ffi/scalar/http.ts +208 -77
  98. package/src/rust-ffi/scalar/index.ts +3 -3
  99. package/src/rust-ffi/scalar/interface.ts +88 -213
  100. package/src/rust-ffi/scalar/json-packed.ts +175 -0
  101. package/src/rust-ffi/scalar/json.ts +28 -12
  102. package/src/rust-ffi/scalar/payload.ts +159 -42
  103. package/src/rust-ffi/text.ts +31 -5
  104. package/src/selection.ts +27 -31
  105. package/src/shared/adaptive.ts +1 -2
  106. package/src/shared/buffer-pool.ts +72 -17
  107. package/src/shared/bytes.ts +75 -6
  108. package/src/shared/codec.ts +78 -0
  109. package/src/shared/env.ts +4 -4
  110. package/src/shared/log.ts +1 -0
  111. package/src/shared/packed/index.ts +10 -0
  112. package/src/shared/packed/parsers.ts +31 -0
  113. package/src/shared/packed/schema.ts +23 -0
  114. package/src/shared/{packed.ts → packed/wire.ts} +31 -39
  115. package/src/shared/proven.ts +189 -496
  116. package/src/shared/runtime.ts +20 -25
  117. package/src/shared/uuid.ts +12 -28
  118. package/src/data/json-rows.ts +0 -33
  119. package/src/rust-ffi/batch.ts +0 -431
  120. package/src/shared/bench-classify.ts +0 -209
package/docs/API.md ADDED
@@ -0,0 +1,337 @@
1
+ # castrum — API Reference
2
+
3
+ The complete public API, verified against the current source. For the friendly
4
+ overview and quick start, see the [README](../README.md); for the HTTP pipeline
5
+ deep-dive, see [`INGRESS.md`](./INGRESS.md).
6
+
7
+ Everything is **synchronous**. All byte-level functions accept `Uint8Array`;
8
+ `rust.text.*` provides string-in/string-out ergonomics.
9
+
10
+ ---
11
+
12
+ ## Runtime return-type divergence (Bun vs Node)
13
+
14
+ On **Bun** the `bun:ffi` C-ABI transport returns text results as **JS strings**
15
+ via JSC's native transfer (cstring → `new CString(ptr, 0, len)`), with **zero
16
+ `TextEncoder`/`TextDecoder`** on the hot path. So `rust.urlEncode`,
17
+ `rust.base64Encode`, `rust.hexEncode`, `rust.hmacSha256`, `rust.etag`,
18
+ `rust.wsAcceptKey`, `rust.randomToken`, `rust.signCookie`, `rust.verifyCookie`
19
+ (`string | null`), `rust.csrfToken`, `rust.passwordHash`,
20
+ `rust.passwordHashBcrypt`, `rust.jwtSignBytes`, `rust.jwtVerify`
21
+ (`string | null`), `rust.jsonPatch`, `rust.httpDate`, `rust.mimeFromExtension`,
22
+ `rust.urlResolve`, `rust.urlEncodeQuery`, and `rust.acceptNegotiatorNegotiate`
23
+ (`string | null` — the `null` is the C `NULL` sentinel) return **`string`** on Bun. On **Node** (napi fallback) the same
24
+ functions return **`Uint8Array`**. The TypeScript surface types these as
25
+ `Uint8Array | string`; normalize with `toBytes(value)` / `toText(value)` (from
26
+ `src/shared/bytes`). The `*Into`/pooled variants always return bytes on both
27
+ runtimes, and binary ops (`gzip*`, `brotli*`, `aead*`, `pbkdf2Sha256`,
28
+ `wsFrame*`, packed parsers, `batch.*`/`packed.*`) return `Uint8Array` on both.
29
+
30
+ ---
31
+
32
+ ## Core exports
33
+
34
+ | Export | Description |
35
+ |--------|-------------|
36
+ | `rust` | The default, shared native client — every Rust utility, flat and complete. |
37
+ | `createRust(options?)` | Build an isolated client (own caches / rayon settings). |
38
+ | `proven`, `PROVEN_SELECTION`, `provenImpl`, `provenStatus`, `isProven`, `provenSurface`, `provenSummary` | The full `rust.*` surface + the baked (pure-data) proven-selection registry of benchmark winners (`native` / `js` / `bun`). |
39
+ | `opImpl`, `isNativeOp`, `opDecision` | Native-vs-JS selection hints (benchmark-driven). |
40
+ | `loader`, `createLoader` | Higher-order loader over the batch surface. |
41
+ | `encoder`, `decoder` | Codec-backed UTF-8 helpers (Bun: `Bun.ArrayBufferSink` / `CString`; Node: `TextEncoder`/`TextDecoder`). `encode`/`decode` accept the `Uint8Array | string` union and normalize. |
42
+ | `uuidv7()` | UUIDv7 — `Bun.randomUUIDv7` on Bun, `crypto.randomUUID` on Node. |
43
+ | `AdaptiveEstimate`, `AdaptiveEstimateOptions` | Bounded EWMA adaptive-estimate utility. |
44
+ | `createMetrics`, `DEFAULT_BUCKETS` | Zero-dependency metrics registry (counters/gauges/histograms + Prometheus text). |
45
+ | `packBatch`, `packPairs`, `readPairsPacked`, `readHttpPacked`, `pairsToObject`, `parseQueryString`, `parseCookieHeader`, `parseFormBody`, `unpackBitset`, `unpackByteResults`, `unpackI64ArrayAsBigInt`, `unpackU32Array` | Packed-wire + high-level parsing utilities. |
46
+ | Ingress: `createIngress`, `createIngressSync`, `createIngressFast`, `createIngressHandler`, `createIngressServer`, `createIngressServerNode`, `createIngressRouter`, route factories, metrics/health/trace helpers | See [`INGRESS.md`](./INGRESS.md) and [`INGRESS-ROUTER.md`](./INGRESS-ROUTER.md). |
47
+ | Integration: `createPipeline`, `createWebSocketUpgrade`, `sseResponse` | Framework-agnostic helpers — see [`INGRESS.md`](./INGRESS.md) §Framework Integration. |
48
+
49
+ ---
50
+
51
+ ## The `rust` client
52
+
53
+ `rust` is a single flat object exposing **every** native utility. Text-oriented
54
+ operations also have ergonomic string variants under `rust.text`.
55
+
56
+ | Namespace | Description |
57
+ |-----------|-------------|
58
+ | `rust.crc32(...)`, `rust.jsonValid(...)`, … | Scalar utilities (bytes in → typed out) |
59
+ | `rust.text.mimeFromExtension(".js")`, … | String ergonomics (string in → string/bool out) |
60
+ | `rust.batch.jsonValid(docs)`, … | High-throughput batch (arrays → unpacked results) |
61
+ | `rust.packed.jsonValidBatchPacked(p)`, … | Raw packed low-level variants + metadata |
62
+ | `rust.configure({ ... })` | Override defaults on the shared instance |
63
+
64
+ `rust.transport()` returns `"ffi"` / `"napi"` (the resolved transport);
65
+ `rust.ffiActive()` reports whether the bun:ffi transport is live.
66
+
67
+ ### Scalar utilities
68
+
69
+ | Function | Returns |
70
+ |----------|---------|
71
+ | `crc32(input)` | `number` — CRC32 checksum |
72
+ | `fnv1a64(input)` | `bigint` — FNV-1a 64-bit hash |
73
+ | `xxh3(input)` | `bigint` — XXH3-64 (non-crypto hash) |
74
+ | `hmacSha256(key, data)` | `Uint8Array` — HMAC-SHA256 signature |
75
+ | `hmacSha256Verify(key, data, sig)` | `boolean` |
76
+ | `jsonValid(input)` | `boolean` — JSON validity check (zero-DOM) |
77
+ | `jsonParse(input)` | `unknown` — JSON → JS value (native sonic-rs DOM; throws on invalid) |
78
+ | `jsonSumIds(input)` | `bigint` — sum of numeric `id` fields |
79
+ | `jsonPatch(doc, patch)` | `Uint8Array` — RFC 6902 result |
80
+ | `createSchemaValidator(schema)` | `SchemaValidator` — compile a JSON Schema (draft-07); `.validate(doc): boolean`, `.validateDetailed(doc): SchemaError[]`, `.validateFirstError(doc): SchemaError | null`, `.validateBatchPackedCount(packed): number`, … |
81
+ | `mimeFromExtension(ext)` | `Uint8Array` — MIME type |
82
+ | `randomToken(byteLen)` | `Uint8Array` — CSPRNG token |
83
+ | `urlEncode(input)` / `urlDecode(input)` | `Uint8Array` — percent encode/decode |
84
+ | `urlDecodeBytes(input)` | `Uint8Array` — strict %-decode (no UTF-8 check) |
85
+ | `validateEmail/Uuid/Ipv4/Ipv6(input)` | `boolean` |
86
+ | `wsAcceptKey(key)` | `Uint8Array` — WebSocket accept key |
87
+ | `passwordHashBcrypt(password, salt?, cost?)` | `Uint8Array` — bcrypt `$2b$` PHC string |
88
+ | `passwordVerifyBcrypt(password, phc)` | `boolean` |
89
+ | `pbkdf2Sha256(password, salt, iterations, dkLen?)` | `Uint8Array` — PBKDF2-HMAC-SHA256 |
90
+ | `gzipCompress(data)` / `gzipDecompress(data, maxDecompressed?)` | `Uint8Array` — gzip (decompress capped at 64 MiB by default) |
91
+ | `brotliCompress(data)` / `brotliDecompress(data, maxDecompressed?)` | `Uint8Array` — brotli |
92
+
93
+ ### Framework actions (HTTP semantics + security)
94
+
95
+ | Function | Returns |
96
+ |----------|---------|
97
+ | `formParsePacked(body)` | `Uint8Array` — packed x-www-form-urlencoded pairs |
98
+ | `createFormParser(capacity?)` | `FormParser` — reusable-buffer form parser (`.parse`, `.parseInto`) |
99
+ | `parseMediaType(header)` | `{ mediaType, charset, boundary, params }` — Content-Type parse |
100
+ | `createMediaTypeParser()` | `MediaTypeParser` — `.parse`, `.matches(actual, expected)` (wildcards) |
101
+ | `createMediaTypeMatcher(expected)` | `MediaTypeMatcher` — `.matches(actual)` (expected precompiled once) |
102
+ | `etag(data, weak?)` | `Uint8Array` — strong/weak ETag (crc32-based) |
103
+ | `httpDate(secs?)` / `parseHttpDate(input)` | `Uint8Array` / `bigint | null` — IMF-fixdate format/parse |
104
+ | `createConditionalRequest(etag, lastModifiedSecs?)` | `ConditionalRequest` — `.isNotModified(ifNoneMatch, ifModifiedSince)` → 304 |
105
+ | `parseAcceptEncoding(header)` | `{ encoding, q, order }[]` |
106
+ | `createAcceptNegotiator(supported[])` | `AcceptNegotiator` — `.negotiate(header)` → best encoding |
107
+ | `base64Encode/Decode(input, urlSafe?, padding?)`, `base64UrlEncode/Decode(input)` | `Uint8Array` |
108
+ | `createBase64Codec(urlSafe?, padding?)` | `Base64Codec` — `.encode`, `.decode` |
109
+ | `hexEncode(input)` / `hexDecode(input)` | `Uint8Array` — lowercase hex |
110
+ | `signCookie(value, secret)` / `verifyCookie(signed, secret)` | `Uint8Array` / `Uint8Array | null` — signed cookies |
111
+ | `createCookieSigner(secret)` | `CookieSigner` — `.sign`, `.verify` (HMAC key compiled once) |
112
+ | `csrfToken(secret)` / `csrfVerify(token, secret)` | `Uint8Array` / `boolean` |
113
+ | `createCsrfProtector(secret)` | `CsrfProtector` — `.create`, `.verify` |
114
+ | `urlResolve(base, reference)` | `Uint8Array` — RFC 3986 resolution |
115
+ | `urlEncodeQuery(params)` | `Uint8Array` — percent-encoded query (sorted keys) |
116
+ | `createUrlBuilder(base)` | `UrlBuilder` — `.resolve(reference)` (base parsed once) |
117
+ | `createJwtSigner(secret, ttlSeconds?)` | `JwtSigner` — `.sign(claims, now)`, `.verify(token, now)` (HS256 key + ttl compiled once) |
118
+ | `createAeadCipher(key, algorithm?)` | `AeadCipher` — `.encrypt(nonce, pt)`, `.decrypt(nonce, ct)` (key compiled once) |
119
+ | `createArgon2Hasher(options?)` | `Argon2Hasher` — `.hash(password, salt)`, `.verify(password, phc)` (params compiled once) |
120
+ | `httpParseRequestPacked(bytes)` | packed HTTP request parse (low-level) |
121
+ | `queryParsePacked(query)` / `cookieParsePacked(header)` / `formParsePacked(body)` | packed pair parsing (low-level) |
122
+
123
+ > `passwordHash` / `passwordVerify` (argon2id) and `passwordHashBcrypt` /
124
+ > `passwordVerifyBcrypt` are also exposed as scalar helpers; the compiled-once
125
+ > `createArgon2Hasher` is preferred when hashing repeatedly.
126
+
127
+ ### String ergonomics (`rust.text`)
128
+
129
+ ```ts
130
+ rust.text.mimeFromExtension(".js"); // "text/javascript"
131
+ rust.text.mimeFromExtension("html"); // "text/html"
132
+ rust.text.urlEncode("a b"); // "a%20b"
133
+ rust.text.urlDecode("a%20b"); // "a b"
134
+ rust.text.wsAcceptKey(keyBase64); // base64 accept key
135
+ rust.text.validateEmail("a@b.co"); // true
136
+ ```
137
+
138
+ ### Batch operations (`rust.batch`)
139
+
140
+ All batch helpers accept `Uint8Array[]` and return unpacked, ready-to-use results:
141
+
142
+ ```ts
143
+ const docs = [encoder.encode('{"id":1}'), encoder.encode('{"id":2}')];
144
+
145
+ rust.batch.jsonValid(docs); // Uint8Array bitset (1 per doc)
146
+ rust.batch.crc32(docs); // Uint32Array
147
+ rust.batch.jsonSumIds(docs); // BigInt64Array
148
+ rust.batch.queryParse(docs); // Uint8Array[]
149
+ rust.batch.formParse(docs); // Uint8Array[] (packed pairs per doc)
150
+ rust.batch.signCookie(docs, secret); // Uint8Array[] (signed)
151
+ rust.batch.verifyCookie(docs, secret); // Uint8Array bitset (valid?)
152
+ rust.batch.csrfVerify(tokens, secret); // Uint8Array bitset (valid?)
153
+ rust.batch.schemaValidate(validator, docs); // bitset
154
+
155
+ // Hashing / url / mime / ws / base64url
156
+ rust.batch.fnv1a64(docs); // BigUint64Array (unsigned hashes)
157
+ rust.batch.etag(docs); // Uint8Array[]
158
+ rust.batch.urlEncode(docs); // Uint8Array[]
159
+ rust.batch.urlDecode(docs); // Uint8Array[]
160
+ rust.batch.base64UrlEncode(docs); // Uint8Array[]
161
+ rust.batch.wsAcceptKey(keys); // Uint8Array[]
162
+ rust.batch.mimeFromExtension(exts); // Uint8Array[]
163
+
164
+ // Backend-framework batches (zipped lists / shared args)
165
+ rust.batch.passwordVerify(pwds, phcs); // Uint8Array bitset (zipped)
166
+ rust.batch.urlResolve(bases, refs); // Uint8Array[] (zipped)
167
+ rust.batch.jwtSign(claimDocs, secret); // Uint8Array[] (sign N JSON claim docs)
168
+ rust.batch.jwtVerify(tokens, secret, now); // Uint8Array bitset
169
+ rust.batch.sseEncode(items, "event"); // Uint8Array[]
170
+ rust.batch.wsFrameEncode(items, 1, false, true); // Uint8Array[]
171
+ rust.batch.wsFrameDecode(frames); // Uint8Array[] (payloads)
172
+ rust.batch.multipartParse(bodies, boundary); // MultipartPart[][]
173
+ ```
174
+
175
+ Raw packed + metadata variants (advanced): `rust.packed.*` mirrors the native
176
+ functions 1:1 (`jsonValidBatchPacked`, `jsonValidBatchCountPacked`,
177
+ `queryParseBatchTotalLenPacked`, …).
178
+
179
+ ---
180
+
181
+ ## Higher-order data loader (`loader` / `createLoader`)
182
+
183
+ `loader` is a callable higher-order function over the whole batch surface. It
184
+ pre-binds an op so repeated calls skip registry dispatch, and it routes small
185
+ vs. large workloads automatically: a single item → one scalar native call, a
186
+ bulk → ONE packed batch call, and `load()` coalesces N same-tick calls into one
187
+ packed call (DataLoader-style) with a bounded LRU cache.
188
+
189
+ ```ts
190
+ import { loader } from "castrum";
191
+
192
+ const isEmail = loader("validateEmail");
193
+ isEmail(emailBytes); // scalar → boolean
194
+ isEmail([a, b, c]); // bulk → Uint8Array bitset (one packed call)
195
+ await isEmail.load(a); // coalesced + cached (auto batch/strategy)
196
+
197
+ // Covered op families (44 ops):
198
+ // hash crc32, fnv1a64
199
+ // json jsonValid, jsonSumIds, jsonPatch*, schemaValidate
200
+ // validate email, uuid, ipv4, ipv6
201
+ // parse query, cookie, form, httpParseRequest, urlResolve*
202
+ // encode hex, base64, base64Url, urlEncode, urlDecode, urlDecodeBytes, etag
203
+ // compress gzip, brotli
204
+ // crypto hmacSha256, hmacSha256Verify*, signCookie, verifyCookie, csrfVerify,
205
+ // passwordHash, passwordVerify*, aeadEncrypt, aeadDecrypt, jwtSign, jwtVerify
206
+ // web wsAcceptKey, wsFrameEncode, sseEncode, mimeFromExtension
207
+ // (* = paired: per-item companion array, e.g. loader.run("jsonPatch", docs, patches))
208
+
209
+ // Paired ops take a companion array:
210
+ loader.run("jsonPatch", docs, patches); // bulk
211
+ loader.run("jsonPatch", doc, patch); // single
212
+
213
+ // verifyCookie / jwtVerify are boolean-validity ops (valid/invalid);
214
+ // use rust.verifyCookie / rust.jwtVerify for the decoded value.
215
+
216
+ // Bind a schema validator for repeated single/bulk validation + count:
217
+ const schema = loader.schema(rust.createSchemaValidator(schemaBytes));
218
+ schema(doc); // boolean
219
+ schema(docs); // Uint8Array bitset
220
+ schema.count(docs); // number of valid docs
221
+
222
+ // Fine-grained control: createLoader({ adaptive, batchMin, maxCacheKeys, ... })
223
+ ```
224
+
225
+ ---
226
+
227
+ ## The `proven` selection surface
228
+
229
+ `proven` is literally the same object as `rust` (`export const proven = rust`) —
230
+ nothing is filtered. Alongside it, the pure-data registry `PROVEN_SELECTION`
231
+ (`src/shared/proven.ts`) states which implementation is the benchmark-proven
232
+ winner for every op: `native` (the addon), `js` (pure TS), or `bun` (a Bun
233
+ built-in that is delegated under Bun). The winners are **baked** from
234
+ measurements (`src/selection.json` + `docs/bun-builtins-decision-matrix.md`),
235
+ and `test/unit/shared/proven.test.ts` verifies each winner is actually wired
236
+ (`opImpl` agrees, `builtins.has` matches, native/js entries match the addon's
237
+ embedded `selection.json`) — so the registry cannot silently drift. There is no
238
+ live benchmark gate (the old `check:proven` / `check:annotate` friction is gone).
239
+
240
+ - Statuses: `proven` (decisive win) / `parity` (borderline) / `unmeasured` (no
241
+ direct ratio — pinned by policy). `provenImpl(op)` returns the baked winner.
242
+ - The full surface is unchanged — `rust.jsonParse`, `rust.createSchemaValidator`,
243
+ etc. remain available for completeness; they just aren't advertised as wins.
244
+ - Classifications are based on the **release build on the shipped baseline CPU**
245
+ (published artifacts are baseline — not the local SIMD `build:perf`).
246
+
247
+ ```ts
248
+ import { proven, PROVEN_SELECTION, provenImpl } from "castrum";
249
+
250
+ proven.fnv1a64(bytes); // native — Rust wins ~12x vs the JS baseline
251
+ provenImpl("crc32"); // "bun" — Bun.hash.crc32 wins under Bun
252
+ provenImpl("validateEmail"); // "js" — pure-TS wins
253
+ ```
254
+
255
+ ```bash
256
+ bun test test/unit/shared/proven.test.ts # proves every baked winner is wired
257
+ ```
258
+
259
+ ---
260
+
261
+ ## Native-vs-JS selection surface
262
+
263
+ For framework consumers that want to bind each operation to a fixed
264
+ implementation at load time (instead of calling `rust.*` directly):
265
+
266
+ ```ts
267
+ import { opImpl, isNativeOp, opDecision } from "castrum";
268
+
269
+ opImpl("gzipCompress"); // "native" | "js" | null
270
+ isNativeOp("crc32"); // boolean
271
+ opDecision("fnv1a64"); // { impl, note? } | null
272
+ ```
273
+
274
+ - Source of truth: `rust/selection.rs`, which embeds the benchmark-generated
275
+ `src/selection.json` (produced by `scripts/select-native.ts --write`, audited
276
+ by `--check` in CI).
277
+ - Under Bun, ops where the Bun built-in beats the Rust addon (gzip, crc32, xxh3,
278
+ HMAC, random tokens) are selected as `"js"` (a JS path that delegates to the
279
+ Bun built-in); under Node the base benchmark decision stands.
280
+ - Consumers read `opImpl(op)` **once at startup** and bind each op to a fixed
281
+ implementation — they do not swap native↔js per call.
282
+
283
+ ---
284
+
285
+ ## Configuration
286
+
287
+ Defaults are selected automatically, and can be overridden:
288
+
289
+ ```ts
290
+ // Defaults:
291
+ // rayonThreads = max(1, hardwareConcurrency - 1)
292
+ // (env override: CASTRUM_RAYON_THREADS / RUST_RAYON_THREADS)
293
+ // mimeCache = true
294
+ // hmacCache = true
295
+
296
+ // Override per-instance options on the shared instance:
297
+ rust.configure({ mimeCache: false, hmacCache: false });
298
+
299
+ // Or create an isolated instance with custom defaults:
300
+ import { createRust } from "castrum";
301
+ const myRust = createRust({ mimeCache: false });
302
+ ```
303
+
304
+ > **Thread pool note**: the rayon pool is process-wide and initialized **once**
305
+ > (native `OnceLock`) — the first initialization wins. Because importing the
306
+ > module initializes it with the default, set
307
+ > `CASTRUM_RAYON_THREADS` / `RUST_RAYON_THREADS` (or call
308
+ > `rust.configure({ rayonThreads })` / `createRust({ rayonThreads })` before any
309
+ > other pool use) to tune it. `mimeCache` / `hmacCache` are per-instance and can
310
+ > be toggled any time.
311
+
312
+ ---
313
+
314
+ ## Shared utilities
315
+
316
+ ```ts
317
+ import {
318
+ encoder, decoder, // TextEncoder/TextDecoder singletons
319
+ packBatch, packPairs, // batch/pair packing
320
+ unpackBitset, unpackByteResults, unpackI64ArrayAsBigInt, unpackU32Array,
321
+ readPairsPacked, pairsToObject, readHttpPacked,
322
+ parseQueryString, parseCookieHeader, parseFormBody,
323
+ uuidv7, createMetrics,
324
+ } from "castrum";
325
+
326
+ parseQueryString("a=1&b=2&tag=a&tag=b"); // { a: "1", b: "2", tag: ["a", "b"] }
327
+ parseCookieHeader("session=abc; theme=dark"); // { session: "abc", theme: "dark" }
328
+ ```
329
+
330
+ > **Environment variables**: see [`ENVIRONMENT.md`](./ENVIRONMENT.md) for the
331
+ > full reference of `INGRESS_*`, `CASTRUM_*`, and `RUST_*` runtime variables.
332
+
333
+ ## Ingress & integration
334
+
335
+ The HTTP pipeline (options, route factories, `BakedIngressResult`, server
336
+ builders, observability, and the framework integration helpers) has its own
337
+ dedicated reference: [`INGRESS.md`](./INGRESS.md).
@@ -31,14 +31,22 @@ The bridge between TypeScript and Rust has **two transports**, both loading the
31
31
  cdylib (built with napi-rs):
32
32
 
33
33
  - **`bun:ffi` (PRIMARY on Bun)** — `src/native/ffi.ts` `dlopen`s the addon's
34
- `extern "C"` exports (`rust/ffi.rs`, 45 `castrum_*` symbols) and Bun JIT-calls them
34
+ `extern "C"` exports (`rust/ffi/`, 85 `castrum_*` symbols — 77 direct + 4
35
+ `validator_c_abi!` + 4 `compress_to_out!`, parity guarded by
36
+ `test/unit/native/ffi-symbol-parity.test.ts`) and Bun JIT-calls them
35
37
  directly (~10-20ns crossing vs ~100-350ns N-API). A bind-time self-test guards
36
38
  correctness: on ANY failure the layer is disabled and calls fall back to napi.
37
39
  `CASTRUM_FFI_MODE=auto|ffi|napi` selects the transport (default `auto`).
38
40
  - **NAPI (napi-rs v3) — the fallback** — used under Node.js, when `bun:ffi` is
39
41
  unavailable (self-test failure, `CASTRUM_FFI_MODE=napi`), and for surfaces with no
40
- C-ABI export (stateful instances, `rust.batch.*` / `rust.packed.*`, `text.*`, and
41
- structured-return scalars like `jsonParse` / `jwtVerify`).
42
+ C-ABI export. Since Phase 6 the compiled-once instances are NOT napi-only — their
43
+ hot per-call ops route through the C-ABI via an opaque-`usize` handle
44
+ (`conditional_is_not_modified`, `media_type_matcher_matches`,
45
+ `accept_negotiator_negotiate`, `jwt_signer_sign`/`_verify`, `template_render`,
46
+ `schema_validator_validate`, `rate_limiter_check*`, `url_builder_resolve`), and
47
+ `jwtVerify` has `castrum_jwt_verify`. Still napi-only: `rust.batch.*` /
48
+ `rust.packed.*`, `text.*`, and the DOM `jsonParse` (only `castrum_json_parse_packed`
49
+ exists).
42
50
 
43
51
  Shared characteristics:
44
52
 
@@ -46,10 +54,25 @@ Shared characteristics:
46
54
  - **Synchronous calls**: All functions are synchronous, running on the main JS thread
47
55
  - **Thread pool**: Rayon is available for parallel CPU-bound work
48
56
 
57
+ ### Dual-binary CPU-detect (x86-64-v3 SIMD variant)
58
+
59
+ On `linux-x64` the package ships a second artifact, `castrum.linux-x64-v3-gnu.node`,
60
+ built with `-C target-cpu=x86-64-v3 -C target-feature=+avx2,+bmi2,+fma,+sse4.2`
61
+ (`scripts/build-v3.sh`, package script `build:v3`; `build:all` builds both). The
62
+ loader (`src/native/loader.ts`) picks the v3 variant at runtime when the host
63
+ CPU exposes AVX2+BMI2+FMA+SSE4.2 (read from `/proc/cpuinfo`), falling back to the
64
+ baseline artifact otherwise — a v3 binary dlopened on an unsupported CPU would
65
+ SIGILL (not catchable from JS), so detection is conservative and requires ALL
66
+ four flags. The v3 variant is OPTIONAL (an absent v3 file resolves to baseline),
67
+ so `build:v3` is never a hard publish requirement.
68
+
49
69
  ---
50
70
 
51
71
  ## Module Architecture
52
72
 
73
+ > **Canonical tree**: [`REPO_MAP.md`](./REPO_MAP.md) is the single-source module map
74
+ > (src/ + rust/ trees, kept in sync). The trees below are the deep-dive annotations.
75
+
53
76
  ### Rust Modules
54
77
 
55
78
  One cdylib crate (`Cargo [lib] → rust/lib.rs`) decomposed into domain folders.
@@ -60,7 +83,8 @@ shared-infra names so legacy `crate::util::*` call sites keep working.
60
83
  rust/
61
84
  ├── lib.rs ── Crate root: folder declarations + module map, global allocator
62
85
  ├── test_support.rs ── shared #[cfg(test)] helpers (pack_headers, decode_packed_pairs, Rng)
63
- ├── unit_tests.rs ── cross-module test suite
86
+ ├── panic_safety.rs ── #[cfg(test)] cross-parser fuzz tests (malformed input never panics)
87
+ ├── proptest_suite.rs ── #[cfg(test)] property-based adversarial-parser tests
64
88
  │
65
89
  ├── util/ ── Shared infrastructure (mod.rs re-exports crate::util::*)
66
90
  │ ├── bytes.rs ── byte primitives (word-compare, hex, %XX decode, cookie_pairs)
@@ -97,9 +121,16 @@ rust/
97
121
  │ └── random_token.rs ── random hex tokens
98
122
  │
99
123
  ├── json/ ── JSON & schema
100
- │ ├── json_ops.rs ── zero-DOM validate/sum + DOM parse
124
+ │ ├── json_ops.rs ── zero-DOM validate/sum + DOM parse (sonic Value → JS)
125
+ │ ├── napi_marshal.rs ── sonic_rs::Value → napi walker (sonic_value_to_js, napi
126
+ │ │ serde-json Number parity) + sonic_values_equal
127
+ │ │ (jsonschema cmp::equal; shared by jwt_verify, json_parse,
128
+ │ │ fast_schema enum/const/uniqueItems)
101
129
  │ ├── json_ser.rs ── zero-alloc JSON escaping + cookie/query → JSON writers
102
- │ ├── json_patch_ops.rs ── RFC 6902 JSON patch
130
+ │ ├── patch/ ── CUSTOM sonic RFC 6902 engine (pointer.rs + ops.rs + engine.rs +
131
+ │ │ api.rs + tests.rs; Pointer ~0/~1 unescape + add/remove/replace/
132
+ │ │ move/copy/test incl. array `-`/bounds + 1==1.0; json-patch/jsonptr
133
+ │ │ deps REMOVED)
103
134
  │ ├── json_schema.rs ── SchemaValidator napi class (fast + jsonschema fallback)
104
135
  │ └── fast_schema/ ── zero-DOM draft-07 JSON Schema fast path
105
136
  │ └── mod.rs + types.rs / cursor.rs / compile.rs / validate.rs / errors.rs / email.rs (format:"email" replica) / tests.rs
@@ -137,7 +168,7 @@ src/
137
168
  │ ├── fast.ts ── Thin: createIngressFast (packed input) + re-exports
138
169
  │ ├── handlers.ts ── Thin: createIngressHandler (packs frame in JS → handleRequestPacked) + response builders
139
170
  │ ├── server.ts ── createIngressServer (Bun.serve builder) + buildRouteHandlers + gracefulShutdown
140
- │ ├── server-node.ts ── createIngressServerNode (node:http adapter, same route handlers)
171
+ │ ├── server-node.ts ── createIngressServerNode (node:http adapter, same route handlers; the runtime adapter picks the backend)
141
172
  │ ├── constants.ts ── Layout constants (from Rust NAPI)
142
173
  │ ├── shared.ts ── JS constants shared by both paths
143
174
  │ ├── status.ts ── Status normalization helpers (both paths)
@@ -153,14 +184,15 @@ src/
153
184
  │ └── routes/ ── read/head/json-write/echo/fallback factories
154
185
  │
155
186
  ├── shared/
156
- │ ├── runtime.ts ── isBun/isNode runtime detection (single seam)
187
+ │ ├── runtime.ts ── facade over src/runtime/detect.ts (isBun/isNode — cached)
188
+ │ ├── codec.ts ── facade over src/runtime/codec.ts (Bun-native UTF-8 codec)
189
+ │ ├── uuid.ts ── facade over src/runtime/uuid.ts
157
190
  │ ├── log.ts ── createStructuredLogger (CASTRUM_LOG_LEVEL-gated JSON lines)
158
191
  │ ├── request-id.ts ── zero-alloc request ID generator
159
192
  │ ├── buffer-pool.ts ── reusable output-buffer pool
160
193
  │ ├── response.ts ── pooledBodyResponse (zero-copy response)
161
- │ ├── bytes.ts ── shared TextEncoder/Decoder singletons
162
- │ ├── packed.ts ── packed-wire unpackers + packers
163
- │ └── proven.ts ── PROVEN_SURFACE registry (single source for `proven`)
194
+ │ ├── bytes.ts ── codec-backed encoder/decoder + toBytes/toText normalizers
195
+ │ └── packed.ts ── packed-wire unpackers + packers
164
196
  │
165
197
  ├── native/ ── Native transport: bun:ffi (primary on Bun) + NAPI fallback
166
198
  │ ├── types.ts ── NativeAddon interface + instance types
@@ -169,6 +201,18 @@ src/
169
201
  │ │ CASTRUM_FFI_MODE gating; castrum_ingress_layout blob)
170
202
  │ └── index.ts ── Barrel
171
203
  │
204
+ ├── runtime/ ── RUNTIME ADAPTER seam (Bun/Node), selected once at load
205
+ │ ├── detect.ts ── cached runtime detection (the ONLY place `typeof Bun` is checked)
206
+ │ ├── types.ts ── RuntimeAdapter contract (codec/uuid/env/builtins/transport/server/websocket)
207
+ │ ├── codec.ts ── runtime-native UTF-8 codec (Bun transfer vs TextEncoder/Decoder)
208
+ │ ├── uuid.ts ── uuidv7 (Bun.randomUUIDv7 vs crypto.randomUUID)
209
+ │ ├── builtins.ts ── BUN_WINS delegation registry (single source, `has(op)`)
210
+ │ ├── transport.ts ── bun:ffi-first `resolve(op)` + ffi/napi surfaces (lazy bind)
211
+ │ ├── native.ts ── FFI-facing seam (codec/uuid/env/builtins/transport) — imported by rust-ffi
212
+ │ ├── server.ts ── createIngressServer: Bun.serve on Bun, node:http on Node
213
+ │ ├── websocket.ts ── 101 upgrade on Bun; clear Bun-only error on Node
214
+ │ └── index.ts ── FULL adapter `runtime` (native + server + websocket) — for the public API
215
+ │
172
216
  ├── rust-ffi/ ── Rust FFI bindings
173
217
  │ ├── options.ts ── RustOptions + input-normalization helpers
174
218
  │ ├── addon.ts ── shared lazy addon proxy
@@ -223,7 +267,7 @@ src/
223
267
  > (`handle_request_packed`). Different wire format: success
224
268
  > `{"ok":true,...,"requestId":...}`, errors
225
269
  > `{"ok":false,"error":{code,message}}`, `ratelimit-*` headers. The
226
- > benchmark server (`bench/servers/ingress-server.ts`) uses this path and
270
+ > benchmark server (`bench/http/servers/ingress-server.ts`) uses this path and
227
271
  > re-exports the pre-baked functions.
228
272
 
229
273
  ## Node.js Compatibility
@@ -240,10 +284,12 @@ package ships a compiled ESM bundle built by `bun run build:js`:
240
284
  from multiple candidate roots so the same loader works from `src/native/…`
241
285
  (Bun, source layout) and `dist/…` (Node, bundled layout), plus the
242
286
  `CASTRUM_NATIVE_LIBRARY_PATH` / `NAPI_RS_NATIVE_LIBRARY_PATH` override.
243
- - `src/shared/runtime.ts` is the single runtime-detection seam (`isBun`/`isNode`).
244
- - `createIngressServer` (Bun.serve) is Bun-only; `createIngressServerNode`
245
- (`src/ingress/server-node.ts`) serves the SAME route handlers over `node:http`,
246
- sharing `buildRouteHandlers` in `server.ts`. The `BakedServer.server` type is a
287
+ - `src/runtime/detect.ts` is the single runtime-detection seam (the ONLY place
288
+ `typeof Bun` is checked); `src/shared/runtime.ts` is a thin facade over it.
289
+ - The public `createIngressServer` is runtime-adaptive: it delegates to the
290
+ Bun.serve builder (`src/ingress/server.ts`) on Bun and to `createIngressServerNode`
291
+ (`src/ingress/server-node.ts`, same route handlers over `node:http`) on Node,
292
+ via `src/runtime/server.ts`. The `BakedServer.server` type is a
247
293
  runtime-agnostic `ServerHandle` so Node TS consumers don't need `@types/bun`.
248
294
  - The benchmark tooling (`bench/`, `bun:test` suites) remains Bun-only by design.
249
295
 
@@ -385,6 +431,54 @@ Offset (bytes)
385
431
 
386
432
  ---
387
433
 
434
+ ## JSON & zero-DOM architecture
435
+
436
+ - **`sonic_rs::Value` is the primary DOM** on every crate boundary that allows
437
+ it (compact 16-byte tagged nodes, borrowed/inline strings, zero per-key heap
438
+ `String`). `serde_json::Value` REMAINS only where a crate is hardwired to it
439
+ and cannot be swapped: the jsonschema 0.48 DOM fallback
440
+ (`json_schema.rs` + `pipeline.rs::IngressSchema`), `selection.rs`, and the
441
+ napi JS-object input of `jwt_sign` / `JwtSigner::sign`.
442
+ - **`rust/json/napi_marshal.rs`** is the single sonic→JS bridge:
443
+ `sonic_value_to_js` walks a `sonic_rs::Value` into a napi value with
444
+ **byte-parity** to napi's `serde-json` `ToNapiValue for serde_json::Number`
445
+ (PosInt u64 ≤ u32::MAX → Number, larger → BigInt; NegInt within ±2^53-1 →
446
+ Number, outside → BigInt; Float → Number). `sonic_values_equal` replicates
447
+ jsonschema `cmp::equal` (exact cross-type numeric equality + insertion-order
448
+ object keys) for fast_schema enum/const/uniqueItems and is unit-pinned against
449
+ a serde_json reference over a value corpus.
450
+ - **Zero-DOM hot paths** (no `serde_json::Value` on the per-request path):
451
+ - `castrum_json_parse_packed` (FFI) parses ONCE with sonic-rs and emits a
452
+ typed token stream (deduped string table) that the JS decoder assembles
453
+ with NO second JSON text parse.
454
+ - JWT verify (`verify_token_with_key`) decodes the payload ONCE, checks
455
+ `exp`/`nbf`/`iat` over `sonic_rs::Value`, and returns the payload BYTES
456
+ (no re-serialize); `jwt_verify` / `JwtSigner::verify` marshal via the
457
+ walker. JWT sign (`jwtSignBytes`, `JwtSigner::signBytes`,
458
+ `castrum_jwt_sign_bytes*`) injects `iat`/`exp` into a sonic object
459
+ (`inject_and_payload_b64_sonic`) and serializes with `sonic_rs::to_vec`.
460
+ - fast_schema enum/const/uniqueItems re-parse the doc into `sonic_rs::Value`
461
+ and compare with `sonic_values_equal` (no `Vec<serde_json::Value>`).
462
+ - minijinja contexts are `sonic_rs::Value` (implements `Serialize`).
463
+ - **Custom RFC 6902 patch engine** (`rust/json/patch/`): no longer round-trips
464
+ through the serde_json-DOM `json-patch` crate. A `Pointer` type (`~0`/`~1`
465
+ unescape, RFC 6901) + `PatchOp` parser + applicator (add incl. array `-` and
466
+ bounds, remove, replace, move, copy, test incl. `1 == 1.0`) runs directly on
467
+ `sonic_rs::Value`, keeping `JsonPatchError` semantics, the input/output size
468
+ guards, and the packed batch entry points. `json-patch` and `jsonptr` deps
469
+ are removed.
470
+ - **Serialization determinism (`sonic sort_keys`)**: sonic's owned `Value`
471
+ backs MUTABLE objects with `AHashMap` (non-deterministic iteration order), so
472
+ the crate enables `sort_keys` (`Cargo.toml`), swapping the owned map to
473
+ `BTreeMap` → deterministic sorted serialization. This is a hard wire-format
474
+ requirement: JWT sign and the patch engine must emit byte-identical output
475
+ across the FFI/napi/scalar/bulk transports. Parsed (unmutated) values keep
476
+ the shared Vec (insertion order) — only `as_object_mut()` + `insert` switches
477
+ to the map — so `sonic_values_equal` / `sonic_value_to_js` (which iterate the
478
+ shared representation) keep insertion-order semantics.
479
+
480
+ ---
481
+
388
482
  ## Concurrency Model
389
483
 
390
484
  - **Synchronous calls**: Run on the main JS thread (Bun's event loop)
@@ -405,7 +499,7 @@ Offset (bytes)
405
499
  └─── [cookie_parser]
406
500
  [hashing] ──── [hmac_sha256]
407
501
  [validation]
408
- [json_ops] ──── [json_patch_ops]
502
+ [json_ops] ──── [json/patch]
409
503
  [json_schema]
410
504
  [url_codec]
411
505
  [mime_lookup]