Move SSD native detection JSON parsing to typed Kotlin

ober

bed2723d46a8818e9a885c90ed0cb66e283a31e3

diff --git a/.build.yml b/.build.yml
index 377d7b8..b2667ad 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#79546b9d6d7f3fa15555c3b4193c2080b1ad4aed"
+  - "https://git.sr.ht/~lisp/jerboa#5ddcc1f4398dad190a45719fb4e1d1e26479b000"
   # 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)" = 79546b9d6d7f3fa15555c3b4193c2080b1ad4aed
-      test "$(git -C ../jerboa rev-parse 'HEAD^{tree}')" = ff5aa378868436fe9ef1f906f52a27aa27b9502e
+      test "$(git -C ../jerboa rev-parse HEAD)" = 5ddcc1f4398dad190a45719fb4e1d1e26479b000
+      test "$(git -C ../jerboa rev-parse 'HEAD^{tree}')" = ea37e94e6d22df8c5221f5af6999ff0dc866e905
       JERBOA="chez --libdirs .:../jerboa/lib --script" make test
diff --git a/dependencies.lock.json b/dependencies.lock.json
index d8120b3..bc97af4 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": "79546b9d6d7f3fa15555c3b4193c2080b1ad4aed",
-    "tree": "ff5aa378868436fe9ef1f906f52a27aa27b9502e"
+    "commit": "5ddcc1f4398dad190a45719fb4e1d1e26479b000",
+    "tree": "ea37e94e6d22df8c5221f5af6999ff0dc866e905"
   },
   "assurance_tools": {
     "osv_scanner": {
diff --git a/scripts/verify-supply-chain.sh b/scripts/verify-supply-chain.sh
index 14880ed..a5a0652 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=79546b9d6d7f3fa15555c3b4193c2080b1ad4aed # gitsafe:ignore
-jerboa_tree=ff5aa378868436fe9ef1f906f52a27aa27b9502e # gitsafe:ignore
+jerboa_commit=5ddcc1f4398dad190a45719fb4e1d1e26479b000 # gitsafe:ignore
+jerboa_tree=ea37e94e6d22df8c5221f5af6999ff0dc866e905 # 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 eeed387..578dc05 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -1753,6 +1753,91 @@
                                   (float32 h)
                                   "android-contour")))))))))))))))
 
+    (typed-kotlin-file "com/sfb/ssdreview/SsdVisionJson.kt"
+      (kotlin-imports (org json JSONArray) (org json JSONObject))
+      (typed-library (com sfb ssdreview)
+        (export nativeDetectionCellsFromJson nativeDetectionGroupsFromJson)
+        (type JSONArray)
+        (type JSONObject)
+        (type Int32)
+        (type SsdCell)
+        (type SsdGroup)
+        (def (nullableJsonArrayOrEmpty (raw : (Nullable JSONArray))) : JSONArray
+          (if (nullable-null? raw)
+              (json-array-empty)
+              (nullable-get raw)))
+        (def (nativeDetectionCellId (index : Int32)) : String
+          (string-append "c"
+            (string-pad-start (int32->string (+ index (int32 1))) (int32 4) #\0)))
+        (def (nativeDetectionGroupId (index : Int32)) : String
+          (string-append "g"
+            (string-pad-start (int32->string (+ index (int32 1))) (int32 4) #\0)))
+        (def (nativeDetectionCellsFromJson (raw : (Nullable JSONArray))) : (MutableList SsdCell)
+          (let ((cells (mutable-list-empty SsdCell)))
+            (let ((items (nullableJsonArrayOrEmpty raw)))
+              (begin
+                (for/fold ((ignored (int32 0)))
+                          ((index (in-range (int32 0) (json-array-length items))))
+                  (let ((entry (json-array-opt-json-object items index)))
+                    (if (nullable-null? entry)
+                        ignored
+                        (let ((json (nullable-get entry)))
+                          (begin
+                            (mutable-list-add! cells
+                              (make-SsdCell
+                                (json-object-opt-string-default json "id"
+                                  (nativeDetectionCellId index))
+                                (json-object-opt-float32 json "x")
+                                (json-object-opt-float32 json "y")
+                                (json-object-opt-float32 json "w")
+                                (json-object-opt-float32 json "h")
+                                (json-object-opt-string-default json "detector" "rust-color")))
+                            ignored)))))
+                cells))))
+        (def (nativeDetectionGroupsFromJson (raw : (Nullable JSONArray))) : (MutableList SsdGroup)
+          (let ((groups (mutable-list-empty SsdGroup)))
+            (let ((items (nullableJsonArrayOrEmpty raw)))
+              (begin
+                (for/fold ((ignored (int32 0)))
+                          ((index (in-range (int32 0) (json-array-length items))))
+                  (let ((entry (json-array-opt-json-object items index)))
+                    (if (nullable-null? entry)
+                        ignored
+                        (let ((json (nullable-get entry)))
+                          (let ((bboxArray (nullableJsonArrayOrEmpty
+                                             (json-object-opt-json-array json "bbox"))))
+                            (let ((idsArray (nullableJsonArrayOrEmpty
+                                               (json-object-opt-json-array json "cell_ids"))))
+                              (let ((ids (mutable-list-empty String)))
+                                (begin
+                                  (for/fold ((idsIgnored (int32 0)))
+                                            ((idIndex (in-range (int32 0) (json-array-length idsArray))))
+                                    (let ((id (json-array-opt-string idsArray idIndex)))
+                                      (if (string-blank? id)
+                                          idsIgnored
+                                          (begin
+                                            (mutable-list-add! ids id)
+                                            idsIgnored))))
+                                  (mutable-list-add! groups
+                                    (make-SsdGroup
+                                      (json-object-opt-string-default json "id"
+                                        (nativeDetectionGroupId index))
+                                      (json-object-opt-string json "label")
+                                      (json-object-opt-string json "box_type_id")
+                                      (json-object-opt-string-default json "status" "candidate")
+                                      (json-object-opt-int32-default json "count" (list-size ids))
+                                      (float-array
+                                        (json-array-opt-float32 bboxArray (int32 0) (float32 0.0))
+                                        (json-array-opt-float32 bboxArray (int32 1) (float32 0.0))
+                                        (json-array-opt-float32 bboxArray (int32 2) (float32 0.0))
+                                        (json-array-opt-float32 bboxArray (int32 3) (float32 0.0)))
+                                      ids
+                                      (json-object-opt-string-default json "firing_arc"
+                                        (json-object-opt-string json "arc"))
+                                      (json-object-opt-string json "notes")))
+                                  ignored))))))))
+                groups))))))
+
     (typed-kotlin-file "com/sfb/ssdreview/UiTextHelpers.kt"
       (typed-library (com sfb ssdreview)
         (export normalizeFiringArc normalizeLabel shouldReplaceGuessLabel
@@ -3967,7 +4052,6 @@
        "package com.sfb.ssdreview"
        ""
        "import android.graphics.Bitmap"
-       "import org.json.JSONArray"
        "import org.json.JSONObject"
        ""
        "object SsdVisionBridge {"
@@ -3998,9 +4082,9 @@
        "            loadError = IllegalStateException(root.optString(\"error\", \"native detector failed\"))"
        "            return null"
        "        }"
-       "        val cells = parseCells(root.optJSONArray(\"cells\"))"
+       "        val cells = nativeDetectionCellsFromJson(root.optJSONArray(\"cells\"))"
        "        if (cells.isEmpty()) return null"
-       "        val groups = parseGroups(root.optJSONArray(\"groups\"))"
+       "        val groups = nativeDetectionGroupsFromJson(root.optJSONArray(\"groups\"))"
        "        return NativeDetection(cells, groups)"
        "    }"
        ""
@@ -4012,51 +4096,6 @@
        "        return pixelsToRgba(pixels)"
        "    }"
        ""
-       "    private fun parseCells(raw: JSONArray?): MutableList<SsdCell> {"
-       "        val cells = mutableListOf<SsdCell>()"
-       "        if (raw == null) return cells"
-       "        for (index in 0 until raw.length()) {"
-       "            val json = raw.optJSONObject(index) ?: continue"
-       "            val id = json.optString(\"id\", \"c%04d\".format(index + 1))"
-       "            cells.add(SsdCell("
-       "                id = id,"
-       "                x = json.optDouble(\"x\", 0.0).toFloat(),"
-       "                y = json.optDouble(\"y\", 0.0).toFloat(),"
-       "                w = json.optDouble(\"w\", 0.0).toFloat(),"
-       "                h = json.optDouble(\"h\", 0.0).toFloat(),"
-       "                detector = json.optString(\"detector\", \"rust-color\")"
-       "            ))"
-       "        }"
-       "        return cells"
-       "    }"
-       ""
-       "    private fun parseGroups(raw: JSONArray?): MutableList<SsdGroup> {"
-       "        val groups = mutableListOf<SsdGroup>()"
-       "        if (raw == null) return groups"
-       "        for (index in 0 until raw.length()) {"
-       "            val json = raw.optJSONObject(index) ?: continue"
-       "            val bboxArray = json.optJSONArray(\"bbox\") ?: JSONArray()"
-       "            val idsArray = json.optJSONArray(\"cell_ids\") ?: JSONArray()"
-       "            val ids = mutableListOf<String>()"
-       "            for (idIndex in 0 until idsArray.length()) {"
-       "                val id = idsArray.optString(idIndex)"
-       "                if (id.isNotBlank()) ids.add(id)"
-       "            }"
-       "            groups.add(SsdGroup("
-       "                id = json.optString(\"id\", \"g%04d\".format(index + 1)),"
-       "                label = json.optString(\"label\"),"
-       "                boxTypeId = json.optString(\"box_type_id\"),"
-       "                status = json.optString(\"status\", \"candidate\"),"
-       "                count = json.optInt(\"count\", ids.size),"
-       "                bbox = FloatArray(4) { bboxArray.optDouble(it, 0.0).toFloat() },"
-       "                cellIds = ids,"
-       "                firingArc = json.optString(\"firing_arc\", json.optString(\"arc\")),"
-       "                notes = json.optString(\"notes\")"
-       "            ))"
-       "        }"
-       "        return groups"
-       "    }"
-       ""
        "    private external fun nativeDetectRgbaJson(width: Int, height: Int, rgba: ByteArray): String"
        "}"
        ))