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,259 @@
1
+ # castrum — Repo Map (what is where, and why it works this way)
2
+
3
+ This is the **navigation doc**. It tells you where everything lives, how the
4
+ pieces connect, and how to run/build/test/bench/publish. If you are brand new,
5
+ read [`GETTING_STARTED.md`](./GETTING_STARTED.md) first; if you want deep-dive
6
+ architecture, read [`ARCHITECTURE.md`](./ARCHITECTURE.md). If you are an AI
7
+ agent editing this repo, [`AGENTS.md`](../AGENTS.md) is the agent-facing map.
8
+
9
+ ---
10
+
11
+ ## 1. What this project is
12
+
13
+ `castrum` is a hybrid **Bun (TypeScript) + Rust** package:
14
+
15
+ - A **Rust cdylib** (`castrum.<platform>-<arch>.node`, built with napi-rs,
16
+ ALSO exporting `extern "C"` symbols for Bun's `bun:ffi` — see
17
+ `rust/ffi.rs`) provides the performance-critical primitives (parsers,
18
+ crypto, hashing, JSON, compression, and the HTTP **ingress pipeline**).
19
+ Under Bun, `bun:ffi` is the PRIMARY transport; NAPI is the fallback
20
+ (Node, `CASTRUM_FFI_MODE=napi`, or a failed ffi self-test).
21
+ - **TypeScript** (`src/`) provides the ergonomic public API: the flat `rust.*`
22
+ FFI client, the `proven` performance surface, and the ingress layer.
23
+ - It ships benchmarks: a **CPU benchmark** (`bench.ts` → `src/bench`) and an
24
+ **HTTP benchmark** (`bench/run-bench.ts` + `bench/servers/*`).
25
+
26
+ Bun is the primary runtime; Node.js ≥20.3 is supported via a compiled ESM entry
27
+ (`dist/`).
28
+
29
+ ---
30
+
31
+ ## 2. Command cheat-sheet
32
+
33
+ | Task | Command | Notes |
34
+ |------|---------|-------|
35
+ | Install deps | `bun install` | |
36
+ | Build Rust addon (release) | `bun run build` | `napi build --release --platform` |
37
+ | Build Rust addon (LOCAL max perf) | `bun run build:perf` | x86-64-v3 + AVX2 — **never for publish** |
38
+ | Build Rust addon (debug) | `bun run build:debug` | |
39
+ | Build compiled JS entry (Node) | `bun run build:js` | bundle + types → `dist/` (gitignored) |
40
+ | TS unit tests | `bun test` | ~540 tests, `test/unit/**` |
41
+ | Rust unit tests | `bun run test:rust` | `cargo test`, ~440 tests |
42
+ | Node smoke tests | `bun run test:node` | explicit `.mjs` paths (Node 24 rejects dir arg) |
43
+ | Installed-tarball e2e | `bun run verify:install` | pack → install → import from `node_modules` |
44
+ | Typecheck | `bun run typecheck` | `bunx tsc --noEmit` (only `index.ts`, `bench.ts`, `src/`) |
45
+ | CPU benchmark + correctness | `bun run check` | == `bun bench.ts`; writes `bench/results/cpu/` |
46
+ | Proven-surface audit | `bun run check:proven[:fail]` | `:fail` gates CI on regressions |
47
+ | Performance JSDoc | `bun run check:annotate` | rewrites perf JSDoc from CPU report; `--dry-run` previews |
48
+ | HTTP bench (all servers) | `bun run bench:http` | |
49
+ | HTTP smoke (fast sanity) | `bun run bench:http:smoke` | **the wire-format guard** (CI-gated) |
50
+ | Startup bench | `bun run bench:startup` | import + first-call timing |
51
+ | JS lint (Biome) | `bun run lint` / `lint:fix` | `bunx biome check [--write] .` |
52
+ | JS format (Biome) | `bun run format` | `bunx biome format --write .` |
53
+ | Version consistency | `bun run check:version` | package.json ↔ Cargo.toml ↔ CHANGELOG |
54
+ | JS dependency audit | `bun run audit` | `bun audit` (reports advisories) |
55
+ | Cargo deny audit | `bun run deny` | `cargo deny check` |
56
+ | Publish (manual, single-platform) | `bun run publish:manual[:dry]` | see §6 |
57
+
58
+ ---
59
+
60
+ ## 3. Directory map
61
+
62
+ ### Source (what you edit)
63
+
64
+ ```
65
+ index.ts Package entry (Bun). Re-exports src/rust-ffi, proven, shared helpers, src/ingress.
66
+ bench.ts CPU benchmark entry point → src/bench/run.ts.
67
+
68
+ examples/ Runnable sample app (basic-server.ts + README) — see also README Quick Start.
69
+ basic-server.ts Minimal pre-baked ingress server + rust.* primitives.
70
+
71
+ src/
72
+ native/ The native transport layer: bun:ffi (primary on Bun) + NAPI fallback.
73
+ types.ts NativeAddon interface + every instance type (mirrors index.d.ts).
74
+ loader.ts Addon path resolution (multi-root) + getAddon()/lazyAddon().
75
+ ffi.ts bun:ffi C-ABI bindings (Bun-only): lazy bind + bind-time self-test,
76
+ CASTRUM_FFI_MODE gating, castrum_ingress_layout blob for constants.ts.
77
+ index.ts Barrel.
78
+ rust-ffi/ The flat `rust.*` FFI client (TS wrappers over the addon).
79
+ client.ts createRust() factory + the default `rust` instance + configure().
80
+ scalar/ / text.ts / batch.ts / packed.ts namespaces (buildScalar, buildText, ...).
81
+ context.ts Per-instance state (caches, rayon-pool bookkeeping).
82
+ options.ts RustOptions + rayon-thread resolution + coercion helpers.
83
+ addon.ts The single shared lazy addon proxy.
84
+ proven.ts `proven` client — the FULL rust.* surface, annotated (same object as `rust`).
85
+ index.ts Barrel (exports rust, createRust, types).
86
+ shared/ Cross-cutting helpers.
87
+ runtime.ts THE ONLY place `typeof Bun` is checked (isBun/isNode).
88
+ bytes.ts Shared TextEncoder/Decoder singletons + toPlainBuffer.
89
+ packed.ts Packed-wire unpackers (unpackU32Array, packBatch, readPairsPacked, ...).
90
+ request-id.ts Zero-alloc request-id generator (both ingress paths).
91
+ buffer-pool.ts Generic reusable byte-buffer pool (pooled ingress output).
92
+ response.ts pooledBodyResponse (releases the pool on body consume).
93
+ log.ts createStructuredLogger (CASTRUM_LOG_LEVEL-gated JSON lines).
94
+ proven.ts PROVEN_SURFACE registry (PURE DATA — no addon imports).
95
+ ingress/ The HTTP ingress pipeline (two paths — see §4.2).
96
+ fast.ts PATH 1: createIngressFast (packed-input via handleRequestPacked).
97
+ handlers.ts PATH 2: createIngressHandler (JS-packs the frame via IngressInputPacker
98
+ + gatherRawHeadersPacked → handleRequestPacked).
99
+ index.ts Barrel + createIngress / createIngressSync convenience factories.
100
+ shared.ts JS constants + buildHeaderPlan + assertSyncCallback (shared by both paths).
101
+ constants.ts Binary-layout constants read from Rust at runtime (SINGLE SOURCE).
102
+ status.ts / errors.ts / options.ts / types.ts / body.ts / context.ts
103
+ packing/ header-packing.ts, input-packer.ts, gather-raw-headers.ts
104
+ headers/ cors.ts, hsts.ts, fast-templates.ts, baked-templates.ts
105
+ decode/ fast-result.ts, baked-result.ts (TWO decoders — see §4.2)
106
+ response/ terminal.ts, error-bodies.ts
107
+ routes/ read/head/json-write/echo/fallback factories + common.ts
108
+ server.ts createIngressServer (Bun.serve) + buildRouteHandlers + gracefulShutdown.
109
+ server-node.ts createIngressServerNode (node:http adapter, same route handlers).
110
+ integration/ Framework integration (wraps path 2 — createIngressHandler).
111
+ pipeline.ts createPipeline (framework-agnostic request stage for Hono/Elysia/Bun.serve).
112
+ websocket.ts createWebSocketUpgrade (RFC 6455 101 handshake + subprotocol).
113
+ streaming.ts sseResponse (SSE frames via rust.sseEncodeEvent).
114
+ baseline/ Pure-JS baseline implementations (benchmark reference). Internal.
115
+ bench/ CPU benchmark framework (tasks, measure, report, checks, comparisons).
116
+ data/ json-rows.ts fixture generator (benchmark-only).
117
+ ```
118
+
119
+ ```
120
+ rust/ ONE cdylib crate (Cargo [lib] → lib.rs).
121
+ lib.rs Declaration hub + module map + crate docs.
122
+ util/ Shared infrastructure: bytes, packed, batch (+batch_core), threadpool, validation.
123
+ http/ HTTP wire formats & parsing: headers, method, http/cookie/query parsers,
124
+ form, media_type, url_codec, url_join, etag, accept, mime_lookup, multipart.
125
+ crypto/ Auth & hashing: hmac_sha256, cookie_sign, csrf, jwt, aead, argon2,
126
+ base64, hashing (fnv/crc32/xxh3), random_token.
127
+ json/ JSON & schema: json_ops, json_ser, json_patch_ops, json_schema (napi),
128
+ fast_schema/ (zero-DOM engine — pure, no napi).
129
+ payload/ Output & streaming: compress, sse, ws_frames, websocket, template.
130
+ ingress/ THE ingress pipeline: mod.rs (napi boundary), pipeline.rs (core 8-stage),
131
+ options/time/packed, cors, proxy, ip_trust, rate_limit, terminal,
132
+ output.rs (single numeric layout source), ingress_constants.rs (napi projection).
133
+ ffi.rs #[no_mangle] extern "C" exports (45 castrum_* symbols) for Bun's bun:ffi
134
+ primary transport, incl. castrum_ingress_layout (the layout blob).
135
+ unit_tests.rs / test_support.rs Cross-module Rust test suites.
136
+ ```
137
+
138
+ ### Generated / ignored (do NOT edit)
139
+
140
+ | Path | What it is | Why ignored |
141
+ |------|-----------|-------------|
142
+ | `dist/` | Compiled ESM entry + types for Node | Built by `build:js`; gitignored |
143
+ | `index.js` / `index.d.ts` | NAPI-RS generated loader/declarations | Regenerated by `napi build`; gitignored |
144
+ | `castrum.<platform>-<arch>.node` | The native addon binary | Built locally; gitignored |
145
+ | `bench/results/` | Bench reports (cpu/, http/) | Generated; gitignored |
146
+ | `target/` | Cargo build output | gitignored |
147
+ | `node_modules/` | Deps | gitignored |
148
+ | `artifacts/` | CI publish staging dir | gitignored |
149
+
150
+ ---
151
+
152
+ ## 4. How the pieces connect (the 5 cross-cutting flows)
153
+
154
+ ### 4.1 Constants never drift (single numeric source)
155
+ `rust/ingress/output.rs` defines every ingress layout constant once → projected
156
+ to JS two ways: `rust/ingress/ingress_constants.rs` via `#[napi] const` (NAPI)
157
+ AND `rust/ffi.rs` `IngressLayout` via `castrum_ingress_layout` (C ABI, primary
158
+ on Bun) → `src/ingress/constants.ts` reads them at runtime (ffi blob on Bun,
159
+ napi on Node). Drift is pinned by the Rust unit test
160
+ `ingress_layout_c_abi_matches_output_source` and the bun:ffi bind-time
161
+ self-test. **Never hardcode a layout value** — if you change the layout, change
162
+ `output.rs` and nothing else.
163
+
164
+ ### 4.2 The TWO ingress paths (do not conflate)
165
+ 1. **`src/ingress/fast.ts`** — `createIngressFast(options)`. JS packs request
166
+ headers into a binary buffer (`IngressInputPacker`), Rust parses via
167
+ `handleRequestPacked`. Responses: `{"error":{code,status,message,requestId}}`
168
+ with `x-ratelimit-*` headers.
169
+ 2. **`src/ingress/handlers.ts`** — `createIngressHandler(options, runtime)`.
170
+ The request frame is packed in JS (`IngressInputPacker` +
171
+ `gatherRawHeadersPacked`) and driven through the SAME native core as path 1
172
+ (`handleRequestPacked`), writing into a **pooled** output buffer. Responses:
173
+ `{"ok":true,...,"requestId":...}` (success) / `{"ok":false,"error":{code,
174
+ message}}` (errors) with `ratelimit-*` headers.
175
+
176
+ These wire formats are a **benchmark contract** (`bench/load.ts` validates
177
+ them) — they are intentionally NOT unified. The two decoders
178
+ (`decode/fast-result.ts` vs `decode/baked-result.ts`) and the two header-template
179
+ builders (`headers/fast-templates.ts` vs `headers/baked-templates.ts`) exist for
180
+ this reason. **Shared between both paths**: `generateRequestId`
181
+ (`src/shared/request-id.ts`) and `buildHeaderPlan` / `assertSyncCallback`
182
+ (`src/ingress/shared.ts`).
183
+
184
+ ### 4.3 Addon loading + runtime seam
185
+ - `src/native/loader.ts` resolves the `.node` from multiple roots so it works
186
+ from both the source layout (`src/…`) and the bundled layout (`dist/…`).
187
+ Honors `CASTRUM_NATIVE_LIBRARY_PATH` / `NAPI_RS_NATIVE_LIBRARY_PATH`.
188
+ - Runtime detection lives **only** in `src/shared/runtime.ts`
189
+ (`isBun`/`isNode`). `createIngressServer` is Bun-only; Node users use
190
+ `createIngressServerNode` (same route handlers via `buildRouteHandlers`).
191
+ - ⚠️ `src/ingress/constants.ts` is the **one module that dlopens the addon at
192
+ import time** (its constants are needed as soon as ingress is imported).
193
+ Everything else is lazy.
194
+
195
+ ### 4.4 The `proven` performance surface
196
+ `src/shared/proven.ts` (`PROVEN_SURFACE`) is the single source of truth for the
197
+ per-function performance classifications. It is **pure data** (no addon
198
+ imports) so `scripts/check-proven.ts` can audit it without dlopening.
199
+ `src/rust-ffi/proven.ts` exposes the FULL `rust.*` surface (nothing is
200
+ filtered); each function's JSDoc carries its measured performance vs the JS
201
+ baseline, written by `scripts/annotate-performance.ts` (`bun run check:annotate`)
202
+ and marked `@deprecated` when slower. `bun run check:proven:fail` (CI) audits
203
+ the registry against the CPU benchmark report on a **release** build.
204
+
205
+ ### 4.5 Pooled output buffers (path 2)
206
+ `createIngressHandler` owns a `BufferPool` (`src/shared/buffer-pool.ts`) sized
207
+ by `runtime.outputBufferSize`. `run()` packs the frame in JS (`IngressInputPacker`
208
+ + `gatherRawHeadersPacked`) and writes via `handleRequestPacked`
209
+ into the pooled buffer, passing the exact written subarray to
210
+ `BakedIngressResult.refresh`. In copy
211
+ mode the buffer is released at the end of `run()`; in zero-copy mode
212
+ `ingress.zeroCopyResponse()` serves a `pooledBodyResponse`
213
+ (`src/shared/response.ts`) that keeps the buffer in flight until the body is
214
+ consumed.
215
+
216
+ ---
217
+
218
+ ## 5. Testing & benchmarks
219
+
220
+ - **TS**: `bun test` → `test/unit/**` (`ingress/`, `shared/`, `features/`).
221
+ - **Rust**: `cargo test` → per-module `#[cfg(test)] mod tests` + cross-module
222
+ `rust/unit_tests.rs`.
223
+ - **Node**: `test/integration/node-smoke.test.mjs` + `node-enterprise.test.mjs`
224
+ via `node --test` (run with `bun run test:node`).
225
+ - **CPU bench**: `bun run check` (correctness checks + comparisons). Sub-µs ops
226
+ are batched (`CASTRUM_BENCH_BATCH_SIZE`). Report → `bench/results/cpu/`.
227
+ - **HTTP bench**: `bench/run-bench.ts` + `bench/load.ts` (load generator that
228
+ validates response SHAPE) + `bench/servers/{bun,elysia,ingress}-server.ts`.
229
+ `bench:http:smoke` is the fast CI wire-format gate.
230
+
231
+ ---
232
+
233
+ ## 6. Publishing (npm)
234
+
235
+ - Package name `castrum` (unscoped, public). Native binary name `castrum`
236
+ (declared in package.json `napi.binaryName` + Cargo.toml — keep in sync).
237
+ - **Multi-platform (recommended)**: push a `v*` tag → `.github/workflows/ci.yml`
238
+ builds each platform addon, the `publish` job downloads them into
239
+ `./artifacts`, stages them, and runs `npm publish` (needs `NPM_TOKEN`).
240
+ - **Manual single-platform**: `bun run publish:manual --increment minor`
241
+ (syncs package.json ↔ Cargo.toml ↔ CHANGELOG, tags, builds, publishes with
242
+ `CASTRUM_PUBLISH_ALLOW_PARTIAL=1`). `--dry-run` plans only.
243
+ - `prepublishOnly` runs `build:js` + `scripts/prepublish.mjs` (stages
244
+ `artifacts/*.node` and verifies every `napi.targets` maps to a `.node`).
245
+ - Run `bun run check:version` before tagging.
246
+
247
+ ---
248
+
249
+ ## 7. Cross-links
250
+
251
+ | Doc | Purpose |
252
+ |-----|---------|
253
+ | [`GETTING_STARTED.md`](./GETTING_STARTED.md) | Beginner tutorial (intern-friendly) |
254
+ | [`ARCHITECTURE.md`](./ARCHITECTURE.md) | Deep-dive: modules, data flow, output layout, memory, concurrency |
255
+ | [`INGRESS.md`](./INGRESS.md) | The pre-baked ingress API + route factories + servers |
256
+ | [`BENCHMARKS.md`](./BENCHMARKS.md) | Benchmark scenarios, report format, how to run |
257
+ | [`ENVIRONMENT.md`](./ENVIRONMENT.md) | All `CASTRUM_*` env vars (+ legacy aliases) |
258
+ | [`AGENTS.md`](../AGENTS.md) | AI-agent editing guidance (commands, constraints, gotchas) |
259
+ | `README.md` | Public-facing quick start + API reference |
@@ -0,0 +1,39 @@
1
+ # ADR-0001: The two ingress wire formats are a contract, not a bug
2
+
3
+ - **Status**: Accepted (2026-08-06)
4
+ - **Deciders**: maintainers (via benchmark + hardening passes)
5
+
6
+ ## Context
7
+
8
+ The ingress layer ships two paths that consume the SAME native core
9
+ (`handleRequestPacked`):
10
+
11
+ 1. `src/ingress/fast.ts` — `createIngressFast`; JS packs headers via
12
+ `IngressInputPacker`; errors are `{"error":{code,status,message,requestId}}`
13
+ with `x-ratelimit-*` headers.
14
+ 2. `src/ingress/handlers.ts` — `createIngressHandler`; JS packs the full frame;
15
+ success is Rust-generated `{"ok":true,...,"requestId":...}`, errors are
16
+ `{"ok":false,"error":{"code","message"}}`, with `ratelimit-*` headers.
17
+
18
+ They also use separate result decoders (`FastIngressResult` vs
19
+ `BakedIngressResult`) and separate header-template builders.
20
+
21
+ ## Decision
22
+
23
+ **Do not unify them.** The HTTP benchmark load generator (`bench/load.ts`)
24
+ requires `ok === true` + `requestId: string` on success and `error.code` /
25
+ `error.message` on errors — i.e. path 2's format. The two formats are a
26
+ **benchmark contract**: the benchmark server uses path 2, and changing its
27
+ wire shape invalidates baselines.
28
+
29
+ ## Consequences
30
+
31
+ - Both paths stay hot and zero-alloc.
32
+ - Duplicated decoders/templates are intentional; sharing them is
33
+ **forbidden** by AGENTS.md (it would couple two differently-shaped outputs).
34
+ - New contributors must not "helpfully" unify the formats.
35
+
36
+ ## See also
37
+
38
+ - AGENTS.md §"Ingress: the two paths (do NOT conflate)"
39
+ - docs/INGRESS.md §"Wire format"
@@ -0,0 +1,38 @@
1
+ # ADR-0002: Zero-DOM fast path for JSON Schema validation
2
+
3
+ - **Status**: Accepted (2026-08-07), extended 2026-08-11
4
+ - **Deciders**: maintainers (measured via `bun run check`)
5
+
6
+ ## Context
7
+
8
+ `jsonschema::Validator::is_valid` requires a `serde_json::Value` — a full DOM
9
+ per document, measured at ~95% of validation cost. That made the Rust schema
10
+ path parity with `ajv` at best, so castrum couldn't match its own zero-DOM
11
+ wins (json_valid / json_sum / packed parsers).
12
+
13
+ ## Decision
14
+
15
+ Implement a **zero-DOM draft-07 fast path** (`rust/json/fast_schema/`) that
16
+ compiles the common keyword subset into a `FastNode` AST and validates raw
17
+ bytes with a zero-alloc cursor. Unsupported keywords → `compile()` returns
18
+ `Err` → the caller falls back to the authoritative `jsonschema` crate (DOM
19
+ path). Parity is **byte-for-byte gated** against the crate
20
+ (`rust/json/fast_schema/tests.rs`).
21
+
22
+ Draft-07 is pinned because `jsonschema` 0.48's default draft is 2020-12 whose
23
+ `items` / `$ref`-sibling / boolean-exclusive semantics differ; both
24
+ `SchemaValidator` and `IngressSchema` build the reference with
25
+ `.with_draft(Draft7)` + `should_validate_formats(true)`.
26
+
27
+ ## Consequences
28
+
29
+ - Fast path: scalar ~1.2–2.4x, batch ~1.0–1.56x vs `ajv` (release build).
30
+ - A `$schema` declaring non-draft-07 (or unsupported keywords) still falls
31
+ back — versatility preserved.
32
+ - The fast path MUST stay byte-parity with the crate for the supported
33
+ keyword surface; any divergence is a bug.
34
+
35
+ ## See also
36
+
37
+ - rust/json/fast_schema/tests.rs (parity corpus)
38
+ - docs/ARCHITECTURE.md §JSON & schema
@@ -0,0 +1,42 @@
1
+ # ADR-0003: Don't reinvent the wheel — benchmark, then delegate to Bun built-ins
2
+
3
+ - **Status**: Accepted (2026-08-11)
4
+ - **Deciders**: maintainers (measured on Bun 1.4.0 / release addon)
5
+
6
+ ## Context
7
+
8
+ castrum ships Rust ops that overlap with Bun's native built-ins
9
+ (`Bun.hash`, `Bun.password`, `Bun.CryptoHasher`, `Bun.gzipSync`,
10
+ `Bun.randomUUIDv7`, `crypto.subtle`, `JSON.parse`, `URLSearchParams`). For the
11
+ same workload, the in-process Bun implementation can beat an FFI-crossing Rust
12
+ op — spending effort building or keeping such ops is reinventing the wheel.
13
+
14
+ ## Decision
15
+
16
+ Every overlapping op is **benchmarked against its Bun built-in** before it is
17
+ built, kept, or delegated. Results live in
18
+ `docs/bun-builtins-decision-matrix.md`. Rules:
19
+
20
+ 1. **Delegate to Bun** at the `@flux/native` wrapper layer where Bun wins
21
+ clearly: non-crypto hashing (`Bun.hash.crc32` ~3x, `xxHash3` ~4x),
22
+ gzip (`Bun.gzipSync`/`gunzipSync`), `Bun.randomUUIDv7` (~2x), HMAC
23
+ (`Bun.CryptoHasher` ~1.2x).
24
+ 2. **Keep rust** where it wins or Bun has no synchronous equivalent:
25
+ argon2id password hashing (~1.8–2x vs `Bun.password`), bcrypt verify
26
+ (~1.5x), brotli, PBKDF2, zero-DOM JSON/parsers.
27
+ 3. **Don't build** what Bun already wins at (UUIDv7 → `uuidv7()` delegates to
28
+ `Bun.randomUUIDv7`, falls back to `crypto.randomUUID`).
29
+
30
+ ## Consequences
31
+
32
+ - New `rust.*` primitives ship with a Bun-built-in baseline + decision-matrix
33
+ entry (`xxh3`, `passwordHashBcrypt`/`verifyBcrypt`, `pbkdf2Sha256`).
34
+ - `PROVEN_SURFACE` classifications reflect the Bun/JS baseline honestly
35
+ (e.g. `xxh3` is `not-competitive` vs `Bun.hash.xxHash3`).
36
+ - Diagnostic benchmark tasks use `diag:` names so they never perturb the
37
+ `check:proven` audit.
38
+
39
+ ## See also
40
+
41
+ - docs/bun-builtins-decision-matrix.md
42
+ - src/bench/tasks/bun-builtins.ts
@@ -0,0 +1,108 @@
1
+ # Bun Built-ins vs castrum — Decision Matrix
2
+
3
+ > **Question this answers**: for the same workload, does a Bun native built-in
4
+ > beat the castrum Rust op? Where it does, castrum should stop reinventing the
5
+ > wheel and `@flux/native` should delegate to Bun; where castrum wins (or Bun
6
+ > has no sync equivalent), the Rust op stands.
7
+
8
+ ## Methodology
9
+
10
+ - **Runtime**: Bun **1.4.0** (the primary target).
11
+ - **Addon**: castrum **release** build (`bun run build`, baseline CPU — the
12
+ shipped profile).
13
+ - **Driver**: `bun run check` (CPU benchmark → `bench/results/cpu/latest.json`).
14
+ Each pair runs on the SAME input (fixture payloads, see table).
15
+ - **Naming**: diagnostic comparisons use `diag:` task names so
16
+ `scripts/check-proven.ts` never aggregates them into a `PROVEN_SURFACE`
17
+ entry (see `src/bench/comparisons.ts`).
18
+ - **Honesty guard**: every Bun API is verified to actually execute real work at
19
+ runtime before a result is trusted. This is important — the first run
20
+ produced false "wins" (brotli 156×, validators 3–5×) because this Bun
21
+ version has **no** `Bun.brotliCompressSync` and **no** `Bun.validators`, and
22
+ the feature-detect fell through to an empty return. Those pairs were removed,
23
+ not reported.
24
+ - Ratio is **nativeAvgMs / rustAvgMs** (>1 = Bun faster; <1 = rust faster).
25
+
26
+ ## Results (Bun 1.4.0, release addon, 2026-08-11)
27
+
28
+ | castrum op | Bun built-in | Input | Winner | Ratio |
29
+ |---|---|---|---|---|
30
+ | `rust.fnv1a64` | `Bun.hash` (wyhash) | 42 B | Bun (parity) | 1.06× |
31
+ | `rust.crc32` | `Bun.hash.crc32` | 42 B | **Bun** | 2.8–8.4× |
32
+ | `rust.xxh3` (new) | `Bun.hash.xxHash3` | 42 B | **Bun** | 4.15× |
33
+ | `rust.hmacSha256` | `Bun.CryptoHasher("sha256", key)` | 32 B | Bun (mild) | 1.1–1.4× |
34
+ | `rust.passwordHash` (argon2id) | `Bun.password.hashSync` (argon2id) | 12 B | **rust** | 0.55× (rust 1.83×) |
35
+ | `rust.passwordVerify` | `Bun.password.verifySync` | 12 B | **rust** | 0.53× (rust 1.88×) |
36
+ | `rust.passwordHashBcrypt` (new) | `Bun.password.hashSync` (bcrypt) | 12 B | Bun (mild) | 1.24× |
37
+ | `rust.passwordVerifyBcrypt` (new) | `Bun.password.verifySync` (bcrypt) | 12 B | **rust** | 0.67× (rust 1.49×) |
38
+ | `rust.pbkdf2Sha256` (new) | `node:crypto.pbkdf2Sync` | 12 B | rust (parity) | 0.93× (rust 1.08×) |
39
+ | `rust.randomToken` (16 B) | `Bun.randomUUIDv7` | — | **Bun** | 1.62× |
40
+ | `rust.gzipCompress` | `Bun.gzipSync` | 11 179 B | **Bun** | 2.02× |
41
+ | `rust.gzipDecompress` | `Bun.gunzipSync` | 1 045 B | **Bun** | 1.38× |
42
+ | `rust.brotliCompress` | *no sync Bun API* | — | N/A | — |
43
+ | `rust.brotliDecompress` | *no sync Bun API* | — | N/A | — |
44
+ | `rust.validateEmail/Uuid/Ipv4/Ipv6` | `Bun.validators` (*absent in 1.4*) | — | N/A | — |
45
+
46
+ > Same-cost note: `Bun.password.hashSync` ran at `memoryCost 4096 / timeCost 1`
47
+ > (4 MiB, 1 iter) matching the rust bench cost (`mCost 4096 / tCost 1`) — the
48
+ > rust argon2id win is cost-for-cost, not a cost cheat.
49
+
50
+ > Transport note: "FFI-crossing Rust op" below means any Rust call over the
51
+ > native bridge. Under Bun that bridge is now `bun:ffi` (the PRIMARY transport);
52
+ > under Node it is the napi fallback. This Bun-builtin-vs-Rust axis is
53
+ > independent of the ffi-vs-napi transport axis — these decisions recommend
54
+ > WHICH implementation to call (`rust.*` vs a Bun built-in), not which transport
55
+ > carries the call.
56
+
57
+ ## Decisions
58
+
59
+ ### → Delegate to Bun at the `@flux/native` wrapper layer (Bun wins)
60
+ These rust ops lose to Bun's native implementation on the SAME workload — the
61
+ FFI crossing cannot beat Bun's in-process C++:
62
+
63
+ | Op | Recommendation |
64
+ |---|---|
65
+ | `crc32` | Prefer `Bun.hash.crc32`; keep rust as fallback (pure-TS path already exists). |
66
+ | `xxh3` | Prefer `Bun.hash.xxHash3` (~4×); keep the rust export as the Node/non-Bun fast path. Classified `not-competitive` in `PROVEN_SURFACE`. |
67
+ | `gzipCompress` / `gzipDecompress` | Prefer `Bun.gzipSync` / `Bun.gunzipSync`. |
68
+ | `randomToken` | Prefer `Bun.randomUUIDv7()` (or `crypto.getRandomValues`) for token-sized output; keep rust for byte-precise control. |
69
+ | `hmacSha256` | Prefer `Bun.CryptoHasher("sha256", key)` (1.17× — mild; keep rust batch path, which wins on larger inputs). |
70
+ | `fnv1a64` | Parity — either is fine; `Bun.hash` (wyhash) is a stronger algorithm, prefer it for NEW non-crypto hashing needs. |
71
+
72
+ **Delegation rule of thumb**: rust stays the default only where it is
73
+ `proven` against its **Bun** baseline (not just its JS baseline). These are the
74
+ ops where rust genuinely wins or Bun has no equivalent.
75
+
76
+ ### → Keep rust (rust wins or Bun has no equivalent)
77
+ | Op | Rationale |
78
+ |---|---|
79
+ | `passwordHash` / `passwordVerify` (argon2id) | rust 1.83–1.88× faster than `Bun.password` at equal cost. **Do not delegate.** |
80
+ | `passwordHashBcrypt` / `passwordVerifyBcrypt` (new) | Hash ≈ parity (Bun 1.24×), verify rust 1.49×. Keep rust — self-contained and works under Node. |
81
+ | `pbkdf2Sha256` (new) | Bun has no sync PBKDF2; rust ≈ parity with node:crypto (1.08×). Keep rust. |
82
+ | `brotliCompress` / `brotliDecompress` | Bun 1.4 has no synchronous brotli; keep rust (async `CompressionStream` is not drop-in). |
83
+ | `validateEmail` / `validateUuid` / `validateIpv4` / `validateIpv6` | `Bun.validators` absent in 1.4; keep rust. Re-check when Bun adds it. |
84
+ | `jsonValid` / `jsonSumIds` / packed parsers / ingress | No Bun sync equivalent for the zero-DOM / zero-copy semantics; keep rust. |
85
+
86
+ ### → Add primitives that close gaps (Phase 2)
87
+ | Gap | Plan |
88
+ |---|---|
89
+ | Public XXH3-64 (`rust.xxh3`) | **Done** — exposed; measured Bun.hash.xxHash3 4.15× faster → classify `not-competitive`, prefer Bun under Bun. |
90
+ | bcrypt | **Done** — `rust.passwordHashBcrypt`/`passwordVerifyBcrypt`; hash parity, verify rust 1.49×. |
91
+ | PBKDF2 | **Done** — `rust.pbkdf2Sha256`; parity with node:crypto, only sync option in Bun. |
92
+ | UUIDv7 | `Bun.randomUUIDv7` already wins — **do not** build a rust UUIDv7; use Bun. |
93
+
94
+ ## Summary
95
+
96
+ **The "don't reinvent the wheel" takeaway:** for non-cryptographic hashing,
97
+ random tokens, and gzip, Bun 1.4's built-ins are already faster than any
98
+ FFI-crossing Rust op — `@flux/native` should prefer them. Rust retains a clear,
99
+ defensible moat exactly where it matters: **argon2id password hashing
100
+ (≈1.9× vs Bun)**, the **zero-DOM / zero-copy parsers and JSON paths** (no Bun
101
+ sync equivalent), and **brotli** (no Bun sync API).
102
+
103
+ ## Re-run
104
+
105
+ ```sh
106
+ bun run build # release addon
107
+ bun run check # writes latest.json; grep the "vs Bun" lines
108
+ ```
package/index.ts CHANGED
@@ -1,8 +1,43 @@
1
- export * from "./src/rust-ffi";
1
+ // index.ts — castrum package entry (barrel).
2
+ //
3
+ // Re-exports the full public API: the flat `rust.*` FFI surface
4
+ // (src/rust-ffi), the runtime selection hints (src/selection), the
5
+ // performance-proven registry (src/shared/proven), and the higher-order
6
+ // loader (src/loader). See AGENTS.md for the layout map.
2
7
 
3
- export * as native from "./src/baseline";
8
+ export * from './src/rust-ffi'
4
9
 
5
- export { encoder, decoder } from "./src/shared/bytes";
10
+ // Native-vs-JS selection (OWNED HERE, not by consumers): `opImpl(op)` is the
11
+ // benchmark-driven recommendation for which operations should use the Rust
12
+ // addon vs a pure-JS implementation. Consumers read it once at load time and
13
+ // bind each op to a fixed impl. Source: rust/selection.rs (audited by
14
+ // `bun run check:selection`). See src/selection.ts.
15
+ export { opImpl, isNativeOp, opDecision } from './src/selection'
16
+ export type { OpImpl, OpDecision } from './src/selection'
17
+
18
+ // Performance-proven surface: only `rust.*` functions that beat their JS
19
+ // baseline in benchmarks. See src/shared/proven.ts for the registry.
20
+ export {
21
+ proven,
22
+ PROVEN_SURFACE,
23
+ provenStatus,
24
+ isProven,
25
+ provenSurface,
26
+ provenSummary,
27
+ } from './src/rust-ffi/proven'
28
+ export type { ProvenClient, ProvenKey } from './src/rust-ffi/proven'
29
+ export type { PerformanceStatus, ProvenEntry } from './src/shared/proven'
30
+
31
+ export { encoder, decoder } from './src/shared/bytes'
32
+ // Bounded EWMA adaptive-estimate utility (drives BufferPool adaptive sizing
33
+ // and reusable runtime self-optimization decisions).
34
+ export { AdaptiveEstimate } from './src/shared/adaptive'
35
+ export type { AdaptiveEstimateOptions } from './src/shared/adaptive'
36
+ // UUIDv7 generation — delegates to Bun.randomUUIDv7, crypto.randomUUID on Node.
37
+ export { uuidv7 } from './src/shared/uuid'
38
+ // Zero-dep metrics registry (counters/gauges/histograms + Prometheus render).
39
+ export { createMetrics, DEFAULT_BUCKETS } from './src/shared/metrics'
40
+ export type { MetricsRegistry, Counter, Gauge, Histogram } from './src/shared/metrics'
6
41
 
7
42
  export {
8
43
  packBatch,
@@ -12,13 +47,21 @@ export {
12
47
  pairsToObject,
13
48
  parseQueryString,
14
49
  parseCookieHeader,
50
+ parseFormBody,
15
51
  unpackBitset,
16
52
  unpackByteResults,
17
53
  unpackI64ArrayAsBigInt,
18
54
  unpackU32Array,
19
- } from "./src/shared/packed";
20
- export type { Pair, ParsedHttpRequestPacked } from "./src/shared/packed";
55
+ } from './src/shared/packed'
56
+ export type { Pair, ParsedHttpRequestPacked } from './src/shared/packed'
57
+
58
+ export * from './src/ingress'
59
+
60
+ // Framework-agnostic integration helpers: run the ingress pipeline as a
61
+ // request stage (createPipeline), RFC 6455 upgrade (createWebSocketUpgrade),
62
+ // and SSE framing (sseResponse). See docs/INGRESS.md.
63
+ export * from './src/integration'
21
64
 
22
- export { jsonRowsBytes, createJsonRows } from "./src/data/json-rows";
23
- export type { JsonRow } from "./src/data/json-rows";
24
- export * from "./src/ingress";
65
+ // Higher-order loader (HFC) over the curated op set: createLoader / loader
66
+ // dispatch single items, packed batches, and load() coalescing. See README.
67
+ export * from './src/loader'