data: save jpkg-session discoveries (cookbook, features, security rule)

ober

e4a2489cb1ce7d9eb80707480b9b211ea52535cd

diff --git a/data/cookbooks.sexp b/data/cookbooks.sexp
index 6893980..b84a58b 100644
--- a/data/cookbooks.sexp
+++ b/data/cookbooks.sexp
@@ -4965,4 +4965,161 @@
    ("tags" "typed" "llvmir" "llvm" "variant" "match" "option")
    ("title"
      .
-     "Lower Typed Jerboa Option, variants, and match to LLVM IR")))
+     "Lower Typed Jerboa Option, variants, and match to LLVM IR"))
+ (("code"
+    .
+    "(define-syntax compile-time-token\n  (lambda (x)\n    (syntax-case x ()\n      ((k)\n       (let ((t (getenv \"JCODE_REPL_TOKEN\")))   ;; runs in the EXPANDER = at build\n         (if (and t (> (string-length t) 0))\n           (datum->syntax #'k t)                  ;; splice the string literal in\n           #'#f))))))\n\n(def *build-token* (compile-time-token))         ;; now a constant in the .so/binary") ("id" . "compile-time-env-constant") ("imports")
+   ("notes"
+     .
+     "The transformer body runs during compilation, so (getenv ...) reads the BUILD machine's environment and the result is frozen into the object code — no runtime file/env dependency. Use to embed a generated auth token, git sha, or version. datum->syntax with the macro keyword (#'k) gives the literal proper lexical context. Pair with a runtime (getenv ...) override if you want a stock binary to still be reconfigurable: (or (getenv \"X\") *build-token*). In jerbuild, set the env on the transpile/build invocation (e.g. JCODE_REPL_TOKEN=$(cat .repl-token) make). Gotcha: a plain (def *t* (getenv ...)) instead resolves at RUN time, defeating the purpose.")
+   ("tags" "compile-time" "getenv" "define-syntax" "macro"
+     "build" "embed" "secret")
+   ("title"
+     .
+     "Bake a build-time value (token/version) into a binary via a getenv macro"))
+ (("code"
+    .
+    "(def (conn->ports conn)\n  ;; conn is an opaque FFI handle (e.g. a rustls server conn). c-read/c-write\n  ;; are __collect_safe foreign-procedures: (handle u8* len) -> int.\n  (let* ((closed? #f)\n         (close! (lambda ()\n                   (unless closed? (set! closed? #t)\n                     (guard (e [#t (void)]) (c-close conn)))))\n         (bin-in (make-custom-binary-input-port \"conn-in\"\n                   (lambda (bv start count)\n                     (if closed? 0\n                       (let* ((tmp (make-bytevector count))\n                              (n (c-read conn tmp count)))\n                         (if (<= n 0) 0   ;; <=0 = EOF/error -> report EOF\n                           (begin (bytevector-copy! tmp 0 bv start n) n)))))\n                   #f #f close!))\n         (bin-out (make-custom-binary-output-port \"conn-out\"\n                    (lambda (bv start count)\n                      (if closed? count\n                        (let ((tmp (make-bytevector count)))\n                          (bytevector-copy! bv start tmp 0 count)\n                          (c-write conn tmp count) count)))\n                    #f #f close!))\n         (ip (transcoded-port bin-in\n               (make-transcoder (utf-8-codec) (eol-style none)\n                 (error-handling-mode replace))))\n         (op (transcoded-port bin-out\n               (make-transcoder (utf-8-codec) (eol-style lf)\n                 (error-handling-mode replace)))))\n    (values ip op close!)))") ("id" . "opaque-conn-to-transcoded-ports") ("imports")
+   ("notes"
+     .
+     "For a REAL fd you can use open-fd-input-port / open-fd-output-port (dup the fd so each port owns one). This custom-binary-port recipe is for handles that are NOT fds (a rustls conn, a userspace buffer). Key points: (1) the read callback must copy into the caller's bv at `start`, not assume start=0; (2) share a `closed?` flag + single close! across both ports so GC finalization and explicit close are idempotent (the port close handler is the 5th arg); (3) eol-style none on input so you read raw bytes and strip CR yourself; (4) blocking FFI calls inside should be __collect_safe or they pin the Chez TC mutex and freeze other green threads. Wrap binary ports with transcoded-port to get char I/O (read-char/read/display).")
+   ("tags" "custom-binary-port" "transcoded-port" "tls" "ffi"
+     "port" "wrap" "utf-8")
+   ("title"
+     .
+     "Wrap an opaque connection handle (rustls/FFI) as transcoded textual ports"))
+ (("code"
+    .
+    ";; WRONG — raises \"#<time-utc ...> is not a real number\":\n;; (> (file-modification-time \"a\") (file-modification-time \"b\"))\n\n;; RIGHT — use the time comparators on the time-utc records:\n(time>? (file-modification-time \"a\") (file-modification-time \"b\"))\n\n;; ...or pull out the integer seconds if you need arithmetic:\n(- (time-second (file-modification-time \"a\"))\n   (time-second (file-modification-time \"b\")))") ("id" . "file-modification-time-compare") ("imports")
+   ("notes"
+     .
+     "file-modification-time (and file-access/change-time) return a time-utc record, so the numeric comparators (>, <, -) throw \"not a real number\". Use time>?, time<?, time=? for ordering, or time-second / time-nanosecond to extract numbers. Common in build scripts checking 'is the source newer than the artifact' or 'did a sibling rebuild a shared .a after my sentinel'. Note time-nanosecond is the sub-second part only — don't compare it alone across different seconds.")
+   ("tags" "file-modification-time" "time-utc" "time>?" "mtime"
+     "compare" "staleness" "build")
+   ("title"
+     .
+     "Compare file mtimes — file-modification-time returns a time-utc record, not an int"))
+ (("code"
+    .
+    ";; defstruct ... transparent: #t  makes the record inspectable even when the\n;; field accessors are not exported. Read a field by NAME at runtime:\n(def (record-field s name)\n  (let* ((rtd   (record-rtd s))\n         (names (record-type-field-names rtd)))   ;; #(field-a field-b ...)\n    (let loop ((i 0))\n      (cond\n        ((= i (vector-length names)) (error 'record-field \"no such field\" name))\n        ((eq? (vector-ref names i) name) ((record-accessor rtd i) s))\n        (else (loop (+ i 1)))))))\n\n;; e.g. in a debug REPL with only (dbg-snapshot *dbg-state*) exported:\n;; (record-field *dbg-state* 'agent-busy?)  => #t\n;; (record-field *dbg-state* 'active-tools) => (\"task\")") ("id" . "read-transparent-record-field-reflection")
+   ("imports")
+   ("notes"
+     .
+     "Invaluable for live debugging via a debug REPL: when a module exports only an opaque state handle, record reflection reads ANY field without code changes. Requires the defstruct to be declared `transparent: #t` (jerboa) / inspectable; opaque records raise on record-rtd. record-type-field-names returns a vector of symbols in field order; record-accessor takes the 0-based index. Read-only — use record-mutator for writes (only on mutable fields).")
+   ("tags" "record-rtd" "record-accessor"
+     "record-type-field-names" "defstruct" "transparent"
+     "reflection" "debug")
+   ("title"
+     .
+     "Read an unexported transparent defstruct field by name via record reflection"))
+ (("code"
+    .
+    "(def (char-utf8-length c)\n  (let ((cp (char->integer c)))\n    (cond ((< cp #x80) 1) ((< cp #x800) 2) ((< cp #x10000) 3) (else 4))))\n\n;; Read exactly n BYTES worth of chars from a textual (utf-8) port.\n(def (port-read-n in n)\n  (let ((out (open-output-string)))\n    (let loop ((remaining n))\n      (if (<= remaining 0)\n        (get-output-string out)\n        (let ((c (read-char in)))\n          (if (eof-object? c)\n            (get-output-string out)\n            (begin (write-char c out)\n                   (loop (- remaining (char-utf8-length c))))))))))") ("id" . "read-n-bytes-from-transcoded-port") ("imports")
+   ("notes"
+     .
+     "HTTP Content-Length counts BYTES; a utf-8 text port reads CHARS. Decrementing the byte budget by char-utf8-length keeps them aligned so a multibyte char near the boundary isn't split or over-read. Requires eol-style none on the transcoder (otherwise CRLF translation distorts the byte count — do CR stripping manually in your line reader). Cleaner alternative when available: read on the underlying BINARY port (get-bytevector-n) and utf8->string after, instead of counting on a text port. The naive read-until-EOF (read-char loop to eof) HANGS forever against an HTTP/1.1 keep-alive server that never closes the socket — honor Content-Length/chunked, or send `Connection: close`.")
+   ("tags" "content-length" "read-char" "utf-8" "bytes" "http"
+     "transcoded-port" "framing")
+   ("title"
+     .
+     "Read exactly N bytes (HTTP Content-Length) from a utf-8 transcoded text port"))
+ (("code"
+    .
+    "(import (std security seccomp))\n\n;; Default-allow blocklist: deny the dangerous syscall set with EPERM, allow\n;; everything else. Robust for a process that KEEPS RUNNING (a live Chez runtime\n;; that then forks/execs): an allowlist + KILL would SIGSYS-kill it on any\n;; runtime syscall you forgot to list. Linux only; install in the forked child\n;; before exec — the filter is inherited across execve. IRREVERSIBLE.\n(when (seccomp-available?)\n  (seccomp-install! safe-blocklist))   ;; ~24 dangerous syscalls -> EPERM\n\n;; Custom blocklist: deny just ptrace + bpf, returning EPERM (errno 1).\n(seccomp-install!\n  (apply make-seccomp-blocklist (seccomp-errno 1) '(ptrace bpf)))") ("id" . "seccomp-blocklist-eperm")
+   ("imports" "(std security seccomp)")
+   ("notes"
+     .
+     "safe-blocklist denies ptrace, process_vm_readv/writev, personality, kexec_load/kexec_file_load, init_module/finit_module/delete_module, bpf, perf_event_open, userfaultfd, add_key/request_key/keyctl, pivot_root/setns, mount/umount2, swapon/swapoff, reboot, settimeofday/clock_settime — all with EPERM (won't kill the program). make-seccomp-blocklist takes (blocked-action . syscall-name-symbols); action is e.g. (seccomp-errno N) for ERRNO or seccomp-kill for KILL. Allowlist alternative: make-seccomp-filter (default-action + allowed names) but non-listed syscalls hit default-action (usually seccomp-kill) — only safe for compute-only/known workloads, not arbitrary programs. CAUTION: in a STATIC-musl binary, this module's raw (foreign-procedure \"syscall\"/\"prctl\") cannot resolve and silently no-ops (seccomp-available? -> #f); use the jsh ffi_seccomp_* C wrappers there.")
+   ("tags" "seccomp" "blocklist" "syscall filter" "sandbox"
+     "EPERM" "ptrace")
+   ("title"
+     .
+     "seccomp: default-allow blocklist (deny dangerous syscalls with EPERM)"))
+ (("code"
+    .
+    ";; WRONG — both (chezscheme) and (std text base64) export base64-encode:\n;;   (import (chezscheme) (std text base64))\n;;   => Exception: multiple definitions for base64-encode in body\n;;\n;; RIGHT — drop the Chez builtin so the (std ...) version wins:\n(library (my mod)\n  (export do-thing)\n  (import (except (chezscheme) base64-encode base64-decode)\n          (std text base64))\n  (def (do-thing bv) (base64-encode bv)))") ("id" . "chez-builtin-shadow-except")
+   ("imports"
+     "(except (chezscheme) base64-encode base64-decode)"
+     "(std text base64)")
+   ("notes"
+     .
+     "Chez ships several names that std modules also provide. If you import (chezscheme) alongside a (std ...) module and hit 'multiple definitions for NAME in body', add NAME to an (except (chezscheme) NAME ...). Seen in this codebase with base64-encode/base64-decode (std text base64) and sha256-bytevector (std crypto sha256-pure). The prelude already handles the common ones (make-hash-table, sort, printf, iota, etc.) but raw (chezscheme) imports do not. This is a compile-time error, not runtime.")
+   ("tags" "chezscheme" "except" "import-conflict" "base64"
+     "multiple-definitions" "library")
+   ("title"
+     .
+     "Import a (std ...) module that re-exports a Chez builtin: use (except (chezscheme) ...)"))
+ (("code"
+    .
+    "(import (std net allowlist))\n\n;; net-allowlist-target-matches? TARGET PATTERN — both are \"host:port\" strings.\n;; Host part: '*' matches exactly ONE label, '**' matches any depth (incl apex),\n;; a bare '*' matches any host. Port '*' matches any port.\n(net-allowlist-target-matches? \"pkg.pypi.org:443\" \"*.pypi.org:*\")   ;; => #t (one label)\n(net-allowlist-target-matches? \"pypi.org:443\"     \"*.pypi.org:*\")   ;; => #f (no subdomain)\n(net-allowlist-target-matches? \"a.b.pypi.org:443\" \"*.pypi.org:*\")   ;; => #f (two labels)\n(net-allowlist-target-matches? \"a.b.pypi.org:443\" \"**.pypi.org:*\")  ;; => #t (any depth)\n(net-allowlist-target-matches? \"pypi.org:443\"     \"**.pypi.org:*\")  ;; => #t (apex too)\n(net-allowlist-target-matches? \"pypi.org:80\"      \"pypi.org:443\")   ;; => #f (port mismatch)") ("id" . "net-allowlist-wildcard-host")
+   ("imports" "(std net allowlist)")
+   ("notes"
+     .
+     "Powers ,sb --allow-host egress rules (via (std os limits sandbox) egress-policy). Semantics: *.dom = exactly one subdomain label; **.dom = zero-or-more labels (matches the apex and any subdomain); * alone = any host. Companion net-allowlist-host-denial-reason rejects IP literals / localnet / link-local targets unless explicitly opted in (allow-ip-literals?, allow-localnet?), so a wildcard pattern can't accidentally permit raw-IP or LAN destinations.")
+   ("tags" "net" "allowlist" "wildcard" "host" "egress" "fqdn"
+     "glob")
+   ("title"
+     .
+     "Match host:port against a wildcard egress allowlist pattern"))
+ (("code"
+    .
+    "(import (std text json))\n\n(let ([h (string->json-object\n          \"{\\\"obj\\\":{\\\"a\\\":1},\\\"arr\\\":[1,2],\\\"s\\\":\\\"x\\\",\\\"b\\\":true,\\\"n\\\":null,\\\"f\\\":1.5}\")])\n  ;; objects  -> hashtable   (hashtable-ref h \"obj\" #f)\n  ;; arrays   -> LIST        (hashtable-ref h \"arr\" #f) => (1 2)\n  ;; strings  -> string\n  ;; true/false -> #t/#f\n  ;; null     -> (void)      (eq? (hashtable-ref h \"n\" 'miss) (void)) => #t\n  ;; integers -> EXACT       (exact? (hashtable-ref h \"obj\"... )) ; 1 is exact\n  ;; 1.5      -> inexact flonum\n  (list (hashtable? (hashtable-ref h \"obj\" #f))\n        (list? (hashtable-ref h \"arr\" #f))\n        (exact? (hashtable-ref h \"f\" 0))))") ("id" . "json-parse-value-mapping")
+   ("imports" "(std text json)")
+   ("notes"
+     .
+     "Non-obvious points that bite when converting JSON to your own data model: (1) JSON arrays decode to plain LISTS, not vectors. (2) JSON null decodes to (void) — test with (eq? v (void)), not #f or 'null. (3) integers are EXACT, floats are inexact, so (and (integer? v) (exact? v)) distinguishes a JSON int from a JSON float. To canonicalize into sorted alists: walk with cond on hashtable?/list?/(eq? x (void)). Object key order from hashtable-keys is unspecified — sort if you need determinism.")
+   ("tags" "json" "string->json-object" "hashtable" "null"
+     "void" "parse")
+   ("title"
+     .
+     "What (std text json) string->json-object returns: hashtable / list / (void) / exact int"))
+ (("code"
+    .
+    "(import (std security capsicum))\n\n(capsicum-available?)              ;; => #t on FreeBSD (cap_enter resolvable)\n(capsicum-in-capability-mode?)    ;; => #f before entering\n\n;; OPTIONAL: pre-open paths / restrict fds BEFORE entering (post-entry you can\n;; only use already-open fds).\n;; (define fd (capsicum-open-path \"/data\" '(read fstat seek lookup)))\n\n(capsicum-enter!)                 ;; IRREVERSIBLE — drops the global namespace\n(capsicum-in-capability-mode?)    ;; => #t\n\n;; Proof: opening a NEW file by path is denied in capability mode.\n(guard (e (#t 'blocked))\n  (let ([p (open-input-file \"/etc/hosts\")]) (close-input-port p) 'allowed))\n;; => 'blocked  (ENOTCAPABLE / \"not permitted in capability mode\")") ("id" . "capsicum-enter-verify-freebsd")
+   ("imports" "(std security capsicum)")
+   ("notes"
+     .
+     "FreeBSD-only and irreversible. cap_enter is a capability (fd-based) sandbox, NOT a path ACL: after entry the process can only operate on already-open fds — no opening files or creating sockets by name. A DYNAMICALLY-linked program cannot exec after cap_enter (the runtime loader can't open its shared libs), so use it for in-process or statically linked work, not as a generic exec wrapper. Restrict individual fds first with capsicum-limit-fd! and presets (capsicum-compute-only-preset / capsicum-io-only-preset / capsicum-apply-preset!).")
+   ("tags" "capsicum" "freebsd" "sandbox" "cap_enter"
+     "capability mode" "security")
+   ("title"
+     .
+     "Enter FreeBSD Capsicum capability mode and verify enforcement"))
+ (("code"
+    .
+    ";; (foreign-procedure \"symlink\" ...) fails with\n;;   Exception in foreign-procedure: no entry for \"symlink\"\n;; in a DYNAMIC build unless libc's symbols are loaded first.\n\n(def _libc\n  (or (try (load-shared-object \"libc.so.7\")  (catch (e) #f))   ; FreeBSD\n      (try (load-shared-object \"libc.so.6\")  (catch (e) #f))   ; glibc\n      (try (load-shared-object \"libc.so\")    (catch (e) #f))   ; musl\n      (try (load-shared-object \"/usr/lib/libSystem.B.dylib\")   ; macOS\n           (catch (e) #f))\n      (try (load-shared-object \"libSystem.dylib\") (catch (e) #f))))\n\n(def c-symlink\n  (try (foreign-procedure \"symlink\" (string string) int)\n       (catch (e) #f)))\n\n(when c-symlink (c-symlink \"/target\" \"/link\"))") ("id" . "libc-foreign-procedure-load-first")
+   ("imports"
+     "(chezscheme)"
+     "(only (jerboa core) def try catch)")
+   ("notes"
+     .
+     "In static/musl builds libc symbols are already linked and foreign-procedure resolves them directly. In dynamic builds (incl. the dev tree on macOS) you must load-shared-object libc first — on macOS that's libSystem, not libc.so. Mirror the dance in (std os posix). Guard both the load and the foreign-procedure binding with try/catch so a missing symbol degrades gracefully instead of erroring at library-load time.")
+   ("tags" "ffi" "foreign-procedure" "libc"
+     "load-shared-object" "symlink" "dynamic")
+   ("title"
+     .
+     "Calling a libc function via foreign-procedure: load libc/libSystem first in dynamic builds"))
+ (("code"
+    .
+    ";; make-sandbox-policy takes a SINGLE entries-alist (low level).\n;; The keyword factory is `sandbox-policy` — use it for readable config:\n(define spol\n  (sandbox-policy\n   'read-paths:  (list \"/src\" \"/usr/lib\")\n   'write-paths: (list \"/build\")\n   'exec-paths:  (list \"/bin/sh\" \"/usr/bin\")\n   'net:         'deny))            ; or 'allow\n\n(sandbox-backend)                    ; => 'landlock | 'seatbelt | 'capsicum | 'none\n;; apply to a command (macOS wraps with sandbox-exec; else returns cmd as-is):\n(if (sandbox-command-wrapper-available? spol)\n    (sandbox-wrap-command spol (list \"/bin/sh\" \"-c\" \"echo hi\"))\n    (list \"/bin/sh\" \"-c\" \"echo hi\"))") ("id" . "limits-sandbox-policy-keyword-constructor")
+   ("imports"
+     "(only (std os limits sandbox) sandbox-policy sandbox-backend sandbox-wrap-command sandbox-command-wrapper-available?)")
+   ("notes"
+     .
+     "Calling (make-sandbox-policy 'read-paths: ...) errors with 'incorrect number of arguments' — make-sandbox-policy is the raw record ctor taking one alist. Use (sandbox-policy KEY: VAL ...) instead. Keys end in a colon (read-paths: write-paths: exec-paths: net: net-allow: syscalls: ptrace?: no-new-privs? capsicum?:). net is a symbol 'allow/'deny. sandbox-backend is a static report; actual enforcement happens at launch / via sandbox-wrap-command. On platforms with no kernel sandbox, backend is 'none — fail closed if you require isolation.")
+   ("tags" "sandbox" "limits" "security" "sandbox-policy"
+     "landlock" "seatbelt")
+   ("title"
+     .
+     "(std os limits sandbox): build a policy with the keyword factory sandbox-policy, not make-sandbox-policy"))
+ (("code"
+    .
+    ";; WRONG — r1/r2/r3 init exprs may run in ANY order under Chez, so a\n;; \"add then list then remove\" sequence can fire as remove-before-add:\n;; (let ([r1 (do-add!)] [r2 (do-list)] [r3 (do-remove!)]) ...)\n;;\n;; RIGHT — let* binds left-to-right, guaranteeing order:\n(let* ([r1 (do-add!)]\n       [r2 (do-list)]\n       [r3 (do-remove!)])\n  (list r1 r2 r3))") ("id" . "let-unsequenced-use-let-star") ("imports")
+   ("notes"
+     .
+     "R6RS/Chez leave the evaluation order of plain `let` init expressions UNSPECIFIED (Chez often runs them right-to-left). If the inits perform ordered side effects — writing then reading a file, add/list/remove, capturing stdout in sequence — a `let` silently reorders them and you get nondeterministic test failures. Use `let*` (or `begin`/explicit sequencing) whenever init order matters. Same caution applies to function argument evaluation order.")
+   ("tags" "let" "let-star" "evaluation-order" "side-effects"
+     "chez" "gotcha")
+   ("title"
+     .
+     "Chez does not sequence (let ...) init exprs: use let* when bindings have side effects")))
diff --git a/data/features.sexp b/data/features.sexp
index 9dbe84d..589b21e 100644
--- a/data/features.sexp
+++ b/data/features.sexp
@@ -1196,4 +1196,99 @@
    ("use_case"
      .
      "After editing a Jerboa project source file, agents need a direct verifier result before building. If verify rejects top-level export forms, agents fall back to make build and lose the faster, more focused syntax/expand feedback.")
+   ("votes" . 0))
+ (("description"
+    .
+    "Calling jerboa_compile_check with file_path on a large .ss file fails with 'Exception in string-ref: <N> is not a valid index for \"<entire file contents>\"' instead of returning compile diagnostics. The exception message also embeds the FULL file source, so a single failed check dumps ~90KB of text back into the agent context. Two asks: (1) fix the off-by-one/length bug so large files validate normally; (2) never inline the entire source into an error string — truncate to the relevant span or omit it. Likely an internal cursor/offset (line-start index, span end) computed past (string-length src) on files above some size.")
+   ("estimated_token_reduction"
+     .
+     "~20k+ tokens per failed check (suppressing the full-file echo) plus avoiding a multi-minute make fallback to validate large files")
+   ("example_scenario"
+     .
+     "jerboa_compile_check(file_path: '.../provider/provider.ss') on a ~94KB file returned 'Exception in string-ref: 94269 is not a valid index for \"<the full 94KB source>\"'. I had to fall back to `make binary` (minutes) to validate a one-line edit, and the error echoed the entire file into context.")
+   ("id" . "compile-check-large-file-robustness")
+   ("impact" . "medium")
+   ("tags" "compile-check" "large-file" "robustness"
+     "error-message" "token-bloat")
+   ("title"
+     .
+     "jerboa_compile_check crashes on large files with a string-ref index exception (and echoes the whole file)")
+   ("use_case"
+     .
+     "Validating any non-trivial generated/edited .ss module before make. Large files (multi-KB providers, generated code) are exactly where a pre-build compile check is most valuable, and exactly where it currently fails.")
+   ("votes" . 0))
+ (("description"
+    .
+    "jerboa_compile_check expands forms against one libdir set, but a real build resolves a different/overlapping set (vendor/ clones + lib/ overrides). Chez prefers an existing compiled .so/.wpo/.<machine> over a newer .ss in libdir order, so a changed .ss whose stale compiled artifact sits earlier on the path (or in a vendored copy) is silently NOT recompiled — compile_check reports 'No errors' while the actual build fails (e.g. 'missing import for X' because the stale library lacks a newly-added export). Add a mode/tool that, given the project + libdirs, flags any compiled artifact older than its sibling .ss (a 'stale shadow') and optionally deletes them so the build picks up source edits.")
+   ("estimated_token_reduction"
+     .
+     "saves 4-6 failed build+inspect cycles (~5-10k tokens) per stdlib edit that touches a vendored or cached module")
+   ("example_scenario"
+     .
+     "Added a safe-blocklist export to (std security seccomp); jerboa_compile_check passed, but the build failed 'missing import for safe-blocklist' because a stale vendor/jerboa/.../seccomp.wpo (compiled from the old source) shadowed the edited .ss. Only deleting ALL stale .so/.wpo/.tarm64osx across the libdirs fixed it — after ~5 failed build+inspect cycles.")
+   ("id" . "compile-check-stale-artifact-detection")
+   ("impact" . "high")
+   ("tags" "compile-check" "stale" "build" "libdir" "vendor")
+   ("title"
+     .
+     "compile-check should detect/clean stale .so/.wpo shadowing a changed .ss")
+   ("use_case"
+     .
+     "After editing a stdlib/module that has a vendored copy or a compiled cache, before building — to avoid silent stale-artifact build failures.")
+   ("votes" . 0))
+ (("description"
+    .
+    "A reusable bounded circular byte buffer with an absolute-offset cursor: make-ring (cap), ring-append! (evicting oldest when full), ring-oldest-offset / ring-newest-offset (absolute byte counts), and ring-slice-from (fresh bytevector of [from-offset, total), clamped forward if evicted). Useful for output scrollback/replay, log tailing, and rate-limited capture. Each consumer currently reimplements it.")
+   ("estimated_token_reduction"
+     .
+     "~60 lines + unit tests reimplemented per consumer; one stdlib module + a howto recipe replaces it")
+   ("example_scenario"
+     .
+     "jsh's mux added a per-pane 256KiB replay ring (append!/oldest-offset/newest-offset/slice-from) so a reconnecting client can resume output from a byte offset. That circular-buffer logic is generic and belongs in the stdlib rather than buried in a jsh module (and was separately unit-tested to get the wrap/eviction offset math right).")
+   ("id" . "std-io-ring-buffer") ("impact" . "medium")
+   ("tags" "ring-buffer" "io" "bytevector" "scrollback"
+     "stdlib")
+   ("title"
+     .
+     "Add a bounded ring buffer to the stdlib ((std io ring-buffer))")
+   ("use_case"
+     .
+     "Capturing the most-recent N bytes of a stream with resumable absolute offsets — terminal output replay, log tailing, bounded capture.")
+   ("votes" . 0))
+ (("description"
+    .
+    "Given an import set that includes (chezscheme) plus one or more (std ...) modules, statically report every name the std modules re-export that also exists in (chezscheme), and produce the exact `(except (chezscheme) NAME ...)` clause to paste. Today this surfaces only as a compile-time 'multiple definitions for NAME in body' error, forcing a write-compile-read-fix cycle per conflicting name. jerboa_check_import_conflicts scans for 'obvious' conflicts but did not catch base64-encode/base64-decode (std text base64) or sha256-bytevector (std crypto sha256-pure) in this session.")
+   ("estimated_token_reduction"
+     .
+     "~1-2 compile/fix round-trips (~400-800 tokens) per affected module")
+   ("example_scenario"
+     .
+     "Importing (std text base64) with (chezscheme) fails with 'multiple definitions for base64-encode in body'; the fix is (except (chezscheme) base64-encode base64-decode) but you only learn the names one compile at a time.")
+   ("id" . "predict-builtin-shadow-except")
+   ("impact" . "medium")
+   ("tags" "import" "except" "chezscheme" "shadow" "conflict")
+   ("title"
+     .
+     "Predict (chezscheme) builtin shadowing and emit the (except ...) fix")
+   ("use_case"
+     .
+     "Writing any library that imports (chezscheme) alongside std modules that wrap Chez builtins (base64, crypto digests, sort, etc.).")
+   ("votes" . 0))
+ (("description"
+    .
+    "A tool that runs a named test or make target (e.g. via jerboa_run_tests / jerboa_make) and compares the pass/fail set against a committed baseline of KNOWN pre-existing failures, reporting only NEW failures. The repo has pre-existing/environmental failures (test-nrepl-auth: missing (std nrepl) on libdirs; test-limits-primitives supervise-timeout: load-sensitive flake that passes standalone but fails under full 'make test' load). Distinguishing 'my change broke this' from 'this was already broken/flaky' currently requires manual `git stash` + rerun on clean master, repeatedly.")
+   ("estimated_token_reduction"
+     .
+     "~2-4 stash/rerun cycles per gating step (~1-3k tokens) on any repo whose suite isn't clean-green")
+   ("example_scenario"
+     .
+     "After each of 8 phases, `make test` exited non-zero solely due to the pre-existing nrepl + supervise failures; confirming that meant stashing changes and rerunning the suite on master several times.")
+   ("id" . "test-target-baseline-diff") ("impact" . "medium")
+   ("tags" "testing" "baseline" "regression" "flaky" "make")
+   ("title"
+     .
+     "Run a test/make target and diff results against a recorded known-failing baseline")
+   ("use_case"
+     .
+     "Gating multi-commit work where 'make test' is not green on clean master, so a raw non-zero exit can't be trusted as 'I broke something'.")
    ("votes" . 0)))
diff --git a/data/security-rules.sexp b/data/security-rules.sexp
index 9012d66..10aa494 100644
--- a/data/security-rules.sexp
+++ b/data/security-rules.sexp
@@ -911,4 +911,20 @@
      .
      "def-C.*:u8vector.*memcpy|def-C.*:u8vector.*memmove|def-C.*:u8vector.*memset")
    ("scope" . "ffi-boundary")
+   ("severity" . "medium"))
+ (("id" . "sandbox-ffi-raw-libc-static-noop")
+   ("message"
+     .
+     "A security/sandbox control is bound via a RAW libc symbol. In a statically-linked (musl) binary there is no dynamic symbol table, so foreign-procedure cannot resolve the symbol at runtime; when the binding is wrapped in a try/catch that falls back to a benign value (e.g. a lambda returning -1), the control silently no-ops — seccomp-available?/the install returns failure, the sandbox is NEVER applied, yet the program proceeds as if protected. This is a fail-OPEN security control and a false sense of security (the same code works in a dynamically-linked build, hiding the gap).")
+   ("pattern"
+     .
+     "foreign-procedure\\s+\"(prctl|cap_enter|cap_getmode|cap_rights_limit|pledge|unveil|seccomp|landlock_[a-z_]+)\"")
+   ("scope" . "ffi-boundary")
+   ("severity" . "high"))
+ (("id" . "unbounded-decompression-bomb")
+   ("message"
+     .
+     "gunzip-bytevector / inflate-bytevector decompress with NO size limit. On attacker-controlled input a few KB can expand to gigabytes (zip/decompression bomb), exhausting memory. (std compress zlib) provides bounded variants for exactly this reason.")
+   ("pattern" . "\\((?:gunzip|inflate)-bytevector")
+   ("scope" . "scheme")
    ("severity" . "medium")))