castrum 0.9.1 → 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 (49) hide show
  1. package/README.md +1 -1
  2. package/castrum.linux-x64-gnu.node +0 -0
  3. package/dist/index.d.ts +239 -5
  4. package/dist/index.js +3852 -3375
  5. package/docs/ARCHITECTURE.md +11 -9
  6. package/docs/BENCHMARKS.md +70 -38
  7. package/docs/CASE_STUDY.md +10 -7
  8. package/docs/ENVIRONMENT.md +16 -1
  9. package/docs/FFI_BUN_GUIDE.md +18 -10
  10. package/docs/GETTING_STARTED.md +5 -4
  11. package/docs/HOW_TO_ADD_AN_OP.md +180 -0
  12. package/docs/INGRESS-ROUTER.md +3 -2
  13. package/docs/INGRESS.md +6 -3
  14. package/docs/NATIVE-ROUTE.md +28 -4
  15. package/docs/REPO_MAP.md +60 -30
  16. package/docs/adr/0001-two-wire-formats.md +1 -1
  17. package/docs/ai/LOCAL_DEV.md +81 -0
  18. package/docs/ai/TREE.md +1070 -0
  19. package/package.json +24 -23
  20. package/src/ingress/decode/result-base.ts +10 -2
  21. package/src/ingress/handlers.ts +45 -290
  22. package/src/ingress/headers/security.ts +41 -0
  23. package/src/ingress/index.ts +12 -148
  24. package/src/ingress/native-route.ts +182 -0
  25. package/src/ingress/packing/gather-raw-headers.ts +26 -2
  26. package/src/ingress/packing/route-wire.ts +197 -0
  27. package/src/ingress/packing/select-headers.ts +8 -1
  28. package/src/ingress/path-matcher.ts +115 -0
  29. package/src/ingress/response/baked-response.ts +302 -0
  30. package/src/ingress/router.ts +42 -2
  31. package/src/ingress/routes/common.ts +9 -1
  32. package/src/ingress/routes/json-write.ts +21 -18
  33. package/src/ingress/routes/native.ts +130 -0
  34. package/src/ingress/server-node.ts +1 -1
  35. package/src/ingress/server.ts +38 -99
  36. package/src/ingress/sync.ts +151 -0
  37. package/src/ingress/types.ts +9 -0
  38. package/src/native/ffi/build/codecs.ts +736 -0
  39. package/src/native/ffi/build/compress.ts +248 -0
  40. package/src/native/ffi/build/instances.ts +598 -0
  41. package/src/native/ffi/build/parse.ts +539 -0
  42. package/src/native/ffi/build/util.ts +140 -0
  43. package/src/native/ffi/build.ts +94 -0
  44. package/src/native/ffi/selftest.ts +11 -718
  45. package/src/native/ffi/types.ts +25 -0
  46. package/src/native/ffi.ts +36 -1320
  47. package/src/native/types/instances.ts +5 -0
  48. package/src/rust-ffi/packed.ts +38 -0
  49. package/src/rust-ffi/scalar/factories.ts +4 -0
package/docs/INGRESS.md CHANGED
@@ -83,7 +83,7 @@ provides ready-made route handlers, response builders, and a `Bun.serve`
83
83
  builder — no need to hand-build responses, header templates, or error bodies.
84
84
 
85
85
  All of these are exported from the package root and are **re-exported from the
86
- benchmark server** (`bench/servers/ingress-server.ts`), which itself is built
86
+ benchmark server** (`bench/http/servers/ingress-server.ts`), which itself is built
87
87
  entirely from them:
88
88
 
89
89
  ```ts
@@ -106,7 +106,7 @@ createIngressServer({
106
106
  });
107
107
  ```
108
108
 
109
- > **Wire format** (a contract — `bench/load.ts` depends on it):
109
+ > **Wire format** (a contract — `bench/http/load.ts` depends on it):
110
110
  > - Success (with `emitMetadataJson: true` — the bench enables it): the body is
111
111
  > Rust-generated `{"ok":true,"requestId":"...",...}`. With the DEFAULT
112
112
  > `emitMetadataJson: false` the success body is **empty** (lean reads) — serve
@@ -138,6 +138,9 @@ native options as `createIngressFast` (`trustProxy`, `https`,
138
138
  | `onResponse` | `(req, result, status, requestId) => void` | — | Hook invoked after a `Response` is produced (metrics/logging) |
139
139
  | `onError` | `(req, requestId, error) => void` | — | Hook invoked when the native pipeline fails (silent 500s are otherwise invisible) |
140
140
  | `structuredLog` | `boolean` | `false` | Emit one JSON line per request/error (gated by `CASTRUM_LOG_LEVEL`) |
141
+ | `logger` | `StructuredLogger` | — | Custom structured logger (custom stream/extra fields); implies `structuredLog: true` |
142
+ | `maxInFlight` | `number` | `0` (unlimited) | Hard cap on concurrently borrowed zero-copy output buffers (`BufferPool.maxInFlight`); exceeding it fails requests with 500 instead of allocating unbounded temporaries |
143
+ | `zeroCopyTimeoutMs` | `number` | `0` (disabled) | Abandonment guard (ms) for zero-copy responses: unread/cancelled bodies release their pooled buffer and close the stream |
141
144
 
142
145
  Returns an `OptimizedIngressHandler`:
143
146
 
@@ -288,7 +291,7 @@ rendering).
288
291
  > (`createIngressServer`) and Node (`createIngressServerNode`). Raw handlers
289
292
  > are served for GET only and skip the ingress pipeline / OPTIONS preflight.
290
293
  > See `examples/basic-server.ts` and the bench ingress server
291
- > (`bench/servers/ingress-server.ts`), which wire `/metrics` + `/healthz`
294
+ > (`bench/http/servers/ingress-server.ts`), which wire `/metrics` + `/healthz`
292
295
  > `/readyz` `/livez` end-to-end.
293
296
 
294
297
  **Tracing** — W3C trace-context helpers: `parseTraceParent(header)`,
@@ -39,13 +39,37 @@ first-failure-wins in stage order.
39
39
  - `castrum_route_compile` / `castrum_route_run` / `castrum_route_destroy`
40
40
  (C-ABI, needed-size convention, panic-guarded, immutable `&self` run).
41
41
  - napi `Route` class (Node / fallback path).
42
+ - **Public castrum surface (`createNativeRoute`, `nativeRouteHandler`)**:
43
+ the stack is now a first-class castrum API, not just the ignex wire.
44
+ - `createNativeRoute(plan)` (`src/ingress/native-route.ts`) compiles a
45
+ route-wire v3 descriptor once and runs each frame in ONE native call —
46
+ `run(query, cookie, body)` / `runFrame(frame)` return the decoded verdict
47
+ (flags + errorCode + query/cookie pairs); `destroy()` frees the handle.
48
+ The pure wire helpers (`encodeRouteDescriptor`, `packRouteFrame`,
49
+ `decodeRouteResult`, layout constants) live in
50
+ `src/ingress/packing/route-wire.ts`.
51
+ - `nativeRouteHandler(plan, responder, opts)` (`src/ingress/routes/native.ts`)
52
+ wraps a compiled route as a `RouteHandler`: extracts the query substring +
53
+ Cookie header, runs the tiny frame, rejects 400/422 on verdict failure,
54
+ and hands the decoded snapshot to the responder for the 2xx.
55
+ - `createIngressRouter`'s `native` route spec wires it into a server (see
56
+ `INGRESS-ROUTER.md`). Measured: ~580ns cheaper per request than the
57
+ full-pipeline responder on a parseQuery+parseCookies route, and **+34%
58
+ RPS at the HTTP level** on the bench server's `/api/native` vs `/api/users`
59
+ (server-bound config, 2000 connections).
60
+ - Trade-off (deliberate): the native stack does NOT do CORS, rate limiting,
61
+ security headers, IP trust, or the castrum metadata envelope — routes that
62
+ need those must use the full pipeline. The lean path is for routes where
63
+ the framework owns the response body and only needs parse + verdict.
42
64
 
43
65
  ## Tests / parity
44
66
 
45
- - `rust/ingress/native_route.rs` unit tests + `rust/ffi.rs` C-ABI tests.
46
- - `test/unit/ingress/native-route.test.ts`.
47
- - `scripts/verify-native-route.ts` + ignex's `route-wire.test.ts` /
48
- `packages/native/test/route.test.ts`.
67
+ - `rust/ingress/native_route.rs` unit tests + `rust/ffi/` C-ABI tests.
68
+ - `test/unit/ingress/native-route.test.ts` (wire round-trip + lenient parity).
69
+ - `test/unit/ingress/native-route-public.test.ts` (public surface + router
70
+ `native` kind).
71
+ - External parity: ignex's `route-wire.test.ts` /
72
+ `packages/native/test/route.test.ts` (in the `@ignex/native` repo).
49
73
 
50
74
  > **Naming note (flux → ignex)**: the external consumer was historically called
51
75
  > `@flux/native` (castrum v0.8.0, guarded by `test/compat/flux-contract.test.ts`).
package/docs/REPO_MAP.md CHANGED
@@ -14,14 +14,14 @@ agent editing this repo, [`AGENTS.md`](../AGENTS.md) is the agent-facing map.
14
14
 
15
15
  - A **Rust cdylib** (`castrum.<platform>-<arch>.node`, built with napi-rs,
16
16
  ALSO exporting `extern "C"` symbols for Bun's `bun:ffi` — see
17
- `rust/ffi.rs`) provides the performance-critical primitives (parsers,
17
+ `rust/ffi/`) provides the performance-critical primitives (parsers,
18
18
  crypto, hashing, JSON, compression, and the HTTP **ingress pipeline**).
19
19
  Under Bun, `bun:ffi` is the PRIMARY transport; NAPI is the fallback
20
20
  (Node, `CASTRUM_FFI_MODE=napi`, or a failed ffi self-test).
21
21
  - **TypeScript** (`src/`) provides the ergonomic public API: the flat `rust.*`
22
22
  FFI client and the ingress layer.
23
23
  - It ships benchmarks: a **CPU benchmark** (`bench.ts` → `src/bench`) and an
24
- **HTTP benchmark** (`bench/run-bench.ts` + `bench/servers/*`).
24
+ **HTTP benchmark** (`bench/http/run-bench.ts` + `bench/http/servers/*`).
25
25
 
26
26
  Bun is the primary runtime; Node.js ≥20.3 is supported via a compiled ESM entry
27
27
  (`dist/`).
@@ -37,7 +37,7 @@ Bun is the primary runtime; Node.js ≥20.3 is supported via a compiled ESM entr
37
37
  | Build Rust addon (LOCAL max perf) | `bun run build:perf` | x86-64-v3 + AVX2 — **never for publish** |
38
38
  | Build Rust addon (debug) | `bun run build:debug` | |
39
39
  | Build compiled JS entry (Node) | `bun run build:js` | bundle + types → `dist/` (gitignored) |
40
- | TS unit tests | `bun test` | ~540 tests, `test/unit/**` |
40
+ | TS unit tests | `bun test` | ~760 tests, `test/unit/**` |
41
41
  | Rust unit tests | `bun run test:rust` | `cargo test`, ~440 tests |
42
42
  | Node smoke tests | `bun run test:node` | explicit `.mjs` paths (Node 24 rejects dir arg) |
43
43
  | Installed-tarball e2e | `bun run verify:install` | pack → install → import from `node_modules` |
@@ -45,6 +45,8 @@ Bun is the primary runtime; Node.js ≥20.3 is supported via a compiled ESM entr
45
45
  | CPU benchmark + correctness | `bun run check` | == `bun bench.ts`; writes `bench/results/cpu/` |
46
46
  | HTTP bench (all servers) | `bun run bench:http` | |
47
47
  | HTTP smoke (fast sanity) | `bun run bench:http:smoke` | **the wire-format guard** (CI-gated) |
48
+ | FFI transport benches | `bun run bench:ffi` / `bench:ffi:load` / `bench:ffi:public` / `bench:ffi:workers` / `bench:margin` | `bench/ffi/` |
49
+ | Ingress cost benches | `bun run bench:ingress-cost` / `bench:ingress-cost:post` / `bench:router` | `bench/cost/` |
48
50
  | Startup bench | `bun run bench:startup` | import + first-call timing |
49
51
  | JS lint (Biome) | `bun run lint` / `lint:fix` | `bunx biome check [--write] .` |
50
52
  | JS format (Biome) | `bun run format` | `bunx biome format --write .` |
@@ -71,9 +73,13 @@ src/
71
73
  types.ts NativeAddon module-surface interface (mirrors index.d.ts).
72
74
  types/instances.ts Per-class INSTANCE types (Ingress, SchemaValidator, JwtSigner, ...) + result types.
73
75
  loader.ts Addon path resolution (multi-root) + getAddon()/lazyAddon().
74
- ffi.ts bun:ffi C-ABI bindings (Bun-only): lazy bind + bind-time self-test,
75
- CASTRUM_FFI_MODE gating, castrum_ingress_layout blob for constants.ts.
76
- ffi/ ffi/types.ts (BunFFI interface) + constants.ts + selftest.ts (bind-time self-test).
76
+ ffi.ts bun:ffi C-ABI transport core (Bun-only): mode resolution, buffer/buffer_length
77
+ probe, bind() orchestration (self-test + CASTRUM_FFI_MODE gating + napi fallback).
78
+ ffi/ Decomposed bind surface: types.ts (BunFFI interface) + constants.ts +
79
+ selftest.ts (thin aggregator) + build.ts (orchestrator) +
80
+ build/{util,codecs,compress,parse,instances}.ts — per-domain method builders
81
+ EACH with its self-test* function, so a new castrum_* symbol is bound AND
82
+ self-tested in one domain file.
77
83
  index.ts Barrel.
78
84
  runtime/ THE RUNTIME ADAPTER seam (Bun/Node) — selected once at load.
79
85
  detect.ts Cached runtime detection — the ONLY place `typeof Bun` is checked.
@@ -108,21 +114,31 @@ src/
108
114
  buffer-pool.ts Generic reusable byte-buffer pool (pooled ingress output).
109
115
  response.ts pooledBodyResponse (releases the pool on body consume).
110
116
  log.ts createStructuredLogger (CASTRUM_LOG_LEVEL-gated JSON lines).
117
+ metrics.ts Zero-dep metrics registry (counters/gauges/histograms + Prometheus render).
118
+ trace.ts W3C traceparent/span-id helpers.
119
+ env.ts Centralized env-var resolution (CASTRUM_* + NAPI_RS_NATIVE_LIBRARY_PATH).
120
+ proven.ts Baked benchmark-proven op selection (PROVEN_SELECTION — pure data).
121
+ loader/ Higher-order-function data loader (HFC) over the curated op set.
122
+ index.ts Dispatch core + factory (scalar/bulk dispatch, coalescing, LRU cache).
123
+ ops.ts Op registry (LOADER_OPS) + type tower; state.ts / cost.ts / batch.ts / types.ts.
111
124
  ingress/ The HTTP ingress pipeline (two paths — see §4.2).
112
125
  fast.ts PATH 1: createIngressFast (packed-input via handleRequestPacked).
113
126
  handlers.ts PATH 2: createIngressHandler (JS-packs the frame via IngressInputPacker
114
- + gatherRawHeadersPacked → handleRequestPacked).
115
- index.ts Barrel + createIngress / createIngressSync convenience factories.
127
+ + gatherRawHeadersPacked → handleRequestPacked); response builders
128
+ extracted to response/baked-response.ts.
129
+ index.ts Pure public barrel (all re-exports).
130
+ sync.ts createIngressSync / createIngress convenience factories (path 1 wrapper).
116
131
  shared.ts JS constants + buildHeaderPlan + assertSyncCallback (shared by both paths).
117
132
  constants.ts Binary-layout constants read from Rust at runtime (SINGLE SOURCE).
118
133
  status.ts / errors.ts / options.ts / types.ts / body.ts / context.ts
119
134
  packing/ header-packing.ts, input-packer.ts, gather-raw-headers.ts
120
- headers/ cors.ts, hsts.ts, fast-templates.ts, baked-templates.ts
135
+ headers/ cors.ts, hsts.ts, security.ts (baked security merge), fast-templates.ts, baked-templates.ts
121
136
  decode/ fast-result.ts, baked-result.ts (TWO decoders — see §4.2)
122
- response/ terminal.ts, error-bodies.ts
123
- routes/ read/head/json-write/echo/fallback factories + common.ts
137
+ response/ terminal.ts, error-bodies.ts, baked-response.ts (pre-baked response builders)
138
+ routes/ read/head/json-write/echo/fallback factories + common.ts + responder.ts
124
139
  server.ts createIngressServer (Bun.serve) + buildRouteHandlers + gracefulShutdown.
125
140
  server-node.ts createIngressServerNode (node:http adapter, same route handlers).
141
+ path-matcher.ts buildPathMatcher + PathMatch + safeDecode (shared by server-node + router).
126
142
  integration/ Framework integration (wraps path 2 — createIngressHandler).
127
143
  pipeline.ts createPipeline (framework-agnostic request stage for Hono/Elysia/Bun.serve).
128
144
  websocket.ts createWebSocketUpgrade (RFC 6455 101 handshake + subprotocol).
@@ -135,22 +151,33 @@ src/
135
151
  ```
136
152
  rust/ ONE cdylib crate (Cargo [lib] → lib.rs).
137
153
  lib.rs Declaration hub + module map + crate docs.
138
- util/ Shared infrastructure: bytes, packed, batch (+batch_core), threadpool, validation.
154
+ selection.rs Rust-side op selection metadata (mirrors src/selection.json).
155
+ util/ Shared infrastructure: bytes, packed, batch/ (api.rs + core.rs + tests.rs),
156
+ threadpool, validation.
139
157
  http/ HTTP wire formats & parsing: headers, method, http/cookie/query parsers,
140
- form, media_type, url_codec, url_join, etag, accept, mime_lookup, multipart.
141
- crypto/ Auth & hashing: hmac_sha256, cookie_sign, csrf, jwt, aead, argon2,
142
- base64, hashing (fnv/crc32/xxh3), random_token.
143
- json/ JSON & schema: json_ops, json_ser, json_patch_ops, json_schema (napi),
158
+ form, media_type, url_codec, url_join, http_date, etag, accept,
159
+ mime_lookup, multipart.
160
+ crypto/ Auth & hashing: hmac_sha256, cookie_sign, csrf, jwt/ (token.rs + api.rs +
161
+ tests.rs), aead, argon2, base64, ed25519, hashing (fnv/crc32/xxh3),
162
+ random_token.
163
+ json/ JSON & schema: json_ops, json_ser, json/patch/ (pointer.rs + ops.rs +
164
+ engine.rs + api.rs + tests.rs), json_schema (napi),
144
165
  fast_schema/ (zero-DOM engine — pure, no napi).
145
166
  payload/ Output & streaming: compress, sse, ws_frames, websocket, template.
146
- ingress/ THE ingress pipeline: mod.rs (napi boundary), pipeline.rs (core 8-stage),
147
- options/time/packed, cors, proxy, ip_trust, rate_limit, terminal,
148
- output.rs (single numeric layout source), ingress_constants.rs (napi projection).
149
- ffi.rs #[no_mangle] extern "C" exports (79 castrum_* symbols — 71 direct + 4
167
+ ingress/ THE ingress pipeline: api.rs (Ingress/IngressInner napi class) + mod.rs
168
+ (declaration hub), pipeline.rs (core 8-stage), native_route.rs (per-route
169
+ native stack), options/time/packed, cors, proxy, ip_trust, rate_limit,
170
+ terminal, output.rs (single numeric layout source),
171
+ ingress_constants.rs (napi projection).
172
+ ffi/ #[no_mangle] extern "C" exports (85 castrum_* symbols — 77 direct + 4
150
173
  validator_c_abi! + 4 compress_to_out!; parity guarded by
151
- test/unit/features/ffi-symbol-parity.test.ts) for Bun's bun:ffi
174
+ test/unit/native/ffi-symbol-parity.test.ts) for Bun's bun:ffi
152
175
  primary transport, incl. castrum_ingress_layout (the layout blob).
153
- unit_tests.rs / test_support.rs Cross-module Rust test suites.
176
+ mod.rs (module map) + util.rs (panic_guard / HMAC cache / cstring helpers)
177
+ + per-domain wrappers (hashing/validators/crypto/jwt/http/payload/json/
178
+ rate_limit/ingress/route/probe) + tests.rs.
179
+ panic_safety.rs / test_support.rs Cross-module fuzz + shared #[cfg(test)] helpers.
180
+ proptest_suite.rs Property-based adversarial-parser tests (dev-dep proptest).
154
181
  ```
155
182
 
156
183
  ### Generated / ignored (do NOT edit)
@@ -160,7 +187,7 @@ rust/ ONE cdylib crate (Cargo [lib] → lib.rs).
160
187
  | `dist/` | Compiled ESM entry + types for Node | Built by `build:js`; gitignored |
161
188
  | `index.js` / `index.d.ts` | NAPI-RS generated loader/declarations | Regenerated by `napi build`; gitignored |
162
189
  | `castrum.<platform>-<arch>.node` | The native addon binary | Built locally; gitignored |
163
- | `bench/results/` | Bench reports (cpu/, http/) | Generated; gitignored |
190
+ | `bench/results/` | Bench reports (cpu/, per-server http/, ffi-margin/, router/, startup/, autocannon/) | Generated; gitignored |
164
191
  | `target/` | Cargo build output | gitignored |
165
192
  | `node_modules/` | Deps | gitignored |
166
193
  | `artifacts/` | CI publish staging dir | gitignored |
@@ -172,7 +199,7 @@ rust/ ONE cdylib crate (Cargo [lib] → lib.rs).
172
199
  ### 4.1 Constants never drift (single numeric source)
173
200
  `rust/ingress/output.rs` defines every ingress layout constant once → projected
174
201
  to JS two ways: `rust/ingress/ingress_constants.rs` via `#[napi] const` (NAPI)
175
- AND `rust/ffi.rs` `IngressLayout` via `castrum_ingress_layout` (C ABI, primary
202
+ AND `rust/ffi/` `IngressLayout` via `castrum_ingress_layout` (C ABI, primary
176
203
  on Bun) → `src/ingress/constants.ts` reads them at runtime (ffi blob on Bun,
177
204
  napi on Node). Drift is pinned by the Rust unit test
178
205
  `ingress_layout_c_abi_matches_output_source` and the bun:ffi bind-time
@@ -191,7 +218,7 @@ self-test. **Never hardcode a layout value** — if you change the layout, chang
191
218
  `{"ok":true,...,"requestId":...}` (success) / `{"ok":false,"error":{code,
192
219
  message}}` (errors) with `ratelimit-*` headers.
193
220
 
194
- These wire formats are a **benchmark contract** (`bench/load.ts` validates
221
+ These wire formats are a **benchmark contract** (`bench/http/load.ts` validates
195
222
  them) — they are intentionally NOT unified. The two decoders
196
223
  (`decode/fast-result.ts` vs `decode/baked-result.ts`) and the two header-template
197
224
  builders (`headers/fast-templates.ts` vs `headers/baked-templates.ts`) exist for
@@ -235,16 +262,19 @@ consumed.
235
262
 
236
263
  ## 5. Testing & benchmarks
237
264
 
238
- - **TS**: `bun test` → `test/unit/**` (`ingress/`, `shared/`, `features/`).
265
+ - **TS**: `bun test` → `test/unit/**` (`native/`, `rust-ffi/`, `contract/`,
266
+ `ingress/`, `shared/`, `integration/`) + `test/compat/` + `test/property/`.
267
+ See `test/README.md` for the intended split.
239
268
  - **Rust**: `cargo test` → per-module `#[cfg(test)] mod tests` + cross-module
240
- `rust/unit_tests.rs`.
269
+ `rust/panic_safety.rs` (fuzz) + `rust/proptest_suite.rs`.
241
270
  - **Node**: `test/integration/node-smoke.test.mjs` + `node-enterprise.test.mjs`
242
271
  via `node --test` (run with `bun run test:node`).
243
272
  - **CPU bench**: `bun run check` (correctness checks + comparisons). Sub-µs ops
244
273
  are batched (`CASTRUM_BENCH_BATCH_SIZE`). Report → `bench/results/cpu/`.
245
- - **HTTP bench**: `bench/run-bench.ts` + `bench/load.ts` (load generator that
246
- validates response SHAPE) + `bench/servers/{bun,elysia,ingress}-server.ts`.
247
- `bench:http:smoke` is the fast CI wire-format gate.
274
+ - **HTTP bench**: `bench/http/run-bench.ts` + `bench/http/load.ts` (load generator that
275
+ validates response SHAPE) + `bench/http/servers/{bun,elysia,ingress,router}-server.ts`.
276
+ `bench:http:smoke` is the fast CI wire-format gate. FFI-transport benches live in
277
+ `bench/ffi/`; per-request cost benches in `bench/cost/` (see `bench/README.md`).
248
278
 
249
279
  ---
250
280
 
@@ -20,7 +20,7 @@ They also use separate result decoders (`FastIngressResult` vs
20
20
 
21
21
  ## Decision
22
22
 
23
- **Do not unify them.** The HTTP benchmark load generator (`bench/load.ts`)
23
+ **Do not unify them.** The HTTP benchmark load generator (`bench/http/load.ts`)
24
24
  requires `ok === true` + `requestId: string` on success and `error.code` /
25
25
  `error.message` on errors — i.e. path 2's format. The two formats are a
26
26
  **benchmark contract**: the benchmark server uses path 2, and changing its
@@ -0,0 +1,81 @@
1
+ # Local Development with the Core Projects — `bun link`
2
+
3
+ > **Scope**: maintainers and AI agents working across the IgnEX core stack.
4
+ > Application developers consume published versions from npm and do **not**
5
+ > need this file.
6
+
7
+ ## Why
8
+
9
+ The IgnEX core packages live side-by-side, one directory back from this repo,
10
+ in `/home/adeel/poc/`. When a change in this repo must be tested against the
11
+ local source of a core project (or a core change must be tested against this
12
+ repo), use `bun link` so the consumer resolves the local directory instead of
13
+ the npm registry. This is the supported Bun ≥ 1.4 (Rust-based runtime)
14
+ local-development mechanism
15
+ ([`bun link` docs](https://bun.com/docs/cli/link),
16
+ [bun.com/blog/bun-v1.4](https://bun.com/blog/bun-v1.4)).
17
+
18
+ This workflow is **only for maintainers and AI agents working with the core
19
+ projects**. CI and release pipelines always resolve from the registry; local
20
+ links are a development-only convenience.
21
+
22
+ ## Core packages (one directory back)
23
+
24
+ | Repo (`/home/adeel/poc/`) | Package(s) | `bun link` name |
25
+ | --- | --- | --- |
26
+ | `ignus` — ignex monorepo, workspaces `packages/*` | `@ignex/core`, `@ignex/cli`, `@ignex/compiler`, `@ignex/native`, `@ignex/shared`, `@ignex/mcp`, `@ignex/app`, `create-ignex` | run `bun link` inside each package dir |
27
+ | `bun-rust-runtime-bench` | `castrum` (Rust addon `castrum.<platform>-<arch>.node`) | `castrum` |
28
+ | `ignex-mongodb` | `@ignex/ninox` | `@ignex/ninox` |
29
+ | `ignex-nova` | `@ignex/nova` | `@ignex/nova` |
30
+
31
+ Known cross-repo edges (verify with `grep` in `package.json` before assuming):
32
+
33
+ - `@ignex/native` (in `ignus/packages/native`) depends on `castrum` — link
34
+ `castrum` there when changing the addon wire (`createNativeRoute`).
35
+ - `@ignex/core` (in `ignus/packages/core`) optionally depends on
36
+ `@ignex/nova`; `@ignex/app` depends on `@ignex/core`, `@ignex/cli`, and
37
+ `@ignex/ninox`.
38
+
39
+ ## How to link
40
+
41
+ ```bash
42
+ # 1. Register the core package (once per machine, from the core repo):
43
+ cd /home/adeel/poc/ignex-mongodb
44
+ bun link # → Success! Registered "@ignex/ninox"
45
+
46
+ # 2. Link it into the consumer project:
47
+ cd /home/adeel/poc/ignex-app
48
+ bun link @ignex/ninox # symlinks node_modules/@ignex/ninox → ../ignex-mongodb
49
+ bun link @ignex/ninox --save # also writes "link:@ignex/ninox" into package.json deps
50
+ ```
51
+
52
+ - `bun link` (no args) in a package directory registers that package globally
53
+ for this user.
54
+ - `bun link <name>` in a consumer creates a symlink in the consumer's
55
+ `node_modules` pointing at the registered directory. `--save` additionally
56
+ records `"<name>": "link:<name>"` in `package.json` dependencies.
57
+ - Unregister a package: `bun unlink` from the core repo dir.
58
+ - Return to registry versions: remove the `link:` entry (or `bun unlink
59
+ <name>` in the consumer) and `bun install`.
60
+
61
+ ## Rust-core caveats (castrum & @ignex/nova)
62
+
63
+ Both `castrum` and `@ignex/nova` ship a Rust cdylib. After changing Rust
64
+ source, rebuild the addon **before** linking or using:
65
+
66
+ - `castrum`: `bun run build` (release `napi build`) or `bun run build:debug`.
67
+ Under Bun the addon is also called through `bun:ffi` from the same cdylib.
68
+ - `@ignex/nova`: `bun run build:rust`
69
+ (`cargo build --release --manifest-path rust/Cargo.toml`), or rely on
70
+ `prepack`/`prebuild` to stage `prebuilds/<platform>-<arch>/`.
71
+
72
+ A stale `.node`/`.so` silently serves old behavior — rebuild, then re-test.
73
+ `@ignex/nova` additionally fails its bind-time self-test on schema/wire-version
74
+ mismatch (`IGNEX_FFI_PATH` can point at a specific build); `castrum` falls back
75
+ to the napi transport when the `bun:ffi` self-test fails.
76
+
77
+ ## Never publish from a linked tree
78
+
79
+ Publishing a consumer whose dependencies are `link:` entries ships symlinks,
80
+ not packages. Releases always run against registry versions (`prepublishOnly` /
81
+ CI re-verify with a clean install). Keep `bun link` strictly local.