From 1fcb6b3d302ec1590ce8fbb5fd14f962e8304aee Mon Sep 17 00:00:00 2001 From: TheLastDarkthorne <36555422+TheLastDarkthorne@users.noreply.github.com> Date: Thu, 20 Aug 2026 00:51:53 -0300 Subject: [PATCH] Read a directory's own json before those of its subdirectories createItems() adds items in the order findFiles() returns them, and mergeDown() keeps the position of the first occurrence. A group declared in its parent's json therefore only holds its declared position if that json is read first. eachFileRecurse returns files in filesystem order, so a subdirectory's json is often read before its parent's. The group is then positioned by the order its directory happened to be visited rather than where it was declared, silently reordering siblings: subgroups get hoisted ahead of leaf items and sorted by name. Ordering matters because Mudlet evaluates triggers in tree order, so this changes which trigger sees a line first. Converting an existing package, 19 of 381 groups came out reordered, including one whose first child was an anti-illusion group that ended up in the middle of the list. Sort the discovered files shallowest-first, breaking ties by path. The tie break matters as much as the depth: eachFileRecurse returns directory entries in whatever order the platform gives - alphabetical on NTFS, effectively arbitrary on ext4 - and a group that no parent json declares takes its position from read order. Without a total order the same sources build a differently ordered package on a different machine, which is how this was found: a package that verified clean on Windows came out reordered when the same commit was built on a Linux CI runner. --- .../muddler/mudlet/packages/Package.groovy | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/main/groovy/muddler/mudlet/packages/Package.groovy b/src/main/groovy/muddler/mudlet/packages/Package.groovy index 6321378..268e0cc 100644 --- a/src/main/groovy/muddler/mudlet/packages/Package.groovy +++ b/src/main/groovy/muddler/mudlet/packages/Package.groovy @@ -143,11 +143,29 @@ abstract class Package { def findFiles(fileName) { def fileList = [] this.baseDir.eachFileRecurse FILES, { - if (it.name == fileName) { - e.echo("Found ${fileToRelativePath(it)}") - fileList << it + if (it.name == fileName) { + fileList << it } } + // Shallowest first, so a directory's own json is always read before those + // of its subdirectories. createItems() adds items in the order it reads + // them and mergeDown() keeps the position of the first occurrence, so a + // group declared in its parent's json only holds its declared position if + // that json was read first. Left in filesystem order, a subdirectory read + // earlier would place the group by name instead, silently reordering + // siblings. + // + // Ties are broken by path so the result does not depend on the filesystem. + // eachFileRecurse returns directory entries in whatever order the platform + // gives: alphabetical on NTFS, effectively arbitrary on ext4. A group that + // no parent json declares takes its position from read order, so without a + // total order here the same sources build a differently ordered package on + // a different machine. + fileList = fileList.sort { a, b -> + def depth = { "${it}".split(Pattern.quote(File.separator)).size() } + depth(a) <=> depth(b) ?: "${a}" <=> "${b}" + } + fileList.each { e.echo("Found ${fileToRelativePath(it)}") } return fileList }