Generate SSD JSON lines merge from typed Jerboa

ober

8684a1fd1300905c8598fcce92ff0787684b3ecb

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index cacac5a..bf3bd7d 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -4748,6 +4748,7 @@
                 ssdPdfDocumentNotDirectory
                 shouldIncludeSsdPdfDocument
                 trimmedNonBlankLines trimmedNonBlankPathParts
+                mergeJsonLinesTextFromLocalAndRemote
                 ensureTruthStoreDirectories
                 sourceBaseName canonicalSourceName isUsefulGuessLabel isErrorStatus
                 formatActivityDuration groupDisplayLabel appendModeStatus
@@ -5172,6 +5173,15 @@
           (kotlin-member-call start))
         (extern (exceptionMessageRaw (error : Exception)) : (Nullable String)
           (kotlin-member-get message))
+        (extern (illegalStateException
+                  (message : String)) : Exception
+          (kotlin-call IllegalStateException))
+        (extern (mutableStringSetSize
+                  (items : (MutableSet String))) : Int32
+          (kotlin-member-get size))
+        (extern (mergedJsonLinesTextFromMutableSet
+                  (items : (MutableSet String))) : String
+          (kotlin-call mergedJsonLinesText))
         (extern (toastMakeTextRaw
                   (activity : MainActivity)
                   (text : String)
@@ -5782,6 +5792,43 @@
                     (begin
                       (mutable-list-add! out part)
                       out))))))
+        (def (mergeJsonLineItems!
+               (lines : (MutableSet String))
+               (items : (List String))
+               (limit : Int32)) : Unit
+          (begin
+            (for/fold ((ignored (int32 0)))
+                      ((i (in-range (int32 0) (list-size items))))
+              (begin
+                (if (jsonLineCountAtLimit
+                      (mutableStringSetSize lines)
+                      limit)
+                    (throw
+                      (illegalStateException
+                        "JSONL line-count limit exceeded")
+                      Unit)
+                    (begin))
+                (mutable-set-add! lines (list-ref items i))
+                ignored))
+            (begin)))
+        (def (mergeJsonLinesTextFromLocalAndRemote
+               (localText : String)
+               (remoteText : String)
+               (hasLocal : Bool)
+               (limit : Int32)) : String
+          (let ((lines (mutable-set-empty String)))
+            (begin
+              (if hasLocal
+                  (mergeJsonLineItems!
+                    lines
+                    (trimmedNonBlankLines localText)
+                    limit)
+                  (begin))
+              (mergeJsonLineItems!
+                lines
+                (trimmedNonBlankLines remoteText)
+                limit)
+              (mergedJsonLinesTextFromMutableSet lines))))
         (def (sourceBaseName (value : String)) : String
           (string-lowercase
             (string-replace-regex
@@ -12075,15 +12122,9 @@
        "    private fun mergeJsonLines(file: File, remoteText: String) {"
        "        require(textUtf8BytesWithinLimit(remoteText, MAX_ZIP_ENTRY_BYTES))"
        "        validateImportedText(file.name, remoteText)"
-       "        val lines = linkedSetOf<String>()"
-       "        if (fileExists(file)) {"
-       "            trimmedNonBlankLines(readLocalText(file)).forEach { lines.add(it) }"
-       "        }"
-       "        trimmedNonBlankLines(remoteText).forEach {"
-       "            if (jsonLineCountAtLimit(lines.size, MAX_JSONL_LINES)) throw IllegalStateException(\"JSONL line-count limit exceeded\")"
-       "            lines.add(it)"
-       "        }"
-       "        val merged = mergedJsonLinesText(lines)"
+       "        val hasLocal = fileExists(file)"
+       "        val localText = if (hasLocal) readLocalText(file) else \"\""
+       "        val merged = mergeJsonLinesTextFromLocalAndRemote(localText, remoteText, hasLocal, MAX_JSONL_LINES)"
        "        require(textUtf8BytesWithinLimit(merged, MAX_ZIP_ENTRY_BYTES))"
        "        atomicWriteText(file, merged)"
        "    }"