Use typed DocumentFile predicates in SSD tree import

ober

b81528fd9772f737027c98a76974f89e156d29f1

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index bf03ed4..5970126 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -3894,7 +3894,8 @@
                 boxTypeFallbackName boxTypeLabelMatches defaultBoxTypeLabel
                 isGenericGuessLabel isSsdPdfName
                 shouldIncludeSsdPdfChoiceName shouldIncludeSsdPdfFile
-                shouldStopSsdPdfTreeScan ssdPdfDocumentIsDirectory
+                shouldStopSsdPdfTreeScan documentFileIsDirectory
+                documentFileIsRegular ssdPdfDocumentIsDirectory
                 shouldIncludeSsdPdfDocument
                 trimmedNonBlankLines trimmedNonBlankPathParts
                 sourceBaseName canonicalSourceName isUsefulGuessLabel isErrorStatus
@@ -4082,13 +4083,17 @@
                                        (maxDepth : Int32)) : Bool
           (or (> depth maxDepth)
               (>= currentChoices limit)))
-        (def (ssdPdfDocumentIsDirectory (file : DocumentFile)) : Bool
+        (def (documentFileIsDirectory (file : DocumentFile)) : Bool
           (documentFileIsDirectoryRaw file))
+        (def (documentFileIsRegular (file : DocumentFile)) : Bool
+          (documentFileIsFile file))
+        (def (ssdPdfDocumentIsDirectory (file : DocumentFile)) : Bool
+          (documentFileIsDirectory file))
         (def (shouldIncludeSsdPdfDocument (file : DocumentFile)
                                           (name : String)
                                           (currentChoices : Int32)
                                           (limit : Int32)) : Bool
-          (and (documentFileIsFile file)
+          (and (documentFileIsRegular file)
                (shouldIncludeSsdPdfChoiceName name currentChoices limit)))
         (def (trimmedNonBlankLines (text : String)) : (MutableList String)
           (let ((lines (stringLines text)))
@@ -5307,7 +5312,8 @@
         (export safePathComponent safeLeaf safeZipEntryName validSourceKey
                 validatedSourceKey treeFileMime
                 safeTreePathParts treePathLeaf treePathDirectoryParts
-                safeImportLeafEntryName zipEntryRootName zipEntryLeafName)
+                safeImportLeafEntryName zipEntryRootName zipEntryLeafName
+                jsonLinesFileName)
         (type Int32)
         (type Char)
         (extern (stringSplitChar (text : String)
@@ -5323,6 +5329,8 @@
           (and (safePathComponent name)
                (or (string-ends-with? name ".json")
                    (string-ends-with? name ".jsonl"))))
+        (def (jsonLinesFileName (name : String)) : Bool
+          (string-ends-with? name ".jsonl"))
         (def (treeFileMime (name : String)) : String
           (if (string-ends-with? name ".json")
             "application/json"
@@ -8495,7 +8503,7 @@
        "        listOf(\"ground_truth\", \"learned\", \"events\", \"reviews\").forEach { name ->"
        "            val sourceDir = tree.findFile(name)"
        "            val localDir = File(root, name)"
-       "            if (sourceDir != null && sourceDir.isDirectory) {"
+       "            if (sourceDir != null && documentFileIsDirectory(sourceDir)) {"
        "                count += importTreeDir(sourceDir, localDir, 0, budget)"
        "            }"
        "        }"
@@ -8514,11 +8522,11 @@
        "        sourceDir.listFiles().forEach { child ->"
        "            budget.entries += 1"
        "            if (budget.entries > MAX_ZIP_ENTRIES) throw IllegalStateException(\"Import tree exceeds entry limit\")"
-       "            if (child.isDirectory) {"
+       "            if (documentFileIsDirectory(child)) {"
        "                val directoryName = child.name ?: return@forEach"
        "                if (!safePathComponent(directoryName)) throw IllegalStateException(\"Unsafe import directory\")"
        "                count += importTreeDir(child, File(localDir, directoryName), depth + 1, budget)"
-       "            } else if (child.isFile) {"
+       "            } else if (documentFileIsRegular(child)) {"
        "                val name = child.name ?: return@forEach"
        "                if (!safeLeaf(name)) return@forEach"
        "                val remaining = MAX_ZIP_TOTAL_BYTES - budget.expandedBytes"
@@ -8530,7 +8538,7 @@
        "                budget.expandedBytes += bytes.size.toLong()"
        "                validateImportedText(name, bytes.decodeToString())"
        "                val dest = containedImportPath(root.toPath().relativize(File(localDir, name).toPath()).toString())"
-       "                if (name.endsWith(\".jsonl\")) {"
+       "                if (jsonLinesFileName(name)) {"
        "                    mergeJsonLines(dest, bytes.decodeToString())"
        "                    count += 1"
        "                } else if (shouldReplaceLocal(dest, bytes.decodeToString(), child.lastModified())) {"
@@ -8544,7 +8552,7 @@
        ""
        "    private fun ensureTreeDir(parent: DocumentFile, name: String): DocumentFile ="
        "        parent.findFile(name.takeIf { safePathComponent(it) }"
-       "            ?: throw IllegalArgumentException(\"Unsafe tree directory\"))?.takeIf { it.isDirectory } ?: parent.createDirectory(name)"
+       "            ?: throw IllegalArgumentException(\"Unsafe tree directory\"))?.takeIf { documentFileIsDirectory(it) } ?: parent.createDirectory(name)"
        "            ?: throw IllegalStateException(unableToCreateSyncFolderMessage(name))"
        ""
        "    private fun writeTreeFile(parent: DocumentFile, relativeName: String, source: File) {"