Move SSD truth matching to typed Kotlin

ober

bae4bc716dd4fa7aae975c9ac775d5419fc67d78

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index a15803e..cbd3610 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -2087,6 +2087,30 @@
                     "."
                     (string-append (int->string tenths) " s")))))))))
 
+    (typed-kotlin-file "com/sfb/ssdreview/TruthMatching.kt"
+      (kotlin-imports (org json JSONObject))
+      (typed-library (com sfb ssdreview)
+        (export sameShipTruth)
+        (type JSONObject)
+        (type Int32)
+        (def (truthMatchingObjectOrEmpty (raw : (Nullable JSONObject))) : JSONObject
+          (if (nullable-null? raw)
+              (json-object-empty)
+              (nullable-get raw)))
+        (def (sameShipTruth (session : SsdSession) (truth : JSONObject)) : Bool
+          (let ((source (truthMatchingObjectOrEmpty (json-object-opt-json-object truth "source"))))
+            (let ((name
+                    (canonicalSourceName
+                      (json-object-opt-string-default
+                        source
+                        "name"
+                        (json-object-opt-string source "path")))))
+              (and (equal? name (canonicalSourceName (SsdSession-sourceName session)))
+                   (and (= (json-object-opt-int32-default source "page" (int32 -1))
+                           (SsdSession-page session))
+                        (= (json-object-opt-int32-default source "dpi" (int32 -1))
+                           (SsdSession-dpi session)))))))))
+
     (typed-kotlin-file "com/sfb/ssdreview/PathSafety.kt"
       (typed-library (com sfb ssdreview)
         (export safePathComponent safeLeaf safeZipEntryName validSourceKey validatedSourceKey)
@@ -5446,14 +5470,6 @@
        "        return true"
        "    }"
        ""
-       "    private fun sameShipTruth(session: SsdSession, truth: JSONObject): Boolean {"
-       "        val source = truth.optJSONObject(\"source\") ?: JSONObject()"
-       "        val name = canonicalSourceName(source.optString(\"name\", source.optString(\"path\")))"
-       "        return name == canonicalSourceName(session.sourceName) &&"
-       "            source.optInt(\"page\", -1) == session.page &&"
-       "            source.optInt(\"dpi\", -1) == session.dpi"
-       "    }"
-       ""
        "    fun applyLearnedGuesses(session: SsdSession): Int {"
        "        val file = File(learnedDir, \"group_examples.json\")"
        "        if (!file.exists()) rebuildLearnedExamples()"