updates

ober

07cd8c8783b8aacc2824bbcaea1970ccf8d6f569

diff --git a/data/anti-patterns.sexp b/data/anti-patterns.sexp
index b875f4e..566796d 100644
--- a/data/anti-patterns.sexp
+++ b/data/anti-patterns.sexp
@@ -5183,6 +5183,28 @@
      "jerboa_balanced_insert"))
  (("advice"
     .
+    "Run jerboa_check_balance on the user-facing .ss file first. If the file is already unbalanced, use jerboa_repair_balance dry-run or restore the broken edit and redo the change with jerboa_balanced_insert/jerboa_balanced_replace around complete forms. Verify with make build before investigating generated build/src artifacts.")
+   ("avoid"
+     .
+     "Do not keep hand-editing close parens or debug generated build/src copies as the source of truth.")
+   ("id"
+     .
+     "manual-paren-repair-loop-after-generated-jerboa-edit")
+   ("kinds" "edit" "debug" "verification")
+   ("pattern"
+     .
+     "A model repeatedly hand-counts parentheses, edits partial closing-paren fragments, or probes build/src generated files after a Jerboa source edit fails balance or EOF syntax checks.")
+   ("severity" . "high")
+   ("tags" "jerboa" "scheme" "balance" "balanced-replace"
+     "repair-loop" "generated-source")
+   ("title"
+     .
+     "Manual paren repair loop after generated Jerboa edit")
+   ("tools" "jerboa_check_balance" "jerboa_repair_balance"
+     "jerboa_balanced_insert" "jerboa_balanced_replace"
+     "jerboa_verify"))
+ (("advice"
+    .
     "Either delete the wrapper and let member code call the top-level helper, or give the wrapper a distinct Kotlin name. After regeneration, grep generated Kotlin for `private fun foo(...): ... { return foo(...) }` and run an emulator launch/logcat check.")
    ("avoid"
      .
@@ -5191,7 +5213,7 @@
    ("kinds" "android" "debug-error" "test")
    ("pattern"
      .
-     "private fun ([A-Za-z0-9_]+)\([^)]*\).*return \1\(")
+     "private fun ([A-Za-z0-9_]+)\\([^)]*\\).*return \\1\\(")
    ("severity" . "high")
    ("tags" "typed-kotlin" "android" "kotlin-name"
      "stack-overflow" "generator")
diff --git a/data/cookbooks.sexp b/data/cookbooks.sexp
index 9e1b5e2..77c7418 100644
--- a/data/cookbooks.sexp
+++ b/data/cookbooks.sexp
@@ -7140,25 +7140,31 @@
      "Recover when a .ss file becomes unbalanced (dropped/extra paren)"))
  (("code"
     .
-    "# tools/build_android_embedded_rules.sh
-JERBOA_WPO_SCHEME="${JERBOA_WPO_SCHEME:-$JERBOA_VENDOR/build/chez/$(uname -m | sed 's/arm64/tarm64osx/;s/x86_64/ta6osx/')/bin/$(uname -m | sed 's/arm64/tarm64osx/;s/x86_64/ta6osx/')/scheme}"
-
-JERBUILD_BINARY_OBJ_DIR="$JERBUILD_OBJ_DIR" \
-JERBUILD_BINARY_KEEP_OBJ_DIR=1 \
-JERBOA_WPO_SCHEME="$JERBOA_WPO_SCHEME" \
-"$JERBOA_BIN" jerbuild binary \
-  --libdirs "$ANDROID_RULES_BUILD_DIR/lib" \
-  --cc "$TARGET_CC -shared -fPIC" \
-  --csv-dir "$CSV_DIR" \
-  --xpatch "$XPATCH" \
-  --os-libs "-llog -lm -ldl" \
-  --main-c "$ANDROID_APP/src/main/jni/originaltactics_rules_jni.c" \
-  "$ROOT/sfb/android-service-main.ss" \
-  "$OUTPUT_SO"
-
-# jerbuild.ss subprocess selection
-# If JERBOA_WPO_SCHEME is set, run: $JERBOA_WPO_SCHEME --script cross-wpo-compile.ss
-# Otherwise fall back to the packaged jerboa runtime.") ("id" . "android-cross-wpo-use-host-scheme") ("imports")
+    "(def (send-json-line output obj)\n  (display (json-object->string obj) output)\n  (newline output)\n  (flush-output-port output))\n\n(def (fd->input-output-ports fd c-dup)\n  (let* ((fd2 (c-dup fd))\n         (inp (open-fd-input-port fd (buffer-mode block) (native-transcoder)))\n         (out (open-fd-output-port fd2 (buffer-mode block) (native-transcoder))))\n    (values inp out)))") ("id" . "mcp-jsonrpc-explicit-port-writes")
+   ("imports" "(chezscheme)" "(jerboa prelude clean)")
+   ("notes"
+     .
+     "Do not use `(displayln value output)` when `displayln` is a variadic stdout helper; it can print the port object instead of writing to that port. When creating separate input/output ports for one fd, duplicate the fd first so closing both ports does not double-close the same descriptor.")
+   ("tags" "mcp" "json-rpc" "display" "newline" "socket"
+     "open-fd-output-port")
+   ("title"
+     .
+     "Write newline-delimited JSON-RPC to an explicit port"))
+ (("code"
+    .
+    "(def (json-key key)\n  (if (symbol? key) (symbol->string key) key))\n\n(def (json-alist? value)\n  (and (pair? value)\n       (pair? (car value))\n       (or (symbol? (caar value)) (string? (caar value)))))\n\n(def (json-safe value)\n  (cond\n    ((json-alist? value)\n     (let* ((ht (make-hash-table)))\n       (for-each\n         (lambda (p)\n           (hash-put! ht (json-key (car p)) (json-safe (cdr p))))\n         value)\n       ht))\n    ((list? value) (map json-safe value))\n    (else value)))\n\n(def (json-rpc-response result id)\n  (json-object->string\n    (json-safe\n      (list (cons 'jsonrpc \"2.0\")\n            (cons 'result result)\n            (cons 'id id)))))") ("id" . "json-rpc-alist-to-hash-response")
+   ("imports" "(jerboa prelude)")
+   ("notes"
+     .
+     "Jerboa JSON parsing returns hash tables, but local code often builds alists. `json-object->string` may reject pair objects such as `(jsonrpc . \"2.0\")`; convert alists recursively and turn symbol keys into strings before encoding. The `json-alist?` guard avoids treating arrays of objects as one object.")
+   ("tags" "json" "json-rpc" "alist" "hash-table"
+     "json-object-string" "symbol-keys")
+   ("title"
+     .
+     "Convert alist JSON-RPC responses to hash tables before encoding"))
+ (("code"
+    .
+    "# tools/build_android_embedded_rules.sh\nJERBOA_WPO_SCHEME=\"${JERBOA_WPO_SCHEME:-$JERBOA_VENDOR/build/chez/$(uname -m | sed 's/arm64/tarm64osx/;s/x86_64/ta6osx/')/bin/$(uname -m | sed 's/arm64/tarm64osx/;s/x86_64/ta6osx/')/scheme}\"\n\nJERBUILD_BINARY_OBJ_DIR=\"$JERBUILD_OBJ_DIR\" \\\nJERBUILD_BINARY_KEEP_OBJ_DIR=1 \\\nJERBOA_WPO_SCHEME=\"$JERBOA_WPO_SCHEME\" \\\n\"$JERBOA_BIN\" jerbuild binary \\\n  --libdirs \"$ANDROID_RULES_BUILD_DIR/lib\" \\\n  --cc \"$TARGET_CC -shared -fPIC\" \\\n  --csv-dir \"$CSV_DIR\" \\\n  --xpatch \"$XPATCH\" \\\n  --os-libs \"-llog -lm -ldl\" \\\n  --main-c \"$ANDROID_APP/src/main/jni/originaltactics_rules_jni.c\" \\\n  \"$ROOT/sfb/android-service-main.ss\" \\\n  \"$OUTPUT_SO\"\n\n# jerbuild.ss subprocess selection\n# If JERBOA_WPO_SCHEME is set, run: $JERBOA_WPO_SCHEME --script cross-wpo-compile.ss\n# Otherwise fall back to the packaged jerboa runtime.") ("id" . "android-cross-wpo-use-host-scheme") ("imports")
    ("notes"
      .
      "When Android cross-WPO fails with `Exception in lookup_c_entry: invalid index 51`, reproduce with a one-line .ss file. If raw host Chez Scheme can run the generated cross-wpo helper but `dist/jerboa runtime` cannot, teach jerbuild to honor `JERBOA_WPO_SCHEME` for isolated WPO helpers and rebuild `vendor/jerboa/dist/jerboa` so the packaged multicall binary sees the source change.")
diff --git a/data/error-fixes.sexp b/data/error-fixes.sexp
index 1fdacd7..3997a27 100644
--- a/data/error-fixes.sexp
+++ b/data/error-fixes.sexp
@@ -2946,11 +2946,21 @@
    ("type" . "compile"))
  (("code_example"
     .
-    "JERBOA_WPO_SCHEME="$PWD/vendor/jerboa/build/chez/tarm64osx/bin/tarm64osx/scheme" \
-vendor/jerboa/dist/jerboa jerbuild binary \
-  --csv-dir vendor/jerboa/.chez-cross-tarm64le/lib/csv10.4.0-pre-release.4/tarm64le \
-  --xpatch vendor/jerboa/build/chez/xc-tarm64le/s/xpatch \
-  entry.ss output.so")
+    ";; Before\n(json-object->string (list (cons 'jsonrpc \"2.0\") (cons 'id 1)))\n\n;; After\n(let ((ht (make-hash-table)))\n  (hash-put! ht \"jsonrpc\" \"2.0\")\n  (hash-put! ht \"id\" 1)\n  (json-object->string ht))")
+   ("explanation"
+     .
+     "Jerboa's JSON writer expects JSON objects as hash tables; an alist pair such as `(jsonrpc . \"2.0\")` is not a JSON value by itself and serialization fails.")
+   ("fix"
+     .
+     "Do not pass raw alists or pairs with symbol keys directly to `json-object->string`. Convert response objects to hash tables with string keys first, recursively preserving arrays as lists.")
+   ("id" . "write-json-cannot-serialize-pair")
+   ("pattern"
+     .
+     "Exception in write-json: cannot serialize with irritant \\([^)]* \\. [^)]*\\)")
+   ("type" . "runtime"))
+ (("code_example"
+    .
+    "JERBOA_WPO_SCHEME=\"$PWD/vendor/jerboa/build/chez/tarm64osx/bin/tarm64osx/scheme\" \\\nvendor/jerboa/dist/jerboa jerbuild binary \\\n  --csv-dir vendor/jerboa/.chez-cross-tarm64le/lib/csv10.4.0-pre-release.4/tarm64le \\\n  --xpatch vendor/jerboa/build/chez/xc-tarm64le/s/xpatch \\\n  entry.ss output.so")
    ("explanation"
      .
      "A packaged Jerboa multicall runtime can fail when loading a Chez xpatch and running Android cross-WPO helpers. The same generated helper may succeed under the raw host Chez Scheme binary.")
@@ -2964,12 +2974,7 @@ vendor/jerboa/dist/jerboa jerbuild binary \
    ("type" . "cross-build"))
  (("code_example"
     .
-    ";; Bad inside MainActivity typed template:
-(def (truncateForLogLocal (text : String) (limit : Int32)) : String
-  (modifiers private) (kotlin-name truncateForLog)
-  (truncateForLog text limit))
-
-;; Fix: delete this wrapper and call the top-level truncateForLog directly.")
+    ";; Bad inside MainActivity typed template:\n(def (truncateForLogLocal (text : String) (limit : Int32)) : String\n  (modifiers private) (kotlin-name truncateForLog)\n  (truncateForLog text limit))\n\n;; Fix: delete this wrapper and call the top-level truncateForLog directly.")
    ("explanation"
      .
      "A Typed Android template emitted a private member with `(kotlin-name helper)` whose body calls `helper` with the same arity. Kotlin resolves the call to the member instead of the top-level helper, causing infinite recursion.")
@@ -2979,5 +2984,19 @@ vendor/jerboa/dist/jerboa jerbuild binary \
    ("id" . "typed-kotlin-helper-wrapper-stack-overflow")
    ("pattern"
      .
-     "java\.lang\.StackOverflowError:.*MainActivity\.(truncateForLog|autoEventDelayMillis|hudActionLabel|seekingSortBucket|shortDefenseKind|isInterestingEvent)")
-   ("type" . "android-runtime")))
+     "java\\.lang\\.StackOverflowError:.*MainActivity\\.(truncateForLog|autoEventDelayMillis|hudActionLabel|seekingSortBucket|shortDefenseKind|isInterestingEvent)")
+   ("type" . "android-runtime"))
+ (("code_example"
+    .
+    "(if (nullable-null? truth)\n  (begin)\n  (begin\n    (mainServerPageApplyTruth session (nullable-get truth))\n    (mainServerPagePrune session)\n    (begin)))")
+   ("explanation"
+     .
+     "Typed Android/Jerboa if branches must have the same result type. A branch that ends with a helper returning Int32, such as a prune/count function, mismatches an empty (begin) Unit branch even when the enclosing function returns Unit.")
+   ("fix"
+     .
+     "When using an if for side effects in typed Android code, make both branches return Unit. If the non-empty branch calls an Int32-returning helper for its side effect, add a final (begin) after that call so the branch result is Unit.")
+   ("id" . "typed-branch-type-mismatch-unit-int-return")
+   ("pattern"
+     .
+     "Exception in typed-modules->kotlin-files: typed modules have check errors with irritant (((com sfb ssdreview) branch-type-mismatch))")
+   ("type" . "typecheck")))
diff --git a/lib/jerboa/prelude/clean.ss b/lib/jerboa/prelude/clean.ss
index 4345bb4..dde7e0e 100644
--- a/lib/jerboa/prelude/clean.ss
+++ b/lib/jerboa/prelude/clean.ss
@@ -27,6 +27,8 @@
 
     ;; ---- Runtime (no make-hash-table, hash-table?, 1+, 1-, iota) ----
     ~ bind-method! call-method
+    make-rwlock rwlock? rwlock-read-lock! rwlock-read-unlock! rwlock-write-lock! rwlock-write-unlock! with-read-lock with-write-lock
+    make-thread thread-start! thread-join!
     make-hash-table-eq
     hash-ref hash-get hash-put! hash-update! hash-remove!
     hash-key? hash->list hash->plist hash-for-each hash-map hash-fold
@@ -76,6 +78,7 @@
     zip
 
     ;; ---- std/misc/alist ----
+    alist
     agetq agetv aget
     asetq! asetv! aset!
     pgetq pgetv pget
@@ -117,11 +120,13 @@
       register-struct-type! *struct-types*
       struct-predicate struct-field-ref struct-field-set!
       struct-type-info)
+    (only (std concur util) make-rwlock rwlock? rwlock-read-lock! rwlock-read-unlock! rwlock-write-lock! rwlock-write-unlock! with-read-lock with-write-lock)
+    (only (std misc thread) make-thread thread-start! thread-join!)
     (only (jerboa ffi) c-lambda define-c-lambda begin-ffi c-declare)
     (only (std sort) stable-sort stable-sort!)
     (only (std format) eprintf)
     (only (std error) Error ContractViolation)
-    (only (std sugar) chain chain-and assert!)
+    (only (std sugar) chain chain-and assert! alist)
     (only (std text json) read-json write-json json-object->string string->json-object)
     (only (std os path) path-expand path-normalize path-directory path-strip-directory
                         path-strip-extension path-join)