updates

ober

3ac91567a4ec0f5bcef9fc7577a6931dbd9948ef

diff --git a/data/cookbooks.sexp b/data/cookbooks.sexp
index 3f442a2..8153ba7 100644
--- a/data/cookbooks.sexp
+++ b/data/cookbooks.sexp
@@ -4383,10 +4383,10 @@
      "Render an exception condition into a string instead of swallowing it"))
  (("code"
     .
-    ";; The MCP server speaks newline-delimited JSON-RPC over stdio. You can drive it from\n;; bash without going through Claude Code MCP shim - useful for scripting, debugging,\n;; or when the shim is hanging on a particular call shape.\n\n;; Read tool exposed in hybrid mode:\n;;   echo {jsonrpc:2.0,id:1,method:tools/call,params:{name:jerboa_howto,arguments:{query:hash-table}}} | jmcp\n\n;; Writer tools are critical now, so they are exposed directly in hybrid mode:\n;;   echo {jsonrpc:2.0,id:1,method:tools/call,params:{name:jerboa_howto_add,arguments:{id:foo,title:t,code:c}}} | jmcp\n\n;; Compact dispatcher route, useful in mini mode or for lazy tool discovery:\n;;   echo {jsonrpc:2.0,id:1,method:tools/call,params:{name:jerboa,arguments:{tool:search,args:{query:benchmark}}}} | jmcp\n\n;; Pattern for batch saves with status check (bash):\n;;   call() { resp=$(echo $1 | timeout 15 /path/to/jmcp | head -1); echo $resp | python3 -c\n;;     import sys, json; r = json.loads(sys.stdin.read());\n;;     print(OK if isError not in str(r) else ERR, r[result][content][0][text][:120]); }") ("id" . "jmcp-direct-jsonrpc-pipe") ("imports")
+    ";; The MCP server speaks newline-delimited JSON-RPC over stdio. You can drive it from\n;; bash without going through Claude Code MCP shim - useful for scripting, debugging,\n;; or when the shim is hanging on a particular call shape.\n\n;; Set JERBOA_MCP_REPO when running jmcp outside the Jerboa repo; otherwise\n;; repo-root defaults to the current directory and tools may read embedded data.\n;;   export JERBOA_MCP_REPO=$HOME/mine/jerboa\n\n;; Read tool exposed in hybrid mode:\n;;   echo {jsonrpc:2.0,id:1,method:tools/call,params:{name:jerboa_howto,arguments:{query:hash-table}}} | jmcp\n\n;; Writer tools are critical now, so they are exposed directly in hybrid mode:\n;;   echo {jsonrpc:2.0,id:1,method:tools/call,params:{name:jerboa_howto_add,arguments:{id:foo,title:t,code:c}}} | jmcp\n\n;; Compact dispatcher route, useful in mini mode or for lazy tool discovery:\n;;   echo {jsonrpc:2.0,id:1,method:tools/call,params:{name:jerboa,arguments:{tool:search,args:{query:benchmark}}}} | jmcp\n\n;; Pattern for batch saves with status check (bash):\n;;   call() { resp=$(echo $1 | timeout 15 /path/to/jmcp | head -1); echo $resp | python3 -c\n;;     import sys, json; r = json.loads(sys.stdin.read());\n;;     print(OK if isError not in str(r) else ERR, r[result][content][0][text][:120]); }") ("id" . "jmcp-direct-jsonrpc-pipe") ("imports")
    ("notes"
      .
-     "Default mode is hybrid (env JERBOA_MCP_MODE). Mode full exposes every tool top-level; mode mini exposes only the compact jerboa dispatcher. Writer tools such as jerboa_howto_add and jerboa_suggest_feature are now critical/direct in hybrid mode. Dispatcher uses short names (search, catalog, describe, howto_add). Each invocation respawns the server unless you keep a process attached - fine for one-shots. Server reads one request, writes one response, then EOF terminates.")
+     "Default mode is hybrid (env JERBOA_MCP_MODE). Mode full exposes every tool top-level; mode mini exposes only the compact jerboa dispatcher. Writer tools such as jerboa_howto_add and jerboa_suggest_feature are now critical/direct in hybrid mode. Dispatcher uses short names (search, catalog, describe, howto_add). Each invocation respawns the server unless you keep a process attached - fine for one-shots. Server reads one request, writes one response, then EOF terminates. Set JERBOA_MCP_REPO when invoking jmcp from another repository so data-path points at the live data/ directory instead of falling back to embedded data.")
    ("tags" "jmcp" "mcp" "json-rpc" "dispatcher" "pipe"
      "scripting")
    ("title"
@@ -4614,6 +4614,150 @@
      "Useful for vendored R6RS/Chez libraries when older consumers still import or exclude a legacy export name. Exporting `(rename (canonical legacy))` lets `(except (module) legacy)` remain valid without defining a second binding in the library body.")
    ("tags" "library" "export" "rename" "compatibility" "alias"
      "chezscheme")
+   ("title" . "Export compatibility aliases with R6RS rename"))
+ (("code"
+    .
+    ";; Single binding — list, vector, or any Iterable\n(for (x '(1 2 3))\n  (displayln x))\n\n;; in-range: (end), (start end), or (start end step)\n(for (x (in-range 5))         ; 0 1 2 3 4\n  (displayln x))\n(for (x (in-range 4 7))       ; 4 5 6\n  (displayln x))\n(for (x (in-range 5 10 2))    ; 5 7 9\n  (displayln x))\n(for (x (in-range 7 4 -1))    ; 7 6 5 (auto-decrements)\n  (displayln x))\n\n;; Parallel iteration (stops at shortest)\n(for ((x '(1 2 3)) (y '#(a b c d)))\n  (displayln x \" \" y))  ; 1 a / 2 b / 3 c\n\n;; in-range-inclusive (includes end)\n(for (x (in-range-inclusive 3)) ; 0 1 2 3\n  (displayln x))\n\n;; Iterate over vectors explicitly\n(for (x (in-vector '#(10 20 30)))\n  (displayln x))\n\n;; Coroutine iterator (generator pattern)\n(def (my-gen n)\n  (lambda (yield)\n    (let lp ((k 0))\n      (when (< k n) (yield k) (lp (1+ k))))))\n(for (x (my-gen 3))\n  (displayln x))  ; 0 1 2") ("id" . "v019-iter-for-basics") ("imports" ":std/iter")
+   ("notes"
+     .
+     "v0.19 rewrites std/iter from scratch around a new Iterator interface protocol. Key changes from v0.18: (1) in-iota is removed — use in-range; (2) the iter-end sentinel is gone — iterators now signal end with #!eof; (3) the iterator struct is replaced by the Iterator interface (next! method); (4) in-range auto-detects direction when no step given: counts up if end>start, down if end<start; (5) lists, vectors, and procedures are all Iterable.")
+   ("tags" "iter" "for" "iteration" "in-range" "in-list"
+     "v0.19")
+   ("title" . "v0.19: Basic for loop with iterators"))
+ (("code"
+    .
+    ";; for/collect — build a list (skips #!void results)\n(for/collect (x (in-range 5))\n  (* x x))  ; => (0 1 4 9 16)\n\n;; Multi-binding for/collect (double parens for multiple)\n(for/collect ((x '(1 2 3)) (y '#(a b c d)))\n  (cons x y))  ; => ((1 . a) (2 . b) (3 . c))\n\n;; for/fold — accumulate a value\n(for/fold (r []) ((x '(1 2 3)))\n  (cons x r))  ; => (3 2 1)\n\n(for/fold (sum 0) (x (in-range 5))\n  (+ sum x))  ; => 10\n\n;; with/when filter\n(for/collect (x (in-range 5) when (odd? x))\n  x)  ; => (1 3)\n\n;; when/unless in multi-binding (after bindings)\n(for/collect ((x (in-range 5)) (y '(a b c d e)) when (odd? x))\n  (cons x y))  ; => ((1 . b) (3 . d))\n\n(for/fold (r []) ((x (in-range 5)) unless (odd? x))\n  (cons x r))  ; => (4 2 0)\n\n;; Per-binding filter (inside the binding form)\n(for/collect ((x '(1 2 3 4) when (odd? x)))\n  x)  ; => (1 3)") ("id" . "v019-iter-collect-fold") ("imports" ":std/iter")
+   ("notes"
+     .
+     "Two filter placement styles in v0.19: (1) after the binding form as `(for/collect (x expr when pred) body)` — applies to single-binding; (2) after all bindings as `(for/collect ((x e1) (y e2) when pred) body)` — filters the combined iteration. for/fold accumulator init `iv` can be any value; body should return the next accumulator value (returning #!void skips the update). for/collect skips #!void returns.")
+   ("tags" "iter" "for/collect" "for/fold" "comprehension"
+     "collect" "v0.19")
+   ("title"
+     .
+     "v0.19: for/collect and for/fold comprehensions"))
+ (("code"
+    .
+    ";; The Iterator interface has one method: (next!) => :t\n;; Returns #!eof when exhausted (replaces v0.18 iter-end sentinel)\n(interface Iterator\n  (next!) => :t)\n\n(interface Iterable\n  (iter) => Iterator)\n\n;; Implement a custom countdown iterator\n(defstruct countdown\n  ((n :- :fixnum))\n  final: #t)\n\n(implement Iterator countdown\n  (next!\n   (lambda (self)\n     (if (fx> self.n 0)\n       (let (v self.n)\n         (set! self.n (fx- self.n 1))\n         v)\n       #!eof))))\n\n;; Wrap in Iterator interface for use with for\n(def (in-countdown n)\n  => Iterator\n  (Iterator (countdown n)))\n\n;; Use it\n(for (x (in-countdown 3))\n  (displayln x))  ; 3 2 1\n\n(for/collect (x (in-countdown 5)) x)  ; => (5 4 3 2 1)\n\n;; Make a type Iterable by implementing Iterable interface\n(defstruct my-range ((lo :- :fixnum) (hi :- :fixnum)))\n\n(implement Iterable my-range\n  (iter (lambda (self)\n          (in-range self.lo self.hi))))\n\n;; Now for/for/collect work directly on my-range\n(for (x (iter (my-range 2 5)))\n  (displayln x))  ; 2 3 4") ("id" . "v019-iter-custom") ("imports" ":std/iter")
+   ("notes"
+     .
+     "The Iterator interface replaces v0.18's iterator struct. Key differences: (1) return #!eof (not iter-end) to signal end; (2) implement via (implement Iterator <struct> (next! ...)); (3) wrap in Iterator interface with (Iterator instance); (4) procedures (lambdas) are automatically Iterable as coroutines — (lambda (yield) ...) can be passed directly to for. The iter function coerces any Iterable to an Iterator.")
+   ("tags" "iter" "Iterator" "Iterable" "interface" "custom"
+     "v0.19")
+   ("title" . "v0.19: Implement custom Iterator/Iterable"))
+ (("code"
+    .
+    ";; in-hash yields (key . value) cons pairs\n(let (ht (hash (a 1) (b 2) (c 3)))\n  (for (pair (in-hash ht))\n    (displayln (car pair) \" => \" (cdr pair))))\n\n;; Destructure the pair in a match pattern\n(let (ht (hash (x 10) (y 20)))\n  (for ([k . v] (in-hash ht))  ; pattern binding\n    (displayln k \" -> \" v)))\n\n;; Keys only\n(let (ht (hash (a 1) (b 2)))\n  (for/collect (k (in-hash-keys ht)) k))\n\n;; Values only\n(let (ht (hash (a 1) (b 2)))\n  (for/collect (v (in-hash-values ht)) v))\n\n;; HashTable is Iterable — default iteration yields (key . value)\n(let (ht (hash (p 7) (q 8)))\n  (for (kv ht)\n    (displayln kv)))") ("id" . "v019-iter-hash") ("imports" ":std/iter")
+   ("notes"
+     .
+     "in-hash returns (key . value) cons cells. Hash tables implement Iterable directly (iterating yields pairs). Use pattern binding `([k . v] expr)` to destructure inline. Iteration order is unspecified for hash tables. Works with any HashTable interface instance.")
+   ("tags" "iter" "hash" "in-hash" "in-hash-keys"
+     "in-hash-values" "v0.19")
+   ("title" . "v0.19: Iterate over hash tables"))
+ (("code"
+    .
+    ";; format returns a string; compile-time optimized when fmt is a literal\n(format \"%a\" 42)           ; => \"42\"   (%a = display)\n(format \"%s\" '(1 2))       ; => \"(1 2)\" (%s = write)\n(format \"Hello %a!\" \"world\") ; => \"Hello world!\"\n\n;; Integer specifiers\n(format \"%d\" 255)          ; => \"255\"   decimal\n(format \"%x\" #x1234ab)     ; => \"#x1234ab\" hex with prefix\n(format \"%X\" #x1234ab)     ; => \"1234AB\"  hex uppercase, no prefix\n(format \"%b\" #b1001)       ; => \"#b1001\" binary with prefix\n(format \"%B\" #b1001)       ; => \"1001\"   binary, no prefix\n(format \"%o\" #o755)        ; => \"#o755\"  octal with prefix\n(format \"%O\" #o755)        ; => \"755\"    octal, no prefix\n\n;; Flonum specifiers\n(format \"%f\" 3.14159)      ; => \"3.14159\"\n(format \"%.2f\" 3.14159)    ; => \"3.14\"\n(format \"%8.3f\" 3.14)      ; => \"   3.140\" (width 8)\n(format \"%-8.3f\" 3.14)     ; => \"3.140   \" (left-align)\n(format \"%e\" 12345.6)      ; => \"1.23456e4\"\n(format \"%g\" 0.001)        ; => \"1e-3\"\n\n;; Multiple args\n(format \"[%d, %d]\" 1 2)   ; => \"[1, 2]\"\n(format \"%a/%a\" 'foo 'bar) ; => \"foo/bar\"\n\n;; fprintf — write to a port or BufferedWriter\n(fprintf (current-output-port) \"count: %d\\n\" 42)\n\n;; printf — shorthand for fprintf to current-output-port\n(printf \"x=%d y=%d\\n\" 10 20)\n\n;; eprintf — write to current-error-port\n(eprintf \"error: %a\\n\" 'oops)\n\n;; Runtime format (non-literal string)\n(let (fmt \"%a %a\")\n  (format fmt 'hello 'world))  ; => \"hello world\"") ("id" . "v019-format-basics") ("imports" ":std/format")
+   ("notes"
+     .
+     "v0.19 replaces the old :std/format (which used Common Lisp ~a/~s directives) with a new compile-time-optimized format module. Key differences: (1) uses C-style % directives instead of CL-style ~; (2) when the format string is a literal, format/fprintf expand at compile-time to direct writer calls (zero allocation for the format string); (3) lowercase specifiers (%x, %b, %o) include radix prefix (#x, #b, #o), uppercase (%X, %B, %O) omit it; (4) %a = display, %s = write, %q = debug; (5) \\ escape sequences work in the format string (\\n, \\t, etc.). Width/precision/flags (#,-,+,space,0) are supported as in C printf.")
+   ("tags" "format" "printf" "fprintf" "string" "output"
+     "v0.19")
+   ("title"
+     .
+     "v0.19: format/fprintf/printf string formatting"))
+ (("code"
+    .
+    ";; make-formater pre-parses a format string into a reusable Formater object\n(def coord-fmt (make-formater \"(%d, %d)\"))\n\n;; apply-formater returns a string\n(apply-formater coord-fmt 3 7)    ; => \"(3, 7)\"\n(apply-formater coord-fmt 10 20)  ; => \"(10, 20)\"\n\n;; apply-formater-to-output writes to a port/BufferedWriter\n(apply-formater-to-output coord-fmt (current-output-port) 1 2)  ; prints \"(1, 2)\"\n\n;; Useful in hot paths: parse fmt once, apply many times\n(def log-line-fmt (make-formater \"%a [%a] %a\"))\n\n(def (emit-log level source msg)\n  (apply-formater log-line-fmt level source msg))") ("id" . "v019-format-formater") ("imports" ":std/format")
+   ("notes"
+     .
+     "Use Formater when the same format string is applied many times (e.g., in a logging hot path). make-formater parses the format string once into a Formater struct; apply-formater applies it without re-parsing. Compare to (format \"%d\" x) which, when given a literal string, is compile-time-expanded anyway — Formater is only worth the effort for runtime-constructed or very frequently-applied format strings.")
+   ("tags" "format" "Formater" "make-formater" "apply-formater"
+     "v0.19")
+   ("title"
+     .
+     "v0.19: Reusable Formater objects for repeated formatting"))
+ (("code"
+    .
+    ";; Start the system logger (required before any log output)\n;; defaults: sinks=[console-log-sink], level=INFO\n(start-system-logger!)\n\n;; Optional: set level and sinks\n(start-system-logger!\n  level: DEBUG\n  sinks: [console-log-sink])\n\n;; Default process-level loggers (from :std/log)\n;; Levels: CRITICAL=-1  ERROR=0  WARN=1  INFO=2  DEBUG=3  VERBOSE=4\n(log.info \"server started\" 'port 8080)\n(log.warn \"retrying\" 'attempt 3)\n(log.errorf \"failed after %d attempts\" 5)\n(log.debug \"connection from\" 'addr \"127.0.0.1\")\n\n;; deflogger — create a named sub-logger for a subsystem\n;; The deflogger form creates .info, .warn, .error, .debug etc. macros\n;; prefixed with the logger identifier\n(deflogger myapp name: 'myapp)\n\n;; Now use myapp.info, myapp.warn, etc.\n(myapp.info \"starting\" 'workers 4)\n(myapp.warn \"high load\" 'queue-depth 1000)\n(myapp.errorf \"DB timeout after %dms\" 5000)\n(myapp.debug \"request\" 'method 'GET 'path \"/api/v1/users\")\n\n;; Available level macros for each logger:\n;; .critical .criticalf .error .errorf .warn .warnf\n;; .info .infof .debug .debugf .verbose .verbosef\n;; (f-suffix = format string; plain = display args)\n\n;; Structured key-value args follow the message\n(myapp.info \"user login\" 'user-id 42 'ip \"10.0.0.1\")\n\n;; Control per-subsystem log level\n(user-log-level 'myapp)            ; get current level\n(set-user-log-level! 'myapp DEBUG) ; set level") ("id" . "v019-log-deflogger") ("imports" ":std/log")
+   ("notes"
+     .
+     "v0.19 replaces the old :std/logger actor-based logging with a new :std/log package. Key points: (1) must call start-system-logger! before logging anything or output is dropped; (2) deflogger creates a per-subsystem logger bound to a symbol name — the generated macros check level before formatting, so disabled-level log calls have near-zero cost; (3) the f-suffix variants (.infof, .debugf) take a format string (\"%a %d\" style) followed by args; (4) plain variants (.info, .debug) take a message followed by alternating key/value pairs (alist-style); (5) log levels are integers — CRITICAL(-1) through VERBOSE(4); lower numbers are more severe.")
+   ("tags" "log" "deflogger" "logging" "logger"
+     "start-system-logger" "v0.19")
+   ("title" . "v0.19: Structured logging with deflogger"))
+ (("code"
+    .
+    ";; let-hash rebinds %%ref so dot-prefixed identifiers look up the hash\n(let (ht (hash (name \"Alice\") (age 30) (role 'admin)))\n  (let-hash ht\n    (displayln .name)    ; => Alice    (hash-ref ht 'name)\n    (displayln .?age)    ; => 30       (hash-get ht 'age)  weak — #f if missing\n    (displayln .?missing)))  ; => #f\n\n;; Dot syntax variants inside let-hash:\n;;   .x   -> (hash-ref ht 'x)   strong: raises error if missing\n;;   .?x  -> (hash-get ht 'x)   weak: returns #f if missing\n;;   .$x  -> (hash-get ht \"x\")  string key, weak\n;;   ..x  -> escape: look up 'x in outer scope (not the hash)\n\n;; Nested hashes\n(let (ht (hash (user (hash (name \"Bob\") (id 7)))))\n  (let-hash ht\n    (let-hash .user\n      (displayln .name \" #\" .id))))\n\n;; hash=? for structural equality\n(def a (hash (x 1) (y 2)))\n(def b (hash (x 1) (y 2)))\n(hash=? a b)           ; => #t\n(equal-hash? a b)      ; => #t  (also checks hash-table?)") ("id" . "v019-hash-let-hash") ("imports" ":std/hash")
+   ("notes"
+     .
+     "let-hash is now in :std/hash (v0.19) — it was previously in :std/sugar (now removed). The .? prefix is the safe nil-returning variant. The .$ prefix looks up by string key. Use ..x to \"escape\" dot lookup and reference a binding from the outer scope. hash=? does deep key-value equality; equal-hash? additionally checks that both are hash-table? first.")
+   ("tags" "hash" "let-hash" "destructuring" "hash-ref"
+     "std/hash" "v0.19")
+   ("title" . "v0.19: let-hash for hash table destructuring"))
+ (("code"
+    .
+    ";; DELETED in v0.19 (no replacement needed or merged elsewhere)\n;;\n;; :std/sugar          -> removed; let-hash moved to :std/hash\n;;                        awhen/chain/is/with-id still in core sugar\n;; :std/contract       -> removed (was already a compat shim in v0.18)\n;; :std/misc/list      -> functionality split across std/list/*\n;; :std/misc/hash      -> :std/hash\n;; :std/misc/barrier   -> :std/sync/barrier\n;; :std/misc/channel   -> :std/sync/channel\n;; :std/misc/completion -> :std/sync/completion\n;; :std/misc/deque     -> :std/struct/queue (or std/list/deque)\n;; :std/misc/ports     -> :std/io/*\n;; :std/misc/threads   -> removed (use :gerbil/runtime/thread)\n;; :std/misc/timeout   -> :std/time/timeout\n;; :std/misc/walist    -> :std/list/walist\n;; :std/logger         -> :std/log (completely rewritten)\n;; :std/sort           -> removed (use :std/srfi/132 or sort from srfi-32)\n;; :std/srfi/*         -> all SRFIs removed from stdlib\n;; :std/actor-v13/*    -> removed\n;; :std/actor-v18/*    -> removed (replaced by new actor system)\n;; :std/format         -> :std/format/* (new, C-printf style)\n;; :std/generic        -> removed (use interface system)\n;; :std/io/strio/*     -> :std/io/bio/* (buffered I/O rewrite)\n\n;; NEW in v0.19\n;; :std/iter           -> completely rewritten (Iterator interface protocol)\n;; :std/format         -> new C-printf style format module\n;; :std/log            -> new structured logging (replaces :std/logger)\n;; :std/cache          -> object/global cache management\n;; :std/serde          -> serialization/deserialization framework\n;; :std/ffi            -> FFI macrology (replaces :std/foreign)\n;; :std/hash           -> hash utilities (let-hash, hash=?)\n;; :std/sync/spinlock  -> new spinlock primitive\n;; :std/sync/barrier   -> moved from :std/misc/barrier\n;; :std/sync/channel   -> moved from :std/misc/channel\n;; :std/sync/completion -> moved from :std/misc/completion\n;; :std/sync/rwlock    -> new reader-writer lock\n;; :std/list/walist    -> moved from :std/misc/walist\n;; :std/time/timeout   -> moved from :std/misc/timeout\n;; :std/time/time      -> time utilities\n;; :std/time/format    -> time formatting\n;; :std/net/address/*  -> net address utilities\n\n;; Quick migration pattern for let-hash\n;; v0.18: (import :std/sugar)\n;; v0.19: (import :std/hash)\n(import :std/hash)  ; let-hash is here now") ("id" . "v019-stdlib-migration") ("imports")
+   ("notes"
+     .
+     "v0.19 is a major stdlib reorganization. The :std/misc/* namespace is almost entirely removed — its contents moved to more specific top-level packages. The SRFI collection (:std/srfi/*) is removed. The generic dispatch system (:std/generic) is removed in favor of the interface system. The old iterator struct-based :std/iter is rewritten around interfaces. :std/sugar is removed — most sugar is now in gerbil/core or specific modules.")
+   ("tags" "v0.19" "migration" "std/misc" "sugar" "contract"
+     "stdlib")
+   ("title"
+     .
+     "v0.19: stdlib reorganization — what moved where"))
+ (("code"
+    .
+    ";; for iterates bindings IN PARALLEL (stops at shortest)\n(for ((x '(1 2 3)) (y '(a b c)))\n  (displayln x \" \" y))\n;; 1 a\n;; 2 b\n;; 3 c\n\n;; for* iterates bindings NESTED (cross-product, inner resets each outer step)\n(for* ((x '(1 2 3)) (y '(a b)))\n  (displayln x \" \" y))\n;; 1 a\n;; 1 b\n;; 2 a\n;; 2 b\n;; 3 a\n;; 3 b\n\n;; for* with for/collect-like pattern: use for/collect with nested for\n(for/collect (x '(1 2 3))\n  (for/collect (y '(a b))\n    (cons x y)))\n;; => (((1 . a) (1 . b)) ((2 . a) (2 . b)) ((3 . a) (3 . b)))\n\n;; for* with filter\n(for* ((x (in-range 3)) (y (in-range 3)) when (not (= x y)))\n  (displayln x \",\" y))") ("id" . "v019-iter-for-star") ("imports" ":std/iter")
+   ("notes"
+     .
+     "for = parallel iteration (like Racket's for, Python zip). for* = nested/cross-product iteration (like Racket's for*, Python nested loops). for stops at the shortest iterator; for* exhausts all combinations. When you want a flat cross-product list, use for/collect with a for* body or nest for/collect calls.")
+   ("tags" "iter" "for*" "nested" "cross-product" "iteration"
+     "v0.19")
+   ("title"
+     .
+     "v0.19: for* for nested (cross-product) iteration"))
+ (("code"
+    .
+    ";; ObjectCache pools reusable objects to avoid repeated allocation\n;; (e.g., byte buffers, connection objects, scratch vectors)\n\n;; Create a named object cache\n(def buf-cache\n  (ObjectCache\n   name:     '/my/buf-cache\n   lock:     (SpinLock)\n   objects:  []\n   size:     0\n   max-size: 32                        ; hold up to 32 buffers\n   new:      (lambda () (make-u8vector 4096 0))  ; allocate fresh\n   reset!:   (lambda (buf) (u8vector-fill! buf 0))))  ; reset before reuse\n\n;; Register with the global cache registry (optional, for flush/stats)\n(global-cache-register! buf-cache)\n\n;; Get an object (from pool, or freshly allocated if empty)\n(let (buf (object-cache-get buf-cache))\n  ;; ... use buf ...\n  ;; Return it when done\n  (object-cache-put! buf-cache buf))\n\n;; Global cache stats and flushing\n(global-cache-size)    ; total objects across all registered caches\n(global-cache-flush!)  ; release all pooled objects\n\n;; Flush a specific cache\n(object-cache-flush! buf-cache)") ("id" . "v019-cache-object")
+   ("imports" ":std/cache" ":std/iter")
+   ("notes"
+     .
+     "ObjectCache is a spin-lock protected pool. object-cache-get either pops from the pool or calls new:. object-cache-put! calls reset!: then pushes back if below max-size, otherwise discards. The global cache registry (global-cache-register!) is optional but lets you flush all caches at once (e.g., on memory pressure). SpinLock from :std/sync/spinlock is the recommended lock for object caches.")
+   ("tags" "cache" "object-cache" "pool" "ObjectCache"
+     "std/cache" "v0.19")
+   ("title"
+     .
+     "v0.19: Object cache for pooling reusable objects"))
+ (("code"
+    .
+    ";; std/ffi provides high-level macrology for C FFI (replaces :std/foreign)\n;; Requires compilation target C: (require ,(compilation-target? C))\n\n(require ,(compilation-target? C))\n(import :std/ffi)\n\n;; C-ffi-macrology: declare helper C macros (call once at top of file)\n(C-ffi-macrology)\n\n;; C-include: emit #include directives\n(C-include \"<stdio.h>\" \"<string.h>\")\n\n;; C-declare: emit raw C code\n(C-declare \"static int my_counter = 0;\")\n\n;; def-C: define a Scheme wrapper around a C expression\n;; Syntax: (def-C (proc-name (arg ~ :type) ...) => :return-type \"C code\")\n;; In the C code string, use $1, $2, ... for the (unwrapped) args\n(def-C (add-fixnums (a :- :fixnum) (b :- :fixnum)) => :fixnum\n  \"$1 + $2\")\n\n(def-C (copy-bytes (dst :- :u8vector) (src :- :u8vector) (n :- :fixnum)) => :fixnum\n  \"(memcpy($1, $2, $3), $3)\")\n\n;; Use the defined procedures normally\n(add-fixnums 3 5)    ; => 8\n\n;; Type annotations in def-C:\n;;   :fixnum   -> int, unwrapped with ___INT(), wrapped with ___FIX()\n;;   :flonum   -> double, unwrapped with ___F64UNBOX()\n;;   :u8vector -> __uint8_t*, unwrapped with ___U8VECTOR_AS()\n;; The => :return-type determines how to box the C return value") ("id" . "v019-ffi-def-c") ("imports" ":std/ffi")
+   ("notes"
+     .
+     "std/ffi is a v0.19 replacement for :std/foreign. def-C generates a Gambit ___BEGIN_C_LINKAGE block with inline C code plus the Scheme wrapper. The C code receives raw Gambit object pointers named ___ARG1, ___ARG2, ... which def-C unwraps to the correct C types per the :type annotations before passing as $1, $2, .... Use C-ffi-macrology once per compilation unit to get ___U8VECTOR_AS and ___TRAP_ERRNO helpers. The require guard ensures the form is only compiled for C-target builds.")
+   ("tags" "ffi" "C" "def-C" "C-include" "foreign" "v0.19")
+   ("title"
+     .
+     "v0.19: FFI macrology with def-C and C-include"))
+ (("code"
+    .
+    ";; support/build-check.ss\n#!chezscheme\n;;; Import-only build entrypoint: compile the same modules as main.ss,\n;;; but do not call cli-main or start the app.\n(import (chezscheme)\n        (myapp core config)\n        (myapp core agent)\n        (myapp ui cli))\n\n;; Makefile\nJERBUILD ?= jerbuild\nJH := $(shell $(JERBUILD) --jerboa-home 2>/dev/null)\nLIBDIRS := --libdirs ./lib:$(JH)/lib\n\nbuild:\n\t$(JERBUILD) compile $(LIBDIRS) support/build-check.ss\n")
+   ("id" . "jerbuild-import-only-build-check") ("imports")
+   ("notes"
+     .
+     "jerbuild compile executes the script after compiling it. If main.ss calls cli-main, a build target can accidentally start the TUI/CLI or hang. Use an import-only entrypoint that imports the same top-level modules needed to force compilation, but performs no application action.")
+   ("tags" "jerbuild" "compile" "entrypoint" "import"
+     "build-check" "cli")
+   ("title"
+     .
+     "Compile a Jerboa project without running its CLI entrypoint"))
+ (("code"
+    .
+    ";; support/sqlite-bundled/Cargo.toml\n[package]\nname = \"app-sqlite-bundled\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[lib]\ncrate-type = [\"staticlib\"]\n\n[dependencies]\nlibsqlite3-sys = { version = \"0.30\", features = [\"bundled\"] }\n\n;; support/sqlite-bundled/src/lib.rs\n#[no_mangle]\npub extern \"C\" fn app_sqlite_bundled_anchor() -> i32 {\n    unsafe { libsqlite3_sys::sqlite3_libversion_number() }\n}\n\n;; .jerbuild\n(entry \"main.ss\")\n(output \"app\")\n(ffi-symbols \"support/ffi-symbols.list\")\n(extra-sources\n  (\"vendor/jerboa-sqlite/jerboa_sqlite_shim.c\" cflags: \"-Isupport\"))\n(extra-archives\n  \"support/sqlite-bundled/target/release/libapp_sqlite_bundled.a\")\n\n;; Makefile\nsupport/sqlite-bundled/target/release/libapp_sqlite_bundled.a:\n\tcargo build --manifest-path support/sqlite-bundled/Cargo.toml --release\n\nbinary: support/sqlite-bundled/target/release/libapp_sqlite_bundled.a\n\tjerbuild build --config .jerbuild --os-libs \"-lm -ldl -lpthread -luuid -lncurses -lstdc++\"\n")
+   ("id" . "jerbuild-bundled-sqlite-shim") ("imports")
+   ("notes"
+     .
+     "This keeps user repos buildable with jerbuild plus a C compiler and Rust/Cargo, without requiring system sqlite headers or libsqlite3 packages. Put a minimal sqlite3.h in support/ if the shim only needs a small API surface, add -Isupport on the shim source, and list the bundled Rust static archive in extra-archives so sqlite symbols are available at final link.")
+   ("tags" "jerbuild" "sqlite" "ffi" "staticlib"
+     "extra-archives" "c-shim")
    ("title"
      .
-     "Export compatibility aliases with R6RS rename")))
+     "Bundle SQLite for a jerbuild binary with a Rust static archive and C shim")))
diff --git a/data/features.sexp b/data/features.sexp
index a2b15c1..6db4a2d 100644
--- a/data/features.sexp
+++ b/data/features.sexp
@@ -824,7 +824,7 @@
    ("use_case"
      .
      "Every /save-discoveries invocation. Every manual howto_add / suggest_feature / vote_feature / security_pattern_add. Anything that needs to persist discoveries back to the .sexp data files.")
-   ("votes" . 0))
+   ("votes" . 1))
  (("description"
     .
     "When `jerbuild binary` compiles a program importing (std net tls-rustls) or (std wasm sandbox), the resulting binary still needs libjerboa_native.dylib reachable at runtime via DYLD_LIBRARY_PATH/LD_LIBRARY_PATH, because those std modules do a NAMED (load-shared-object \"libjerboa_native.dylib\"). Add a flag (e.g. --static-native / --with-libjerboa-native) that links the prebuilt libjerboa_native.a via --extra-archive AND auto-emits the ffi-symbols registration header (Sforeign_symbol) for the archive's exported symbols, producing a self-contained binary needing no runtime dylib or DYLD_* env. jerbuild already supports --extra-archive plus a manual (ffi-symbols ...) mechanism; this packages the common case (the shipped libjerboa_native) behind one flag and auto-derives the symbol list (e.g. via nm) so the user need not enumerate jerboa_tls_*/jerboa_wasm_* by hand.")
@@ -982,4 +982,78 @@
    ("use_case"
      .
      "Validating large Jerboa modules after a small edit without falling back to a full make build.")
+   ("votes" . 0))
+ (("description"
+    .
+    "When running jerboa_eval, there is no way to query which Gerbil version the runtime is using. Trying (gerbil-version-string) throws 'unbound identifier'. This makes it impossible to confirm whether v0.19 features are available before testing, or to include version context in error messages.")
+   ("estimated_token_reduction"
+     .
+     "~300 tokens per session: eliminates 3-4 failed eval probes plus the bash fallback to determine version")
+   ("example_scenario"
+     .
+     "Trying to test v0.19 std/iter patterns: (for (x (in-range 5)) body) fails because the runtime is v0.18.1. There was no way to discover this without trial-and-error. A (jerboa-runtime-version) or (##gerbil-version-string) binding in eval would let the recipe-authoring workflow confirm compatibility upfront.")
+   ("id" . "jerboa-version-query") ("impact" . "medium")
+   ("tags" "eval" "version" "runtime" "gerbil" "compatibility")
+   ("title"
+     .
+     "jerboa_eval: expose Gerbil runtime version string")
+   ("use_case"
+     .
+     "Discovering which stdlib API version is available before testing new patterns; writing version-conditional recipes; diagnosing why a function is missing.")
+   ("votes" . 0))
+ (("description"
+    .
+    "When testing API patterns for a future Gerbil version (e.g., v0.19 while runtime is v0.18), jerboa_eval throws cryptic errors instead of a clear 'not available in this runtime' message. A gerbil_version parameter on jerboa_eval would let callers declare the minimum required version and get a clean skip/warning instead of a confusing exception.")
+   ("estimated_token_reduction"
+     .
+     "~400 tokens per session for version-crossing recipe work: eliminates confusing error interpretation and the bash version-probe detour")
+   ("example_scenario"
+     .
+     "Calling jerboa_eval to test (for (x (in-range 5)) body) from v0.19 std/iter while v0.18.1 is installed. The macro has different syntax in v0.18, so the error 'invalid syntax (x (in-range 5))' is confusing — it looks like the test code is wrong, not that it needs a newer runtime.")
+   ("id" . "jerboa-eval-imports-version-guard")
+   ("impact" . "medium")
+   ("tags" "eval" "version" "compatibility" "v0.19" "skip")
+   ("title"
+     .
+     "jerboa_eval: accept gerbil_version guard to skip incompatible evals")
+   ("use_case"
+     .
+     "Writing cookbook recipes for a not-yet-installed Gerbil version. Currently requires trying the call, getting an error, then manually noting 'needs v0.19'. A version guard would produce a clear message instead.")
+   ("votes" . 0))
+ (("description"
+    .
+    "When exploring unfamiliar modules (especially checking if an installed module is the Jerboa version vs. the gerbil-origin version), there's no way to know which file jerboa_module_exports is reading. This session required a bash glob to find that std/iter was at /opt/gerbil/v0.18.1-173-gb3417266/lib/std/iter.ssi before understanding why the API didn't match expectations.")
+   ("estimated_token_reduction"
+     .
+     "~500 tokens: eliminates the bash glob + read .ssi file detour needed to identify which module version is active")
+   ("example_scenario"
+     .
+     "jerboa_module_exports for std/iter returned symbols from Jerboa's own std/iter (in-list, in-vector, in-hash-pairs...) not gerbil-origin's (iterator, iter-end, in-iota...). Without knowing the source path, it was impossible to understand why.")
+   ("id" . "jerboa-module-source-path") ("impact" . "medium")
+   ("tags" "module" "exports" "source" "path" "debugging")
+   ("title"
+     .
+     "jerboa_module_exports: include source file path in output")
+   ("use_case"
+     .
+     "Understanding which installed copy of a module is being used when multiple gerbil versions or Jerboa overrides are present. Critical for v0.18 vs v0.19 disambiguation.")
+   ("votes" . 0))
+ (("description"
+    .
+    "When jmcp writer tools fail, the response can be the literal template 'Tool error: failed for ~a: ~(~a~)' instead of the tool name and exception text. This hides the actionable cause, such as invoking jmcp outside the repo without JERBOA_MCP_REPO set.")
+   ("estimated_token_reduction"
+     .
+     "~500-1500 tokens per failing MCP call; avoids source spelunking to infer the exception.")
+   ("example_scenario"
+     .
+     "Calling jerboa_howto_add from /Users/user/mine/jerboa-shell failed with 'Tool error: failed for ~a: ~(~a~)' because repo-root defaulted to the current directory and the data directory was not present. The real exception was hidden.")
+   ("id" . "fix-mcp-tool-error-formatting")
+   ("impact" . "medium")
+   ("tags" "mcp" "errors" "debugging" "format" "jmcp")
+   ("title"
+     .
+     "Render real exception details in MCP tool error responses")
+   ("use_case"
+     .
+     "Debugging failed MCP tool calls, especially cookbook/feature/security writer calls from direct JSON-RPC or the Claude MCP shim.")
    ("votes" . 0)))
diff --git a/data/security-rules.sexp b/data/security-rules.sexp
index f9e48de..9012d66 100644
--- a/data/security-rules.sexp
+++ b/data/security-rules.sexp
@@ -893,4 +893,22 @@
      .
      "\\(string=\\?\\s+[^\\)]*secret[^\\)]*\\s+\"\"\\)")
    ("scope" . "scheme")
-   ("severity" . "high")))
+   ("severity" . "high"))
+ (("id" . "ffi-c-declare-string-injection")
+   ("message"
+     .
+     "C-declare and begin-foreign accept raw C code strings. If the string is constructed at runtime (e.g., via string-append or format), an attacker controlling any input segment can inject arbitrary C declarations, define malicious macros, or corrupt the generated C file.")
+   ("pattern"
+     .
+     "C-declare\\s+[^\"]*\\(string-append|begin-foreign[^)]*\\(string-append")
+   ("scope" . "ffi-boundary")
+   ("severity" . "high"))
+ (("id" . "ffi-def-c-u8vector-length-unchecked")
+   ("message"
+     .
+     "def-C with :u8vector arguments passes the raw byte pointer to C but does NOT automatically pass the vector's length. If the C code (memcpy, memmove, etc.) uses a caller-supplied length without verifying it against the actual u8vector size, a heap buffer overflow results.")
+   ("pattern"
+     .
+     "def-C.*:u8vector.*memcpy|def-C.*:u8vector.*memmove|def-C.*:u8vector.*memset")
+   ("scope" . "ffi-boundary")
+   ("severity" . "medium")))