Move SSD OCR label replacement helper to typed Kotlin

ober

809c42bd3c79ff87dfba3ceea4031ba907e38012

diff --git a/.build.yml b/.build.yml
index b3ecfdb..4b7b78d 100644
--- a/.build.yml
+++ b/.build.yml
@@ -5,7 +5,7 @@ packages:
   - make=4.4.1-r4
 sources:
   # Build dependency: full immutable commit, mirrored in dependencies.lock.json.
-  - "https://git.sr.ht/~lisp/jerboa#d18e446827df749d11ab9b84c6a3602a40ed9ffa"
+  - "https://git.sr.ht/~lisp/jerboa#d1e30a246a368a56300655395010e695a6225c8c"
   # The second source is the build subject selected by the SourceHut submitter.
   - https://git.sr.ht/~lisp/jerboa-android
 tasks:
@@ -14,6 +14,6 @@ tasks:
       test "$(apk info -v chez-scheme)" = chez-scheme-10.3.0-r2
       test "$(apk info -v git)" = git-2.54.0-r0
       test "$(apk info -v make)" = make-4.4.1-r4
-      test "$(git -C ../jerboa rev-parse HEAD)" = d18e446827df749d11ab9b84c6a3602a40ed9ffa
-      test "$(git -C ../jerboa rev-parse 'HEAD^{tree}')" = a6ca4d76314ba0b773011f6185f822563e1647dd
+      test "$(git -C ../jerboa rev-parse HEAD)" = d1e30a246a368a56300655395010e695a6225c8c
+      test "$(git -C ../jerboa rev-parse 'HEAD^{tree}')" = f66ae467c3960424c5579e4584bc5aab89b81504
       JERBOA="chez --libdirs .:../jerboa/lib --script" make test
diff --git a/dependencies.lock.json b/dependencies.lock.json
index fd907aa..17bd386 100644
--- a/dependencies.lock.json
+++ b/dependencies.lock.json
@@ -11,8 +11,8 @@
   "generator_runtime": {
     "name": "jerboa",
     "repository": "https://git.sr.ht/~lisp/jerboa",
-    "commit": "d18e446827df749d11ab9b84c6a3602a40ed9ffa",
-    "tree": "a6ca4d76314ba0b773011f6185f822563e1647dd"
+    "commit": "d1e30a246a368a56300655395010e695a6225c8c",
+    "tree": "f66ae467c3960424c5579e4584bc5aab89b81504"
   },
   "assurance_tools": {
     "osv_scanner": {
diff --git a/full-kotlin.md b/full-kotlin.md
index 0e431bf..280b9fe 100644
--- a/full-kotlin.md
+++ b/full-kotlin.md
@@ -43,7 +43,7 @@ The vendored Jerboa tree already has the right starting point:
 
 Do not fork that model inside `jandroid.ss`. General Kotlin backend work belongs in Jerboa. Android project and Android DSL work belongs in `jerboa-android`.
 
-Version caveat: this repository now pins Jerboa commit `d18e446827df749d11ab9b84c6a3602a40ed9ffa`, tree `a6ca4d76314ba0b773011f6185f822563e1647dd`. Before editing compiler APIs, check out the exact pinned commit (or the intended replacement commit), rerun the typed parser/checker/core inventory, and record any differences. Do not assume two Jerboa snapshots expose identical IR.
+Version caveat: this repository now pins Jerboa commit `d1e30a246a368a56300655395010e695a6225c8c`, tree `f66ae467c3960424c5579e4584bc5aab89b81504`. Before editing compiler APIs, check out the exact pinned commit (or the intended replacement commit), rerun the typed parser/checker/core inventory, and record any differences. Do not assume two Jerboa snapshots expose identical IR.
 
 ### 2.3 `~/sfb` source requirements
 
diff --git a/scripts/verify-supply-chain.sh b/scripts/verify-supply-chain.sh
index ff53ba8..8ee9c51 100755
--- a/scripts/verify-supply-chain.sh
+++ b/scripts/verify-supply-chain.sh
@@ -3,8 +3,8 @@ set -eu
 
 repo=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd -P)
 lock="$repo/dependencies.lock.json"
-jerboa_commit=d18e446827df749d11ab9b84c6a3602a40ed9ffa # gitsafe:ignore
-jerboa_tree=a6ca4d76314ba0b773011f6185f822563e1647dd # gitsafe:ignore
+jerboa_commit=d1e30a246a368a56300655395010e695a6225c8c # gitsafe:ignore
+jerboa_tree=f66ae467c3960424c5579e4584bc5aab89b81504 # gitsafe:ignore
 gradle_sha=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78
 jdk_macos_sha=8fa1eff40bb637a33613b2ccb8b12c70dc3661cc22cf8e784943715769a05336
 jdk_linux_sha=d8afc263758141a66e0e3aafc321e783f7016696f4eaea067d340a269037d331
diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index f314428..d604e9f 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -366,7 +366,7 @@
 
     (typed-kotlin-file "com/sfb/ssdreview/OcrStrings.kt"
       (typed-library (com sfb ssdreview)
-        (export normalizeOcr compact bankCompatible suppressedByRace)
+        (export normalizeOcr compact bankCompatible suppressedByRace shouldReplaceOcrLabel)
         (def (normalizeOcr (value : String)) : String
           (string-replace-regex
             (string-trim
@@ -394,7 +394,17 @@
               (and (not (equal? race "andromedan"))
                    (or (or (string-contains? key "papanel")
                            (string-contains? key "powerabsorber"))
-                       (string-contains? key "displacementdevice"))))))))
+                       (string-contains? key "displacementdevice"))))))
+        (def (shouldReplaceOcrLabel (label : String) (status : String) (notes : String)) : Bool
+          (let ((text (string-trim label)))
+            (if (string-blank? text)
+              #t
+              (if (string-ends-with? text "?")
+                #t
+                (if (string-matches-regex? text "^\\d+\\s+boxes$")
+                  #t
+                  (and (equal? status "candidate")
+                       (string-starts-with? notes "Guess:")))))))))
 
     (kotlin-file-lines "com/sfb/ssdreview/BoxTypes.kt"
       (
@@ -3144,10 +3154,10 @@
        "        val race = raceKey(session.sourceName + \" \" + session.sourceUri)"
        "        var applied = 0"
        "        session.groups.forEach { group ->"
-       "            if (!shouldReplaceLabel(group)) return@forEach"
+       "            if (!shouldReplaceOcrLabel(group.label, group.status, group.notes)) return@forEach"
        "            val text = nearbyText(session.ocrWords, group.bbox, session.imageWidth, session.imageHeight)"
        "            val best = ocrCandidates(context, text, race).firstOrNull() ?: return@forEach"
-       "            if (protectedBankLabel(group) && !bankCompatible(best.label, best.boxTypeId)) return@forEach"
+       "            if (bankCompatible(group.label, group.boxTypeId) && !bankCompatible(best.label, best.boxTypeId)) return@forEach"
        "            group.label = \"${best.label}?\""
        "            if (best.boxTypeId.isNotBlank()) group.boxTypeId = best.boxTypeId"
        "            group.notes = appendNote(group.notes, \"OCR: ${best.label}, ${best.reason}\")"
@@ -3208,17 +3218,6 @@
        "        }"
        "    }"
        ""
-       "    private fun shouldReplaceLabel(group: SsdGroup): Boolean {"
-       "        val text = group.label.trim()"
-       "        if (text.isBlank()) return true"
-       "        if (text.endsWith(\"?\")) return true"
-       "        if (Regex(\"\"\"^\\d+\\s+boxes$\"\"\").matches(text)) return true"
-       "        return group.status == \"candidate\" && group.notes.startsWith(\"Guess:\")"
-       "    }"
-       ""
-       "    private fun protectedBankLabel(group: SsdGroup): Boolean ="
-       "        bankCompatible(group.label, group.boxTypeId)"
-       ""
        "}"
        ))
     (kotlin-file-lines "com/sfb/ssdreview/SsdReviewView.kt"