Move SSD truth presence checks to typed Kotlin

ober

d9ae31b1274d14094c20da74f38147695fa4b2f1

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 0ac81a2..8704305 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -2059,7 +2059,8 @@
       (typed-library (com sfb ssdreview)
         (export emptyJsonObject jsonObjectFromText
                 truthJsonArrayOrEmpty truthJsonObjectOrEmpty
-                truthGroupCount nullableTruthGroupCount truthSsdAreaRect
+                truthPresent truthGroupCount nullableTruthGroupCount
+                truthGroupCountAtLeast truthSsdAreaRect nullableTruthSsdAreaRect
                 truthSourceKey truthSourceKeyOrUnknown truthSessionIdOr
                 imageWidth imageHeight)
         (type JSONArray)
@@ -2082,16 +2083,24 @@
           (if (< value (int32 1))
               (int32 1)
               value))
+        (def (truthPresent (truth : (Nullable JSONObject))) : Bool
+          (not (nullable-null? truth)))
         (def (truthGroupCount (truth : JSONObject)) : Int32
           (json-array-length
             (truthJsonArrayOrEmpty
               (json-object-opt-json-array truth "groups"))))
         (def (nullableTruthGroupCount (truth : (Nullable JSONObject))) : Int32
-          (if (nullable-null? truth)
-              (int32 0)
-              (truthGroupCount (nullable-get truth))))
+          (if (truthPresent truth)
+              (truthGroupCount (nullable-get truth))
+              (int32 0)))
+        (def (truthGroupCountAtLeast (count : Int32) (threshold : Int32)) : Bool
+          (>= count threshold))
         (def (truthSsdAreaRect (truth : JSONObject)) : (Nullable FloatArray)
           (jsonRect (json-object-opt-json-array truth "ssd_area")))
+        (def (nullableTruthSsdAreaRect (truth : (Nullable JSONObject))) : (Nullable FloatArray)
+          (if (truthPresent truth)
+              (truthSsdAreaRect (nullable-get truth))
+              (nullable-none FloatArray)))
         (def (truthSourceKey (truth : JSONObject)) : String
           (json-object-opt-string truth "source_key"))
         (def (truthSourceKeyOrUnknown (sourceKey : String)) : String
@@ -5743,7 +5752,7 @@
        "        setStatus(startupStatus(truthStore.storageSummary()))"
        "        restoreLastPdf()"
        "        truthStore.seedBundledTruthAsync { imported ->"
-       "            if (imported > 0) {"
+       "            if (positiveCount(imported)) {"
        "                runOnUiThread {"
        "                    setStatus(truthSeedMergedStatus(imported, truthStore.storageSummary()))"
        "                    if (pdfUri != null) renderCurrentPage()"
@@ -6262,7 +6271,7 @@
        ""
        "                val localTruth = if (shouldLoadLocalTruth(forceDetect)) truthStore.loadTruth(session) else null"
 	       "                val localGroupCount = nullableTruthGroupCount(localTruth)"
-	       "                if (localGroupCount >= FAST_TRUTH_GROUP_THRESHOLD) {"
+	       "                if (truthGroupCountAtLeast(localGroupCount, FAST_TRUTH_GROUP_THRESHOLD)) {"
 	       "                    truthStore.applyTruth(session)"
 	      "                    ssdSessionPruneGroupsOutsideSsdArea(session)"
 	       "                    runOnUiThread {"
@@ -6286,14 +6295,14 @@
        "                var detectorNote = \"\""
        "                var detectorSource = \"unknown\""
        ""
-	       "                if (existingTruth != null && shouldUseExistingTruth(existingTruthGroupCount, forceDetect)) {"
+	       "                if (truthPresent(existingTruth) && shouldUseExistingTruth(existingTruthGroupCount, forceDetect)) {"
 	       "                    detectorSource = \"local-truth\""
-	       "                    session.ssdArea = truthSsdAreaRect(existingTruth)"
+	       "                    session.ssdArea = nullableTruthSsdAreaRect(existingTruth)"
 	       "                    truthStore.applyTruth(session)"
 	      "                    ssdSessionPruneGroupsOutsideSsdArea(session)"
        "                } else {"
-       "                    if (existingTruth != null) {"
-       "                        session.ssdArea = truthSsdAreaRect(existingTruth)"
+       "                    if (truthPresent(existingTruth)) {"
+       "                        session.ssdArea = nullableTruthSsdAreaRect(existingTruth)"
        "                    }"
        "                    val nativeDetection = SsdVisionBridge.detect(bitmap)"
        "                    val detectedCells: List<SsdCell>"
@@ -6307,7 +6316,7 @@
        "                    }"
        "                    ssdSessionAddDetectedCellsInsideArea(session, detectedCells)"
        "                    session.groups.addAll(initialGroupsLimited(session.cells, 120))"
-       "                    if (existingTruth != null && shouldApplyExistingTruth(existingTruthGroupCount)) {"
+       "                    if (truthPresent(existingTruth) && shouldApplyExistingTruth(existingTruthGroupCount)) {"
        "                        truthStore.applyTruth(session)"
        "                        ssdSessionPruneGroupsOutsideSsdArea(session)"
        "                    }"
@@ -6323,12 +6332,12 @@
 	       "                    reviewView.session = session"
 	       "                    reviewView.selectedGroupId = null"
 	       "                    reviewView.ssdArea = session.ssdArea"
-       "                    val truthNote = detectionTruthNote(existingTruth != null)"
+       "                    val truthNote = detectionTruthNote(truthPresent(existingTruth))"
        "                    val ocrNote = detectionOcrNote(ocrGuesses, session.ocrWords.size)"
        "                    val learnedNote = detectionLearnedNote(learnedGuesses)"
        "                    setStatus(sessionCountsStatus(session, detectorNote + truthNote + ocrNote + learnedNote))"
        "                    logDetectResult(\"page-load\", session, detectorSource, learnedGuesses, ocrGuesses)"
-       "                    if (shouldMergeRemoteTruth(existingTruth != null, forceDetect)) mergeRemoteTruthAsync(session, expectedGeneration, uri, expectedPageIndex)"
+       "                    if (shouldMergeRemoteTruth(truthPresent(existingTruth), forceDetect)) mergeRemoteTruthAsync(session, expectedGeneration, uri, expectedPageIndex)"
        "                }"
        "            } catch (error: Exception) {"
        "                runOnUiThread {"
@@ -6353,7 +6362,7 @@
        "    private fun mergeRemoteTruthAsync(session: SsdSession, generation: Int, uri: Uri, page: Int) {"
        "        thread {"
        "            val matches = truthStore.pullRemoteTruth(session)"
-       "            if (matches <= 0) return@thread"
+       "            if (!positiveCount(matches)) return@thread"
        "            val truth = truthStore.loadTruth(session) ?: return@thread"
        "            val merged = session.copy("
        "                cells = mutableListOf(),"
@@ -8339,7 +8348,7 @@
        "                        imported += 1"
        "                    }"
        "                }"
-       "                if (imported > 0) rebuildLearnedExamples()"
+       "                if (positiveCount(imported)) rebuildLearnedExamples()"
        "                return remoteTruthPayloadCount(payload)"
        "            }"
        "            val entries = remoteTruthPayloadTruths(payload)"
@@ -8358,7 +8367,7 @@
        "                    imported += 1"
        "                }"
        "            }"
-       "            if (imported > 0) rebuildLearnedExamples()"
+       "            if (positiveCount(imported)) rebuildLearnedExamples()"
        "                remoteTruthEntriesCount(entries)"
        "            } finally {"
        "                connection.disconnect()"
@@ -8535,7 +8544,7 @@
        "    fun syncTree(treeUri: Uri): Pair<Int, Int> {"
        "        val tree = DocumentFile.fromTreeUri(context, treeUri) ?: return 0 to 0"
        "        val imported = importFromTree(tree)"
-       "        if (imported > 0) invalidateTruthIndex()"
+       "        if (positiveCount(imported)) invalidateTruthIndex()"
        "        rebuildLearnedExamples()"
        "        val exported = exportToTree(tree)"
        "        return imported to exported"