From 739b4826dfec32dfc4b65ef8aec2d23d6ee44404 Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Tue, 30 Jun 2026 18:48:54 -0700 Subject: [PATCH] Fix AI Literacy Course lesson-item ordering CourseShell.itemsOf() sorted items by leadingNumber(it.title), but the title had already had its "N-" ordering prefix stripped by itemTitle(). The leadingNumber regex (\d+) then matched the first digit anywhere in the stripped title, so items whose text contains a number (e.g. "5-lesson-1-recap-quiz" -> "Lesson 1 Recap Quiz") sorted on that embedded "1" and jumped ahead of the genuinely-first items (e.g. "1-objectives" -> "Objectives", which had no digit left and sorted last). Every lesson came out scrambled and no longer matched the numbered course sequence. Sort instead by the numeric prefix of the original file/dir name, captured into Item.order before itemTitle() strips it, and anchor leadingNumber to ^(\d+) so it can only read the ordering prefix. Bump CourseInstaller.INSTALL_VERSION 2 -> 3 so existing installs re-extract and regenerate index.html with the corrected order. --- .../ailiteracycourse/CourseInstaller.kt | 5 +++-- .../ailiteracycourse/CourseShell.kt | 20 ++++++++++++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/ai-literacy-course/src/main/kotlin/org/appdevforall/ailiteracycourse/CourseInstaller.kt b/ai-literacy-course/src/main/kotlin/org/appdevforall/ailiteracycourse/CourseInstaller.kt index 8e6ab1e..c71e3e4 100644 --- a/ai-literacy-course/src/main/kotlin/org/appdevforall/ailiteracycourse/CourseInstaller.kt +++ b/ai-literacy-course/src/main/kotlin/org/appdevforall/ailiteracycourse/CourseInstaller.kt @@ -29,8 +29,9 @@ object CourseInstaller { // through this viewer (served same-origin from /pdfjs/web/viewer.html). private const val PDFJS_ASSET = "pdfjs.zip" - // Bumped to 2 to force existing installs to re-extract with the PDF viewer. - private const val INSTALL_VERSION = 2 + // Bumped to 3 to force existing installs to re-extract and regenerate the + // navigation shell with the corrected lesson-item ordering. + private const val INSTALL_VERSION = 3 private const val MARKER = ".installed-v$INSTALL_VERSION" private val installLock = Any() diff --git a/ai-literacy-course/src/main/kotlin/org/appdevforall/ailiteracycourse/CourseShell.kt b/ai-literacy-course/src/main/kotlin/org/appdevforall/ailiteracycourse/CourseShell.kt index 2fc018d..875bfdc 100644 --- a/ai-literacy-course/src/main/kotlin/org/appdevforall/ailiteracycourse/CourseShell.kt +++ b/ai-literacy-course/src/main/kotlin/org/appdevforall/ailiteracycourse/CourseShell.kt @@ -36,25 +36,25 @@ object CourseShell { private enum class Kind { VIDEO, ACTIVITY, PDF } - private data class Item(val title: String, val href: String, val kind: Kind) + private data class Item(val order: Int, val title: String, val href: String, val kind: Kind) private fun itemsOf(root: File, section: File): List { val entries = section.listFiles() ?: return emptyList() return entries.mapNotNull { entry -> when { entry.isFile && entry.extension.equals("mp4", true) -> - Item(itemTitle(entry.nameWithoutExtension), rel(root, entry), Kind.VIDEO) + Item(leadingNumber(entry.nameWithoutExtension), itemTitle(entry.nameWithoutExtension), rel(root, entry), Kind.VIDEO) entry.isFile && entry.extension.equals("pdf", true) -> - Item(itemTitle(entry.nameWithoutExtension), rel(root, entry), Kind.PDF) + Item(leadingNumber(entry.nameWithoutExtension), itemTitle(entry.nameWithoutExtension), rel(root, entry), Kind.PDF) entry.isDirectory -> findIndexHtml(entry)?.let { index -> - Item(itemTitle(entry.name), rel(root, index), Kind.ACTIVITY) + Item(leadingNumber(entry.name), itemTitle(entry.name), rel(root, index), Kind.ACTIVITY) } else -> null } - }.sortedWith(compareBy({ leadingNumber(it.title) }, { it.title })) + }.sortedWith(compareBy({ it.order }, { it.title })) } /** Shallowest index.html under [dir] (activities nest theirs differently). */ @@ -124,8 +124,14 @@ object CourseShell { private fun itemTitle(raw: String): String = titleCase(raw.replace(Regex("^\\d+-"), "")) - private fun leadingNumber(title: String): Int = - Regex("(\\d+)").find(title)?.groupValues?.get(1)?.toIntOrNull() ?: Int.MAX_VALUE + /** + * The leading "12-" ordering prefix of a raw file/dir name, as an int. + * Anchored to the start so a number later in the name (e.g. the "1" in + * "5-lesson-1-recap-quiz") can't be mistaken for the ordering key. + * Unnumbered entries sort last. + */ + private fun leadingNumber(rawName: String): Int = + Regex("^(\\d+)").find(rawName)?.groupValues?.get(1)?.toIntOrNull() ?: Int.MAX_VALUE private fun titleCase(raw: String): String = raw.split('-', ' ', '_').filter { it.isNotBlank() }.joinToString(" ") { w ->