castrum 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +153 -630
  2. package/castrum.linux-x64-gnu.node +0 -0
  3. package/dist/index.d.ts +1653 -1817
  4. package/dist/index.js +6172 -4294
  5. package/docs/API.md +337 -0
  6. package/docs/ARCHITECTURE.md +111 -17
  7. package/docs/BENCHMARKS.md +117 -49
  8. package/docs/CASE_STUDY.md +243 -0
  9. package/docs/ENVIRONMENT.md +24 -7
  10. package/docs/FFI_BUN_GUIDE.md +335 -0
  11. package/docs/GETTING_STARTED.md +18 -34
  12. package/docs/HOW_TO_ADD_AN_OP.md +180 -0
  13. package/docs/INGRESS-ROUTER.md +82 -0
  14. package/docs/INGRESS.md +11 -7
  15. package/docs/NATIVE-ROUTE.md +77 -0
  16. package/docs/REPO_MAP.md +103 -53
  17. package/docs/adr/0001-two-wire-formats.md +1 -1
  18. package/docs/adr/0003-bun-builtins-delegation.md +6 -5
  19. package/docs/ai/LOCAL_DEV.md +81 -0
  20. package/docs/ai/TREE.md +1070 -0
  21. package/docs/bun-builtins-decision-matrix.md +25 -18
  22. package/index.ts +9 -16
  23. package/package.json +41 -28
  24. package/src/ingress/body.ts +167 -27
  25. package/src/ingress/decode/result-base.ts +10 -2
  26. package/src/ingress/handlers.ts +116 -285
  27. package/src/ingress/headers/security.ts +41 -0
  28. package/src/ingress/health.ts +1 -0
  29. package/src/ingress/index.ts +45 -169
  30. package/src/ingress/native-route.ts +182 -0
  31. package/src/ingress/packing/gather-raw-headers.ts +110 -22
  32. package/src/ingress/packing/header-packing.ts +5 -5
  33. package/src/ingress/packing/input-packer.ts +17 -3
  34. package/src/ingress/packing/route-wire.ts +197 -0
  35. package/src/ingress/packing/select-headers.ts +20 -7
  36. package/src/ingress/path-matcher.ts +115 -0
  37. package/src/ingress/response/baked-response.ts +302 -0
  38. package/src/ingress/router.ts +277 -0
  39. package/src/ingress/routes/common.ts +50 -1
  40. package/src/ingress/routes/fallback.ts +2 -2
  41. package/src/ingress/routes/head.ts +3 -13
  42. package/src/ingress/routes/index.ts +5 -4
  43. package/src/ingress/routes/json-write.ts +26 -47
  44. package/src/ingress/routes/native.ts +130 -0
  45. package/src/ingress/routes/options.ts +2 -12
  46. package/src/ingress/routes/read.ts +3 -13
  47. package/src/ingress/routes/responder.ts +159 -0
  48. package/src/ingress/server-node.ts +7 -7
  49. package/src/ingress/server.ts +83 -112
  50. package/src/ingress/sync.ts +151 -0
  51. package/src/ingress/types.ts +44 -1
  52. package/src/integration/pipeline.ts +3 -0
  53. package/src/integration/streaming.ts +2 -3
  54. package/src/integration/websocket.ts +4 -3
  55. package/src/loader/batch.ts +4 -0
  56. package/src/loader/cost.ts +1 -0
  57. package/src/loader/index.ts +43 -228
  58. package/src/loader/state.ts +84 -0
  59. package/src/loader/types.ts +145 -0
  60. package/src/native/ffi/build/codecs.ts +736 -0
  61. package/src/native/ffi/build/compress.ts +248 -0
  62. package/src/native/ffi/build/instances.ts +598 -0
  63. package/src/native/ffi/build/parse.ts +539 -0
  64. package/src/native/ffi/build/util.ts +140 -0
  65. package/src/native/ffi/build.ts +94 -0
  66. package/src/native/ffi/constants.ts +58 -0
  67. package/src/native/ffi/selftest.ts +22 -0
  68. package/src/native/ffi/types.ts +499 -0
  69. package/src/native/ffi.ts +307 -1357
  70. package/src/native/loader.ts +52 -6
  71. package/src/native/types/instances.ts +279 -0
  72. package/src/native/types.ts +41 -238
  73. package/src/runtime/builtins.ts +131 -0
  74. package/src/runtime/codec.ts +147 -0
  75. package/src/runtime/detect.ts +36 -0
  76. package/src/runtime/index.ts +28 -0
  77. package/src/runtime/native.ts +35 -0
  78. package/src/runtime/server.ts +31 -0
  79. package/src/runtime/transport.ts +64 -0
  80. package/src/runtime/types.ts +173 -0
  81. package/src/runtime/uuid.ts +45 -0
  82. package/src/runtime/websocket.ts +30 -0
  83. package/src/rust-ffi/addon.ts +1 -1
  84. package/src/rust-ffi/batch/build.ts +155 -0
  85. package/src/rust-ffi/batch/index.ts +8 -0
  86. package/src/rust-ffi/batch/types.ts +92 -0
  87. package/src/rust-ffi/client.ts +5 -6
  88. package/src/rust-ffi/context.ts +39 -10
  89. package/src/rust-ffi/index.ts +28 -17
  90. package/src/rust-ffi/into.ts +21 -0
  91. package/src/rust-ffi/options.ts +1 -1
  92. package/src/rust-ffi/packed.ts +40 -2
  93. package/src/rust-ffi/proven.ts +16 -30
  94. package/src/rust-ffi/scalar/crypto.ts +188 -107
  95. package/src/rust-ffi/scalar/factories.ts +257 -27
  96. package/src/rust-ffi/scalar/hashing.ts +43 -41
  97. package/src/rust-ffi/scalar/http.ts +208 -77
  98. package/src/rust-ffi/scalar/index.ts +3 -3
  99. package/src/rust-ffi/scalar/interface.ts +88 -213
  100. package/src/rust-ffi/scalar/json-packed.ts +175 -0
  101. package/src/rust-ffi/scalar/json.ts +28 -12
  102. package/src/rust-ffi/scalar/payload.ts +159 -42
  103. package/src/rust-ffi/text.ts +31 -5
  104. package/src/selection.ts +27 -31
  105. package/src/shared/adaptive.ts +1 -2
  106. package/src/shared/buffer-pool.ts +72 -17
  107. package/src/shared/bytes.ts +75 -6
  108. package/src/shared/codec.ts +78 -0
  109. package/src/shared/env.ts +4 -4
  110. package/src/shared/log.ts +1 -0
  111. package/src/shared/packed/index.ts +10 -0
  112. package/src/shared/packed/parsers.ts +31 -0
  113. package/src/shared/packed/schema.ts +23 -0
  114. package/src/shared/{packed.ts → packed/wire.ts} +31 -39
  115. package/src/shared/proven.ts +189 -496
  116. package/src/shared/runtime.ts +20 -25
  117. package/src/shared/uuid.ts +12 -28
  118. package/src/data/json-rows.ts +0 -33
  119. package/src/rust-ffi/batch.ts +0 -431
  120. package/src/shared/bench-classify.ts +0 -209
@@ -0,0 +1,82 @@
1
+ # `createIngressRouter` — per-route compiled ingress
2
+
3
+ `createIngressRouter` (`src/ingress/router.ts`) compiles a **route table into
4
+ dedicated native ingress instances** — one `IngressInner` per route, pruned to
5
+ exactly that route's stages. It is the "one super solution" over the existing
6
+ ingress core for servers with heterogeneous routes.
7
+
8
+ ## Why per-route compilation
9
+
10
+ The plain `createIngressHandler` (see [`INGRESS.md`](./INGRESS.md)) serves every
11
+ path from ONE global `IngressInner` configured with a **superset** of options. A
12
+ router compiles each route's `options` into its OWN handler:
13
+
14
+ - The native pipeline prunes to that route's stages (`parseCookies`,
15
+ `parseQuery`, `schema`, `cors`, `rateLimit`, `limits`, `requireJsonBody`…).
16
+ - `buildHeaderPlan` produces a **per-route header plan** — routes that need no
17
+ headers gather ZERO headers.
18
+ - All routes share the SAME wire format (the existing packed
19
+ `handle_request_packed`), the shared process-wide rate-limiter budgets, and
20
+ optional `warmOnCreate` pre-warming.
21
+
22
+ The per-route native-compile idea is ALSO exposed standalone as the native route
23
+ stack (`rust/ingress/native_route.rs` + the `castrum_route_*` / napi `Route`
24
+ surface consumed by `@ignex/native`) — see [`NATIVE-ROUTE.md`](./NATIVE-ROUTE.md).
25
+
26
+ ## API
27
+
28
+ ### `RouterRouteSpec`
29
+
30
+ Per-route spec: `options` (per-route `IngressHandlerOptions`), plus flags that
31
+ wire route factories over that route's compiled handler:
32
+
33
+ | Flag | Routes wired |
34
+ |------|--------------|
35
+ | `read` | GET + HEAD read handler |
36
+ | `write` | POST/PUT/PATCH JSON-write handler |
37
+ | `echo` | POST echo handler |
38
+ | `cookies` | GET read (cookies-style) handler |
39
+ | `delete` | DELETE read-style handler |
40
+ | `responder` | JS responder route (`methods`, `readBody`, `terminalStyle`) |
41
+ | `native` | LEAN native-stack responder route: the route-wire v3 per-route stack (`createNativeRoute`) runs ONLY the plan's stages (parseQuery/parseCookies/requireJsonBody/validateBody) in ONE native call — no CORS/rate-limit/security/IP/metadata envelope. The responder builds the 2xx from the decoded snapshot. Measured ~580ns cheaper per request than the full-pipeline responder on a parseQuery+parseCookies route, and **+34% RPS at the HTTP level** on the bench server's `/api/native` vs `/api/users` (server-bound config). |
42
+ | `raw` | Raw `Request → Response` handler served OUTSIDE the pipeline (health/metrics probes) |
43
+ | `maxBodyBytes` / `bodyTimeoutMs` | Per-route write/echo overrides |
44
+
45
+ ### `createIngressRouter(options)` → `IngressRouter`
46
+
47
+ - `routes: Record<string, RouterRouteSpec>` — the route table (required).
48
+ - `runtime?: BakedIngressRuntime` — shared hooks applied to every compiled route.
49
+ - `getIp`, `copyBody`, `terminalStyle` — shared defaults.
50
+ - `warmOnCreate?: boolean` — pre-warm every compiled route at construction.
51
+
52
+ `IngressRouter` exposes:
53
+
54
+ - `routeHandlers` — per-path compiled handlers.
55
+ - `routes` — path → method → handler map (compatible with `Bun.serve({ routes })`
56
+ and `createIngressServerNode`).
57
+ - `match(pathname)` — path matcher (`:param` / `*` dynamic routes).
58
+ - `fetch(req, srv?)` — fetch-style dispatcher.
59
+ - `prewarm()` — JIT-warm all compiled routes.
60
+
61
+ ## Example
62
+
63
+ ```ts
64
+ const router = createIngressRouter({
65
+ warmOnCreate: true,
66
+ runtime: ingressMetrics?.runtime,
67
+ routes: {
68
+ '/health': { read: true, options: { parseCookies: false, parseQuery: false } },
69
+ '/api/users': {
70
+ read: true,
71
+ write: true,
72
+ options: { parseCookies: true, parseQuery: true, schema: USER_SCHEMA_BYTES },
73
+ },
74
+ '/metrics': { raw: metricsHandler(ingressMetrics) },
75
+ },
76
+ })
77
+ const server = Bun.serve({ routes: router.routes }) // or router.fetch
78
+ ```
79
+
80
+ The benchmark server `bench/http/servers/router-server.ts` uses this API (same route
81
+ surface as `ingress-server.ts` but per-route compiled). Cost breakdown:
82
+ `bun bench/cost/router-cost.ts` (`bun run bench:router`).
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
 
@@ -219,8 +222,9 @@ interface BakedServer {
219
222
 
220
223
  ### `createIngressServerNode(options)` — Node.js adapter
221
224
 
222
- Bun remains the primary server target; `createIngressServer` is Bun-only. For
223
- Node.js consumers, `createIngressServerNode` serves the SAME pre-baked route
225
+ Bun remains the primary server target. The public `createIngressServer` is
226
+ runtime-adaptive (Bun.serve on Bun, `node:http` on Node); for pinned Node
227
+ consumers `createIngressServerNode` serves the SAME pre-baked route
224
228
  handlers over `node:http` (sharing `buildRouteHandlers`). It returns a
225
229
  `NodeIngressServer` (`BakedServer` plus an async `ready: Promise<number>` that
226
230
  resolves to the bound port — use it with `port: 0`).
@@ -287,7 +291,7 @@ rendering).
287
291
  > (`createIngressServer`) and Node (`createIngressServerNode`). Raw handlers
288
292
  > are served for GET only and skip the ingress pipeline / OPTIONS preflight.
289
293
  > See `examples/basic-server.ts` and the bench ingress server
290
- > (`bench/servers/ingress-server.ts`), which wire `/metrics` + `/healthz`
294
+ > (`bench/http/servers/ingress-server.ts`), which wire `/metrics` + `/healthz`
291
295
  > `/readyz` `/livez` end-to-end.
292
296
 
293
297
  **Tracing** — W3C trace-context helpers: `parseTraceParent(header)`,
@@ -518,6 +522,7 @@ interface SyncIngressHandler {
518
522
 
519
523
  | Code | HTTP Status | Description |
520
524
  |------|-------------|-------------|
525
+ | `ERR_CODE_REQUEST_TIMEOUT` (-1) | 408 | Body-read deadline exceeded on a write/echo route (`bodyTimeoutMs`, default 30 s) |
521
526
  | `ERR_CODE_NONE` (0) | 200 / varies | No error |
522
527
  | `ERR_CODE_CORS_PREFLIGHT` (1) | 403 / 204 | CORS preflight rejected (or allowed → 204) |
523
528
  | `ERR_CODE_RATE_LIMITED` (2) | 429 | Rate limit exceeded |
@@ -741,5 +746,4 @@ async function* tick() {
741
746
  }
742
747
  }
743
748
  return sseResponse(tick());
744
- ```
745
- }
749
+ ```
@@ -0,0 +1,77 @@
1
+ # Native route stack — the `@ignex/native` wire (route-wire v3)
2
+
3
+ `rust/ingress/native_route.rs` exposes the **per-route native stack**: a route
4
+ descriptor compiles once into a pre-baked `NativeRoute`, then each request runs
5
+ ONE native call. It is the LIVE external wire consumed by `@ignex/native`'s
6
+ `createNativeRoute` — a `castrum` consumer that compiles routes natively.
7
+
8
+ > **History**: the old `rust/route.rs` was dead external-project wire (a THIRD
9
+ > wire format, never bound in `src/native/ffi.ts`) and was removed in v0.9.0.
10
+ > `native_route.rs` implements the now-live ignex contract and supersedes it.
11
+
12
+ ## Route-wire v3
13
+
14
+ - Magic `ROUT` (0x524f5554), version 3.
15
+ - Stage tags: `parseQuery = 0 … requireJsonBody = 5`.
16
+ - Result layout: `[flags u32][errorCode u32]` + optional query/cookie pair
17
+ sections.
18
+
19
+ Descriptor/stage/part tags + result layout must match `route-wire.ts` EXACTLY —
20
+ `ROUTE_DESC_VERSION` bumps on any wire change (a mismatched compiler/addon must
21
+ be a hard reject, never a silent misparse).
22
+
23
+ ## Parse semantics (LENIENT)
24
+
25
+ Byte-parity with ignex's JS `queryPairs` / `cookiePairs`: malformed `%ZZ` /
26
+ invalid-UTF-8 `%FF` pass through raw, `+` → space, `%2B` → `+`, cookies trim +
27
+ DQUOTE-unwrap the VALUE but not the name, no cookie URL-decoding. Do NOT reuse
28
+ the strict scalar `query_parser` for this wire.
29
+
30
+ ## Validation
31
+
32
+ The stack validates the BODY only (via `IngressSchema`); a non-body schema in
33
+ the descriptor is an unsupported feature → fail compile so the caller falls
34
+ back to JS. `requireJsonBody` → 400; `validateBody` schema fail → 422;
35
+ first-failure-wins in stage order.
36
+
37
+ ## Surfaces
38
+
39
+ - `castrum_route_compile` / `castrum_route_run` / `castrum_route_destroy`
40
+ (C-ABI, needed-size convention, panic-guarded, immutable `&self` run).
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.
64
+
65
+ ## Tests / parity
66
+
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).
73
+
74
+ > **Naming note (flux → ignex)**: the external consumer was historically called
75
+ > `@flux/native` (castrum v0.8.0, guarded by `test/compat/flux-contract.test.ts`).
76
+ > The current live wire is `@ignex/native`. Both names appear in changelogs;
77
+ > `@ignex/native` is the active contract.
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
- FFI client, the `proven` performance surface, and the ingress layer.
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,16 +37,16 @@ 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` |
44
44
  | Typecheck | `bun run typecheck` | `bunx tsc --noEmit` (only `index.ts`, `bench.ts`, `src/`) |
45
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
46
  | HTTP bench (all servers) | `bun run bench:http` | |
49
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/` |
50
50
  | Startup bench | `bun run bench:startup` | import + first-call timing |
51
51
  | JS lint (Biome) | `bun run lint` / `lint:fix` | `bunx biome check [--write] .` |
52
52
  | JS format (Biome) | `bun run format` | `bunx biome format --write .` |
@@ -62,7 +62,7 @@ Bun is the primary runtime; Node.js ≥20.3 is supported via a compiled ESM entr
62
62
  ### Source (what you edit)
63
63
 
64
64
  ```
65
- index.ts Package entry (Bun). Re-exports src/rust-ffi, proven, shared helpers, src/ingress.
65
+ index.ts Package entry (Bun). Re-exports src/rust-ffi, shared helpers, src/ingress.
66
66
  bench.ts CPU benchmark entry point → src/bench/run.ts.
67
67
 
68
68
  examples/ Runnable sample app (basic-server.ts + README) — see also README Quick Start.
@@ -70,43 +70,75 @@ examples/ Runnable sample app (basic-server.ts + README) — see
70
70
 
71
71
  src/
72
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).
73
+ types.ts NativeAddon module-surface interface (mirrors index.d.ts).
74
+ types/instances.ts Per-class INSTANCE types (Ingress, SchemaValidator, JwtSigner, ...) + result types.
74
75
  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.
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.
84
+ runtime/ THE RUNTIME ADAPTER seam (Bun/Node) — selected once at load.
85
+ detect.ts Cached runtime detection — the ONLY place `typeof Bun` is checked.
86
+ types.ts RuntimeAdapter contract (codec/uuid/env/builtins/transport/server/websocket).
87
+ codec.ts Runtime-native UTF-8 codec (Bun transfer vs TextEncoder/Decoder).
88
+ uuid.ts uuidv7 (Bun.randomUUIDv7 vs crypto.randomUUID).
89
+ builtins.ts BUN_WINS delegation registry (single source; `has(op)`).
90
+ transport.ts bun:ffi-first `resolve(op)` + ffi/napi surfaces (lazy bind).
91
+ native.ts FFI-facing seam (codec/uuid/env/builtins/transport) — imported by rust-ffi.
92
+ server.ts createIngressServer: Bun.serve on Bun, node:http on Node.
93
+ websocket.ts 101 upgrade on Bun; clear Bun-only error on Node.
94
+ index.ts FULL adapter `runtime` (native + server + websocket) — for the public API.
78
95
  rust-ffi/ The flat `rust.*` FFI client (TS wrappers over the addon).
79
96
  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).
97
+ scalar/ / text.ts / packed.ts namespaces (buildScalar, buildText, ...).
98
+ batch/ Array-of-bytes batch namespace: types.ts (RustBatch interface) + build.ts (impl) + index.ts.
99
+ context.ts Per-instance state (caches, rayon-pool bookkeeping, runtime seam).
82
100
  options.ts RustOptions + rayon-thread resolution + coercion helpers.
83
101
  addon.ts The single shared lazy addon proxy.
84
- proven.ts `proven` client — the FULL rust.* surface, annotated (same object as `rust`).
85
102
  index.ts Barrel (exports rust, createRust, types).
86
103
  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, ...).
104
+ runtime.ts Facade over src/runtime/detect.ts (isBun/isNode — cached).
105
+ codec.ts Facade over src/runtime/codec.ts.
106
+ uuid.ts Facade over src/runtime/uuid.ts.
107
+ bytes.ts encoder/decoder (Uint8Array|string union normalization) + toPlainBuffer.
108
+ packed/ Packed wire-format helpers (split from the old packed.ts monolith).
109
+ wire.ts PURE byte encode/decode (u32/bitset/i64/byte-results/multipart, pairs, pack-scratch) — no addon.
110
+ schema.ts SchemaValidator alias + schemaValidateBatch/Count.
111
+ parsers.ts lazy-addon string parsers (parseQueryString/parseCookieHeader/parseFormBody).
112
+ index.ts Barrel (existing `../shared/packed` imports unchanged).
90
113
  request-id.ts Zero-alloc request-id generator (both ingress paths).
91
114
  buffer-pool.ts Generic reusable byte-buffer pool (pooled ingress output).
92
115
  response.ts pooledBodyResponse (releases the pool on body consume).
93
116
  log.ts createStructuredLogger (CASTRUM_LOG_LEVEL-gated JSON lines).
94
- proven.ts PROVEN_SURFACE registry (PURE DATA — no addon imports).
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.
95
124
  ingress/ The HTTP ingress pipeline (two paths — see §4.2).
96
125
  fast.ts PATH 1: createIngressFast (packed-input via handleRequestPacked).
97
126
  handlers.ts PATH 2: createIngressHandler (JS-packs the frame via IngressInputPacker
98
- + gatherRawHeadersPacked → handleRequestPacked).
99
- 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).
100
131
  shared.ts JS constants + buildHeaderPlan + assertSyncCallback (shared by both paths).
101
132
  constants.ts Binary-layout constants read from Rust at runtime (SINGLE SOURCE).
102
133
  status.ts / errors.ts / options.ts / types.ts / body.ts / context.ts
103
134
  packing/ header-packing.ts, input-packer.ts, gather-raw-headers.ts
104
- 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
105
136
  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
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
108
139
  server.ts createIngressServer (Bun.serve) + buildRouteHandlers + gracefulShutdown.
109
140
  server-node.ts createIngressServerNode (node:http adapter, same route handlers).
141
+ path-matcher.ts buildPathMatcher + PathMatch + safeDecode (shared by server-node + router).
110
142
  integration/ Framework integration (wraps path 2 — createIngressHandler).
111
143
  pipeline.ts createPipeline (framework-agnostic request stage for Hono/Elysia/Bun.serve).
112
144
  websocket.ts createWebSocketUpgrade (RFC 6455 101 handshake + subprotocol).
@@ -119,20 +151,33 @@ src/
119
151
  ```
120
152
  rust/ ONE cdylib crate (Cargo [lib] → lib.rs).
121
153
  lib.rs Declaration hub + module map + crate docs.
122
- 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.
123
157
  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),
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),
128
165
  fast_schema/ (zero-DOM engine — pure, no napi).
129
166
  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
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
173
+ validator_c_abi! + 4 compress_to_out!; parity guarded by
174
+ test/unit/native/ffi-symbol-parity.test.ts) for Bun's bun:ffi
134
175
  primary transport, incl. castrum_ingress_layout (the layout blob).
135
- 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).
136
181
  ```
137
182
 
138
183
  ### Generated / ignored (do NOT edit)
@@ -142,7 +187,7 @@ rust/ ONE cdylib crate (Cargo [lib] → lib.rs).
142
187
  | `dist/` | Compiled ESM entry + types for Node | Built by `build:js`; gitignored |
143
188
  | `index.js` / `index.d.ts` | NAPI-RS generated loader/declarations | Regenerated by `napi build`; gitignored |
144
189
  | `castrum.<platform>-<arch>.node` | The native addon binary | Built locally; gitignored |
145
- | `bench/results/` | Bench reports (cpu/, http/) | Generated; gitignored |
190
+ | `bench/results/` | Bench reports (cpu/, per-server http/, ffi-margin/, router/, startup/, autocannon/) | Generated; gitignored |
146
191
  | `target/` | Cargo build output | gitignored |
147
192
  | `node_modules/` | Deps | gitignored |
148
193
  | `artifacts/` | CI publish staging dir | gitignored |
@@ -154,7 +199,7 @@ rust/ ONE cdylib crate (Cargo [lib] → lib.rs).
154
199
  ### 4.1 Constants never drift (single numeric source)
155
200
  `rust/ingress/output.rs` defines every ingress layout constant once → projected
156
201
  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
202
+ AND `rust/ffi/` `IngressLayout` via `castrum_ingress_layout` (C ABI, primary
158
203
  on Bun) → `src/ingress/constants.ts` reads them at runtime (ffi blob on Bun,
159
204
  napi on Node). Drift is pinned by the Rust unit test
160
205
  `ingress_layout_c_abi_matches_output_source` and the bun:ffi bind-time
@@ -173,7 +218,7 @@ self-test. **Never hardcode a layout value** — if you change the layout, chang
173
218
  `{"ok":true,...,"requestId":...}` (success) / `{"ok":false,"error":{code,
174
219
  message}}` (errors) with `ratelimit-*` headers.
175
220
 
176
- These wire formats are a **benchmark contract** (`bench/load.ts` validates
221
+ These wire formats are a **benchmark contract** (`bench/http/load.ts` validates
177
222
  them) — they are intentionally NOT unified. The two decoders
178
223
  (`decode/fast-result.ts` vs `decode/baked-result.ts`) and the two header-template
179
224
  builders (`headers/fast-templates.ts` vs `headers/baked-templates.ts`) exist for
@@ -185,22 +230,22 @@ this reason. **Shared between both paths**: `generateRequestId`
185
230
  - `src/native/loader.ts` resolves the `.node` from multiple roots so it works
186
231
  from both the source layout (`src/…`) and the bundled layout (`dist/…`).
187
232
  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`).
233
+ - Runtime detection lives **only** in `src/runtime/detect.ts` (resolved once at
234
+ load; `src/shared/runtime.ts` is a thin facade). The public
235
+ `createIngressServer` (via the adapter's `server`) picks `Bun.serve` on Bun
236
+ and the `node:http` backend on Node; `createIngressServerNode` stays for
237
+ pinned Node users (same route handlers via `buildRouteHandlers`).
191
238
  - ⚠️ `src/ingress/constants.ts` is the **one module that dlopens the addon at
192
239
  import time** (its constants are needed as soon as ingress is imported).
193
240
  Everything else is lazy.
194
241
 
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.
242
+ ### 4.4 The runtime adapter
243
+ `src/runtime/` is the single seam for every Bun-vs-Node and transport decision
244
+ (detection, codec, uuid, env, the BUN_WINS built-in delegation, the bun:ffi-vs-
245
+ napi transport, the ingress server backend, websocket) — selected ONCE at
246
+ module load. The FFI layer imports the seam (`src/runtime/native.ts`); the
247
+ public API imports the full `runtime` (`src/runtime/index.ts`). No
248
+ `isBun()` / `getBunFFI()` branches in builders.
204
249
 
205
250
  ### 4.5 Pooled output buffers (path 2)
206
251
  `createIngressHandler` owns a `BufferPool` (`src/shared/buffer-pool.ts`) sized
@@ -217,16 +262,19 @@ consumed.
217
262
 
218
263
  ## 5. Testing & benchmarks
219
264
 
220
- - **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.
221
268
  - **Rust**: `cargo test` → per-module `#[cfg(test)] mod tests` + cross-module
222
- `rust/unit_tests.rs`.
269
+ `rust/panic_safety.rs` (fuzz) + `rust/proptest_suite.rs`.
223
270
  - **Node**: `test/integration/node-smoke.test.mjs` + `node-enterprise.test.mjs`
224
271
  via `node --test` (run with `bun run test:node`).
225
272
  - **CPU bench**: `bun run check` (correctness checks + comparisons). Sub-µs ops
226
273
  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.
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`).
230
278
 
231
279
  ---
232
280
 
@@ -251,9 +299,11 @@ consumed.
251
299
  | Doc | Purpose |
252
300
  |-----|---------|
253
301
  | [`GETTING_STARTED.md`](./GETTING_STARTED.md) | Beginner tutorial (intern-friendly) |
302
+ | [`API.md`](./API.md) | The complete public API reference |
303
+ | [`CASE_STUDY.md`](./CASE_STUDY.md) | Data-driven case study: how castrum was built and measured |
254
304
  | [`ARCHITECTURE.md`](./ARCHITECTURE.md) | Deep-dive: modules, data flow, output layout, memory, concurrency |
255
305
  | [`INGRESS.md`](./INGRESS.md) | The pre-baked ingress API + route factories + servers |
256
306
  | [`BENCHMARKS.md`](./BENCHMARKS.md) | Benchmark scenarios, report format, how to run |
257
307
  | [`ENVIRONMENT.md`](./ENVIRONMENT.md) | All `CASTRUM_*` env vars (+ legacy aliases) |
258
308
  | [`AGENTS.md`](../AGENTS.md) | AI-agent editing guidance (commands, constraints, gotchas) |
259
- | `README.md` | Public-facing quick start + API reference |
309
+ | `README.md` | Public-facing overview + quick start |
@@ -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
@@ -17,7 +17,7 @@ Every overlapping op is **benchmarked against its Bun built-in** before it is
17
17
  built, kept, or delegated. Results live in
18
18
  `docs/bun-builtins-decision-matrix.md`. Rules:
19
19
 
20
- 1. **Delegate to Bun** at the `@flux/native` wrapper layer where Bun wins
20
+ 1. **Delegate to Bun** at the selection/delegation layer where Bun wins
21
21
  clearly: non-crypto hashing (`Bun.hash.crc32` ~3x, `xxHash3` ~4x),
22
22
  gzip (`Bun.gzipSync`/`gunzipSync`), `Bun.randomUUIDv7` (~2x), HMAC
23
23
  (`Bun.CryptoHasher` ~1.2x).
@@ -31,10 +31,11 @@ built, kept, or delegated. Results live in
31
31
 
32
32
  - New `rust.*` primitives ship with a Bun-built-in baseline + decision-matrix
33
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.
34
+ - The Bun/JS baselines are recorded honestly in the decision matrix
35
+ (e.g. `xxh3` is measured `Bun.hash.xxHash3` ~4x faster — the public
36
+ `rust.xxh3` delegates to it under Bun via the runtime adapter's `builtins`).
37
+ - Diagnostic benchmark tasks use `diag:` names so they are never mistaken for
38
+ shipped-op `native:`/`rust:` comparisons.
38
39
 
39
40
  ## See also
40
41
 
@@ -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.