Move SSD learned guess application to typed Kotlin

ober

e212dfbe8716ee73eb214eb7247901ac02e976f9

diff --git a/.build.yml b/.build.yml
index 4e1f822..38ec0a6 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#435e375cf4d3120e3f3c85b70ffd104cf3fd967e"
+  - "https://git.sr.ht/~lisp/jerboa#72e8809c61fd49a715fcdaaf693e3e07708fe363"
   # 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)" = 435e375cf4d3120e3f3c85b70ffd104cf3fd967e
-      test "$(git -C ../jerboa rev-parse 'HEAD^{tree}')" = 3df8708e7bc5edf47c535257ad4d6dbb33cc385e
+      test "$(git -C ../jerboa rev-parse HEAD)" = 72e8809c61fd49a715fcdaaf693e3e07708fe363
+      test "$(git -C ../jerboa rev-parse 'HEAD^{tree}')" = b412f8d52319fd8fdd64e2044ffd0bd2d308c194
       JERBOA="chez --libdirs .:../jerboa/lib --script" make test
diff --git a/dependencies.lock.json b/dependencies.lock.json
index ccaedd1..f9f2f88 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": "435e375cf4d3120e3f3c85b70ffd104cf3fd967e",
-    "tree": "3df8708e7bc5edf47c535257ad4d6dbb33cc385e"
+    "commit": "72e8809c61fd49a715fcdaaf693e3e07708fe363",
+    "tree": "b412f8d52319fd8fdd64e2044ffd0bd2d308c194"
   },
   "assurance_tools": {
     "osv_scanner": {
diff --git a/scripts/verify-supply-chain.sh b/scripts/verify-supply-chain.sh
index 02ba984..9a57e13 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=435e375cf4d3120e3f3c85b70ffd104cf3fd967e # gitsafe:ignore
-jerboa_tree=3df8708e7bc5edf47c535257ad4d6dbb33cc385e # gitsafe:ignore
+jerboa_commit=72e8809c61fd49a715fcdaaf693e3e07708fe363 # gitsafe:ignore
+jerboa_tree=b412f8d52319fd8fdd64e2044ffd0bd2d308c194 # 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 7822fb4..a9912c9 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -2175,11 +2175,12 @@
       (typed-library (com sfb ssdreview)
         (export make-LearnedGuessMatch LearnedGuessMatch?
                 LearnedGuessMatch-example LearnedGuessMatch-score
-                bestLearnedGuessForGroup)
+                bestLearnedGuessForGroup applyLearnedGuessesFromExamples)
         (type JSONArray)
         (type JSONObject)
         (type Float32)
         (type Int32)
+        (type String)
         (record LearnedGuessMatch
           ((example : JSONObject)
            (score : Float32)))
@@ -2230,7 +2231,55 @@
                             (nullable-get example))))
                     (if (nullable-null? candidate)
                       best
-                      (betterLearnedGuess (nullable-get candidate) best))))))))))
+                      (betterLearnedGuess (nullable-get candidate) best))))))))
+        (def (learnedGuessCanReplace? (group : SsdGroup)) : Bool
+          (or (string-blank? (SsdGroup-label group))
+              (string-ends-with? (SsdGroup-label group) "?")))
+        (def (learnedGuessNote (match : LearnedGuessMatch)) : String
+          (let ((example (LearnedGuessMatch-example match)))
+            (string-append
+              "Learned guess from "
+              (string-append
+                (json-object-opt-string example "truth_path")
+                (string-append
+                  " score "
+                  (float32-fixed3 (LearnedGuessMatch-score match)))))))
+        (def (applyLearnedGuessMatchToGroup (group : SsdGroup)
+                                            (match : LearnedGuessMatch)) : Unit
+          (let ((example (LearnedGuessMatch-example match)))
+            (begin
+              (SsdGroup-label-set!
+                group
+                (string-append (json-object-opt-string example "label") "?"))
+              (SsdGroup-boxTypeId-set!
+                group
+                (json-object-opt-string-default
+                  example
+                  "box_type_id"
+                  (SsdGroup-boxTypeId group)))
+              (SsdGroup-notes-set! group (learnedGuessNote match)))))
+        (def (applyLearnedGuessForGroup (session : SsdSession)
+                                        (examples : JSONArray)
+                                        (group : SsdGroup)) : Int32
+          (if (not (learnedGuessCanReplace? group))
+            (int32 0)
+            (let ((match (bestLearnedGuessForGroup session examples group)))
+              (if (nullable-null? match)
+                (int32 0)
+                (if (<= (LearnedGuessMatch-score (nullable-get match)) (float32 0.22))
+                  (begin
+                    (applyLearnedGuessMatchToGroup group (nullable-get match))
+                    (int32 1))
+                  (int32 0))))))
+        (def (applyLearnedGuessesFromExamples (session : SsdSession)
+                                              (examples : JSONArray)) : Int32
+          (for/fold ((applied (int32 0)))
+                    ((i (in-range (int32 0) (list-size (SsdSession-groups session)))))
+            (+ applied
+               (applyLearnedGuessForGroup
+                 session
+                 examples
+                 (list-ref (SsdSession-groups session) i)))))))
 
     (typed-kotlin-file "com/sfb/ssdreview/OcrGeometry.kt"
       (typed-library (com sfb ssdreview)
@@ -6573,19 +6622,7 @@
        "        if (!file.exists()) rebuildLearnedExamples()"
        "        if (!file.exists()) return 0"
        "        val examples = JSONObject(readLocalText(file)).optJSONArray(\"examples\") ?: return 0"
-       "        var applied = 0"
-       "        session.groups.forEach { group ->"
-       "            if (group.label.isNotBlank() && !group.label.endsWith(\"?\")) return@forEach"
-       "            val match = bestLearnedGuessForGroup(session, examples, group)"
-       "            if (match != null && match.score <= 0.22f) {"
-       "                val example = match.example"
-       "                group.label = \"${example.optString(\"label\")}?\""
-       "                group.boxTypeId = example.optString(\"box_type_id\", group.boxTypeId)"
-       "                group.notes = \"Learned guess from ${example.optString(\"truth_path\")} score %.3f\".format(match.score)"
-       "                applied += 1"
-       "            }"
-       "        }"
-       "        return applied"
+       "        return applyLearnedGuessesFromExamples(session, examples)"
        "    }"
        ""
        "    fun exportZip(out: OutputStream) {"