Move SSD learned example traversal to typed Kotlin

ober

2f5de13650cb00f3511f53ba4b517113c073211d

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 2a7613e..710fbb1 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -3531,12 +3531,42 @@
     (typed-kotlin-file "com/sfb/ssdreview/TruthStoreJson.kt"
       (kotlin-imports (java io File) (org json JSONArray) (org json JSONObject))
       (typed-library (com sfb ssdreview)
-        (export learnedExamplesDocument autosaveTruthPayloadJson truthIndexEntryFromJson)
+        (export learnedExamplesEmpty learnedExamplesCount learnedExamplesFromDocument
+                appendLearnedExamplesFromTruth learnedExamplesDocument
+                autosaveTruthPayloadJson truthIndexEntryFromJson)
         (type File)
         (type JSONArray)
         (type JSONObject)
         (type Int32)
         (type TruthIndexEntry)
+        (def (learnedExamplesEmpty) : JSONArray
+          (json-array-empty))
+        (def (learnedExamplesCount (examples : JSONArray)) : Int32
+          (json-array-length examples))
+        (def (learnedExamplesFromDocument (document : JSONObject)) : JSONArray
+          (truthJsonArrayOrEmpty (json-object-opt-json-array document "examples")))
+        (def (appendLearnedExamplesFromTruth (examples : JSONArray)
+                                             (maxExamples : Int32)
+                                             (truthFileName : String)
+                                             (truth : JSONObject)) : Unit
+          (let ((groups (truthJsonArrayOrEmpty (json-object-opt-json-array truth "groups"))))
+            (begin
+              (for/fold ((ignored (int32 0)))
+                        ((i (in-range (int32 0) (json-array-length groups))))
+                (if (< (json-array-length examples) maxExamples)
+                    (let ((group (json-array-opt-json-object groups i)))
+                      (begin
+                        (if (nullable-null? group)
+                            (begin)
+                            (json-array-put-json-object!
+                              examples
+                              (learnedExampleFromTruthGroup
+                                truth
+                                truthFileName
+                                (nullable-get group))))
+                        ignored))
+                    ignored))
+              (begin))))
         (def (learnedExamplesDocument (examples : JSONArray) (maxExamples : Int32)) : JSONObject
           (let ((out (json-object-empty)))
             (begin
@@ -3848,7 +3878,8 @@
                 remoteTruthPayloadTruth remoteTruthPayloadMatchModified
                 remoteTruthPayloadCount remoteTruthPayloadTruths
                 remoteTruthEntriesCount remoteTruthEntryAt
-                remoteTruthEntryTruth remoteTruthEntryModified)
+                remoteTruthEntryTruth remoteTruthEntryModified
+                remoteDumpImportedCount)
         (type JSONArray)
         (type JSONObject)
         (type Int)
@@ -3891,6 +3922,8 @@
           (json-object-opt-json-object entry "truth"))
         (def (remoteTruthEntryModified (entry : JSONObject)) : Int
           (json-object-opt-int-default entry "modified" (int 0)))
+        (def (remoteDumpImportedCount (payload : JSONObject)) : Int32
+          (json-object-opt-int32-default payload "imported" (int32 0)))
         (def (bestRemoteTruthForSession (session : SsdSession)
                                         (entries : JSONArray)) : (Nullable JSONObject)
           (for/fold ((best (nullable-none JSONObject)))
@@ -6938,7 +6971,7 @@
        "        val file = File(learnedDir, \"group_examples.json\")"
        "        if (!file.exists()) rebuildLearnedExamples()"
        "        if (!file.exists()) return 0"
-       "        val examples = JSONObject(readLocalText(file)).optJSONArray(\"examples\") ?: return 0"
+       "        val examples = learnedExamplesFromDocument(JSONObject(readLocalText(file)))"
        "        return applyLearnedGuessesFromExamples(session, examples)"
        "    }"
        ""
@@ -7120,7 +7153,7 @@
        "                throw IllegalStateException(\"HTTP $code\")"
        "            }"
        "                val response = readResponseText(connection)"
-       "                return JSONObject(response).optInt(\"imported\", 0)"
+       "                return remoteDumpImportedCount(JSONObject(response))"
        "            } finally {"
        "                connection.disconnect()"
        "            }"
@@ -7145,7 +7178,7 @@
        "    }"
        ""
        "    private fun rebuildLearnedExamples() {"
-       "        val examples = JSONArray()"
+       "        val examples = learnedExamplesEmpty()"
        "        val files = groundTruthDir.listFiles { f -> f.name.endsWith(\".truth.json\") }"
        "            ?.sortedWith(compareByDescending<File> { it.lastModified() }.thenBy { it.name })"
        "            ?: emptyList()"
@@ -7155,16 +7188,8 @@
        "            } catch (_: Exception) {"
        "                continue"
        "            }"
-       "            val groups = truth.optJSONArray(\"groups\") ?: JSONArray()"
-       "            for (i in 0 until groups.length()) {"
-       "                if (examples.length() >= MAX_LEARNED_EXAMPLES) break@fileLoop"
-       "                val group = try {"
-       "                    groups.getJSONObject(i)"
-       "                } catch (_: Exception) {"
-       "                    continue"
-       "                }"
-       "                examples.put(learnedExampleFromTruthGroup(truth, file.name, group))"
-       "            }"
+       "            appendLearnedExamplesFromTruth(examples, MAX_LEARNED_EXAMPLES, file.name, truth)"
+       "            if (learnedExamplesCount(examples) >= MAX_LEARNED_EXAMPLES) break@fileLoop"
        "        }"
        "        val learned = learnedExamplesDocument(examples, MAX_LEARNED_EXAMPLES)"
        "        atomicWriteText(containedLeaf(learnedDir, \"group_examples.json\"), learned.toString(2))"