castrum 0.9.0 → 0.9.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 (102) hide show
  1. package/README.md +153 -630
  2. package/castrum.linux-x64-gnu.node +0 -0
  3. package/dist/index.d.ts +1374 -1772
  4. package/dist/index.js +5570 -4169
  5. package/docs/API.md +337 -0
  6. package/docs/ARCHITECTURE.md +106 -14
  7. package/docs/BENCHMARKS.md +83 -47
  8. package/docs/CASE_STUDY.md +240 -0
  9. package/docs/ENVIRONMENT.md +8 -6
  10. package/docs/FFI_BUN_GUIDE.md +327 -0
  11. package/docs/GETTING_STARTED.md +15 -32
  12. package/docs/INGRESS-ROUTER.md +81 -0
  13. package/docs/INGRESS.md +5 -4
  14. package/docs/NATIVE-ROUTE.md +53 -0
  15. package/docs/REPO_MAP.md +46 -26
  16. package/docs/adr/0003-bun-builtins-delegation.md +6 -5
  17. package/docs/bun-builtins-decision-matrix.md +25 -18
  18. package/index.ts +9 -16
  19. package/package.json +36 -24
  20. package/src/ingress/body.ts +167 -27
  21. package/src/ingress/handlers.ts +91 -15
  22. package/src/ingress/health.ts +1 -0
  23. package/src/ingress/index.ts +39 -27
  24. package/src/ingress/packing/gather-raw-headers.ts +85 -21
  25. package/src/ingress/packing/header-packing.ts +5 -5
  26. package/src/ingress/packing/input-packer.ts +17 -3
  27. package/src/ingress/packing/select-headers.ts +12 -6
  28. package/src/ingress/router.ts +237 -0
  29. package/src/ingress/routes/common.ts +42 -1
  30. package/src/ingress/routes/fallback.ts +2 -2
  31. package/src/ingress/routes/head.ts +3 -13
  32. package/src/ingress/routes/index.ts +5 -4
  33. package/src/ingress/routes/json-write.ts +7 -31
  34. package/src/ingress/routes/options.ts +2 -12
  35. package/src/ingress/routes/read.ts +3 -13
  36. package/src/ingress/routes/responder.ts +159 -0
  37. package/src/ingress/server-node.ts +6 -6
  38. package/src/ingress/server.ts +45 -13
  39. package/src/ingress/types.ts +35 -1
  40. package/src/integration/pipeline.ts +3 -0
  41. package/src/integration/streaming.ts +2 -3
  42. package/src/integration/websocket.ts +4 -3
  43. package/src/loader/batch.ts +4 -0
  44. package/src/loader/cost.ts +1 -0
  45. package/src/loader/index.ts +43 -228
  46. package/src/loader/state.ts +84 -0
  47. package/src/loader/types.ts +145 -0
  48. package/src/native/ffi/constants.ts +58 -0
  49. package/src/native/ffi/selftest.ts +729 -0
  50. package/src/native/ffi/types.ts +474 -0
  51. package/src/native/ffi.ts +1063 -829
  52. package/src/native/loader.ts +52 -6
  53. package/src/native/types/instances.ts +274 -0
  54. package/src/native/types.ts +41 -238
  55. package/src/runtime/builtins.ts +131 -0
  56. package/src/runtime/codec.ts +147 -0
  57. package/src/runtime/detect.ts +36 -0
  58. package/src/runtime/index.ts +28 -0
  59. package/src/runtime/native.ts +35 -0
  60. package/src/runtime/server.ts +31 -0
  61. package/src/runtime/transport.ts +64 -0
  62. package/src/runtime/types.ts +173 -0
  63. package/src/runtime/uuid.ts +45 -0
  64. package/src/runtime/websocket.ts +30 -0
  65. package/src/rust-ffi/addon.ts +1 -1
  66. package/src/rust-ffi/batch/build.ts +155 -0
  67. package/src/rust-ffi/batch/index.ts +8 -0
  68. package/src/rust-ffi/batch/types.ts +92 -0
  69. package/src/rust-ffi/client.ts +5 -6
  70. package/src/rust-ffi/context.ts +39 -10
  71. package/src/rust-ffi/index.ts +28 -17
  72. package/src/rust-ffi/into.ts +21 -0
  73. package/src/rust-ffi/options.ts +1 -1
  74. package/src/rust-ffi/packed.ts +2 -2
  75. package/src/rust-ffi/proven.ts +16 -30
  76. package/src/rust-ffi/scalar/crypto.ts +188 -107
  77. package/src/rust-ffi/scalar/factories.ts +253 -27
  78. package/src/rust-ffi/scalar/hashing.ts +43 -41
  79. package/src/rust-ffi/scalar/http.ts +208 -77
  80. package/src/rust-ffi/scalar/index.ts +3 -3
  81. package/src/rust-ffi/scalar/interface.ts +88 -213
  82. package/src/rust-ffi/scalar/json-packed.ts +175 -0
  83. package/src/rust-ffi/scalar/json.ts +28 -12
  84. package/src/rust-ffi/scalar/payload.ts +159 -42
  85. package/src/rust-ffi/text.ts +31 -5
  86. package/src/selection.ts +27 -31
  87. package/src/shared/adaptive.ts +1 -2
  88. package/src/shared/buffer-pool.ts +72 -17
  89. package/src/shared/bytes.ts +75 -6
  90. package/src/shared/codec.ts +78 -0
  91. package/src/shared/env.ts +4 -4
  92. package/src/shared/log.ts +1 -0
  93. package/src/shared/packed/index.ts +10 -0
  94. package/src/shared/packed/parsers.ts +31 -0
  95. package/src/shared/packed/schema.ts +23 -0
  96. package/src/shared/{packed.ts → packed/wire.ts} +31 -39
  97. package/src/shared/proven.ts +189 -496
  98. package/src/shared/runtime.ts +20 -25
  99. package/src/shared/uuid.ts +12 -28
  100. package/src/data/json-rows.ts +0 -33
  101. package/src/rust-ffi/batch.ts +0 -431
  102. 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.rs`, 79 `castrum_*` symbols — 71 direct + 4
35
+ `validator_c_abi!` + 4 `compress_to_out!`, parity guarded by
36
+ `test/unit/features/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.
@@ -97,9 +120,15 @@ rust/
97
120
  │ └── random_token.rs ── random hex tokens
98
121
  │
99
122
  ├── json/ ── JSON & schema
100
- │ ├── json_ops.rs ── zero-DOM validate/sum + DOM parse
123
+ │ ├── json_ops.rs ── zero-DOM validate/sum + DOM parse (sonic Value → JS)
124
+ │ ├── napi_marshal.rs ── sonic_rs::Value → napi walker (sonic_value_to_js, napi
125
+ │ │ serde-json Number parity) + sonic_values_equal
126
+ │ │ (jsonschema cmp::equal; shared by jwt_verify, json_parse,
127
+ │ │ fast_schema enum/const/uniqueItems)
101
128
  │ ├── json_ser.rs ── zero-alloc JSON escaping + cookie/query → JSON writers
102
- │ ├── json_patch_ops.rs ── RFC 6902 JSON patch
129
+ │ ├── json_patch_ops.rs ── CUSTOM sonic RFC 6902 engine (Pointer ~0/~1 unescape +
130
+ │ │ add/remove/replace/move/copy/test incl. array `-`/bounds +
131
+ │ │ 1==1.0; json-patch/jsonptr deps REMOVED)
103
132
  │ ├── json_schema.rs ── SchemaValidator napi class (fast + jsonschema fallback)
104
133
  │ └── fast_schema/ ── zero-DOM draft-07 JSON Schema fast path
105
134
  │ └── mod.rs + types.rs / cursor.rs / compile.rs / validate.rs / errors.rs / email.rs (format:"email" replica) / tests.rs
@@ -137,7 +166,7 @@ src/
137
166
  │ ├── fast.ts ── Thin: createIngressFast (packed input) + re-exports
138
167
  │ ├── handlers.ts ── Thin: createIngressHandler (packs frame in JS → handleRequestPacked) + response builders
139
168
  │ ├── server.ts ── createIngressServer (Bun.serve builder) + buildRouteHandlers + gracefulShutdown
140
- │ ├── server-node.ts ── createIngressServerNode (node:http adapter, same route handlers)
169
+ │ ├── server-node.ts ── createIngressServerNode (node:http adapter, same route handlers; the runtime adapter picks the backend)
141
170
  │ ├── constants.ts ── Layout constants (from Rust NAPI)
142
171
  │ ├── shared.ts ── JS constants shared by both paths
143
172
  │ ├── status.ts ── Status normalization helpers (both paths)
@@ -153,14 +182,15 @@ src/
153
182
  │ └── routes/ ── read/head/json-write/echo/fallback factories
154
183
  │
155
184
  ├── shared/
156
- │ ├── runtime.ts ── isBun/isNode runtime detection (single seam)
185
+ │ ├── runtime.ts ── facade over src/runtime/detect.ts (isBun/isNode — cached)
186
+ │ ├── codec.ts ── facade over src/runtime/codec.ts (Bun-native UTF-8 codec)
187
+ │ ├── uuid.ts ── facade over src/runtime/uuid.ts
157
188
  │ ├── log.ts ── createStructuredLogger (CASTRUM_LOG_LEVEL-gated JSON lines)
158
189
  │ ├── request-id.ts ── zero-alloc request ID generator
159
190
  │ ├── buffer-pool.ts ── reusable output-buffer pool
160
191
  │ ├── 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`)
192
+ │ ├── bytes.ts ── codec-backed encoder/decoder + toBytes/toText normalizers
193
+ │ └── packed.ts ── packed-wire unpackers + packers
164
194
  │
165
195
  ├── native/ ── Native transport: bun:ffi (primary on Bun) + NAPI fallback
166
196
  │ ├── types.ts ── NativeAddon interface + instance types
@@ -169,6 +199,18 @@ src/
169
199
  │ │ CASTRUM_FFI_MODE gating; castrum_ingress_layout blob)
170
200
  │ └── index.ts ── Barrel
171
201
  │
202
+ ├── runtime/ ── RUNTIME ADAPTER seam (Bun/Node), selected once at load
203
+ │ ├── detect.ts ── cached runtime detection (the ONLY place `typeof Bun` is checked)
204
+ │ ├── types.ts ── RuntimeAdapter contract (codec/uuid/env/builtins/transport/server/websocket)
205
+ │ ├── codec.ts ── runtime-native UTF-8 codec (Bun transfer vs TextEncoder/Decoder)
206
+ │ ├── uuid.ts ── uuidv7 (Bun.randomUUIDv7 vs crypto.randomUUID)
207
+ │ ├── builtins.ts ── BUN_WINS delegation registry (single source, `has(op)`)
208
+ │ ├── transport.ts ── bun:ffi-first `resolve(op)` + ffi/napi surfaces (lazy bind)
209
+ │ ├── native.ts ── FFI-facing seam (codec/uuid/env/builtins/transport) — imported by rust-ffi
210
+ │ ├── server.ts ── createIngressServer: Bun.serve on Bun, node:http on Node
211
+ │ ├── websocket.ts ── 101 upgrade on Bun; clear Bun-only error on Node
212
+ │ └── index.ts ── FULL adapter `runtime` (native + server + websocket) — for the public API
213
+ │
172
214
  ├── rust-ffi/ ── Rust FFI bindings
173
215
  │ ├── options.ts ── RustOptions + input-normalization helpers
174
216
  │ ├── addon.ts ── shared lazy addon proxy
@@ -240,10 +282,12 @@ package ships a compiled ESM bundle built by `bun run build:js`:
240
282
  from multiple candidate roots so the same loader works from `src/native/…`
241
283
  (Bun, source layout) and `dist/…` (Node, bundled layout), plus the
242
284
  `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
285
+ - `src/runtime/detect.ts` is the single runtime-detection seam (the ONLY place
286
+ `typeof Bun` is checked); `src/shared/runtime.ts` is a thin facade over it.
287
+ - The public `createIngressServer` is runtime-adaptive: it delegates to the
288
+ Bun.serve builder (`src/ingress/server.ts`) on Bun and to `createIngressServerNode`
289
+ (`src/ingress/server-node.ts`, same route handlers over `node:http`) on Node,
290
+ via `src/runtime/server.ts`. The `BakedServer.server` type is a
247
291
  runtime-agnostic `ServerHandle` so Node TS consumers don't need `@types/bun`.
248
292
  - The benchmark tooling (`bench/`, `bun:test` suites) remains Bun-only by design.
249
293
 
@@ -385,6 +429,54 @@ Offset (bytes)
385
429
 
386
430
  ---
387
431
 
432
+ ## JSON & zero-DOM architecture
433
+
434
+ - **`sonic_rs::Value` is the primary DOM** on every crate boundary that allows
435
+ it (compact 16-byte tagged nodes, borrowed/inline strings, zero per-key heap
436
+ `String`). `serde_json::Value` REMAINS only where a crate is hardwired to it
437
+ and cannot be swapped: the jsonschema 0.48 DOM fallback
438
+ (`json_schema.rs` + `pipeline.rs::IngressSchema`), `selection.rs`, and the
439
+ napi JS-object input of `jwt_sign` / `JwtSigner::sign`.
440
+ - **`rust/json/napi_marshal.rs`** is the single sonic→JS bridge:
441
+ `sonic_value_to_js` walks a `sonic_rs::Value` into a napi value with
442
+ **byte-parity** to napi's `serde-json` `ToNapiValue for serde_json::Number`
443
+ (PosInt u64 ≤ u32::MAX → Number, larger → BigInt; NegInt within ±2^53-1 →
444
+ Number, outside → BigInt; Float → Number). `sonic_values_equal` replicates
445
+ jsonschema `cmp::equal` (exact cross-type numeric equality + insertion-order
446
+ object keys) for fast_schema enum/const/uniqueItems and is unit-pinned against
447
+ a serde_json reference over a value corpus.
448
+ - **Zero-DOM hot paths** (no `serde_json::Value` on the per-request path):
449
+ - `castrum_json_parse_packed` (FFI) parses ONCE with sonic-rs and emits a
450
+ typed token stream (deduped string table) that the JS decoder assembles
451
+ with NO second JSON text parse.
452
+ - JWT verify (`verify_token_with_key`) decodes the payload ONCE, checks
453
+ `exp`/`nbf`/`iat` over `sonic_rs::Value`, and returns the payload BYTES
454
+ (no re-serialize); `jwt_verify` / `JwtSigner::verify` marshal via the
455
+ walker. JWT sign (`jwtSignBytes`, `JwtSigner::signBytes`,
456
+ `castrum_jwt_sign_bytes*`) injects `iat`/`exp` into a sonic object
457
+ (`inject_and_payload_b64_sonic`) and serializes with `sonic_rs::to_vec`.
458
+ - fast_schema enum/const/uniqueItems re-parse the doc into `sonic_rs::Value`
459
+ and compare with `sonic_values_equal` (no `Vec<serde_json::Value>`).
460
+ - minijinja contexts are `sonic_rs::Value` (implements `Serialize`).
461
+ - **Custom RFC 6902 patch engine** (`json_patch_ops.rs`): no longer round-trips
462
+ through the serde_json-DOM `json-patch` crate. A `Pointer` type (`~0`/`~1`
463
+ unescape, RFC 6901) + `PatchOp` parser + applicator (add incl. array `-` and
464
+ bounds, remove, replace, move, copy, test incl. `1 == 1.0`) runs directly on
465
+ `sonic_rs::Value`, keeping `JsonPatchError` semantics, the input/output size
466
+ guards, and the packed batch entry points. `json-patch` and `jsonptr` deps
467
+ are removed.
468
+ - **Serialization determinism (`sonic sort_keys`)**: sonic's owned `Value`
469
+ backs MUTABLE objects with `AHashMap` (non-deterministic iteration order), so
470
+ the crate enables `sort_keys` (`Cargo.toml`), swapping the owned map to
471
+ `BTreeMap` → deterministic sorted serialization. This is a hard wire-format
472
+ requirement: JWT sign and the patch engine must emit byte-identical output
473
+ across the FFI/napi/scalar/bulk transports. Parsed (unmutated) values keep
474
+ the shared Vec (insertion order) — only `as_object_mut()` + `insert` switches
475
+ to the map — so `sonic_values_equal` / `sonic_value_to_js` (which iterate the
476
+ shared representation) keep insertion-order semantics.
477
+
478
+ ---
479
+
388
480
  ## Concurrency Model
389
481
 
390
482
  - **Synchronous calls**: Run on the main JS thread (Bun's event loop)