« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--package-lock.json509
-rw-r--r--package.json1
-rw-r--r--src/content/dependencies/generateAlbumReleaseInfo.js16
-rw-r--r--src/static/js/search-worker.js1
-rw-r--r--src/url-spec.js2
-rw-r--r--src/util/search-spec.js9
-rw-r--r--src/web-routes.js26
-rw-r--r--src/write/build-modes/static-build.js120
-rw-r--r--tap-snapshots/test/snapshot/generateAlbumTrackList.js.test.cjs50
-rw-r--r--test/lib/composite.js33
-rw-r--r--test/lib/index.js1
-rw-r--r--test/unit/data/composite/control-flow/withResultOfAvailabilityCheck.js18
-rw-r--r--test/unit/data/composite/data/withPropertiesFromObject.js59
-rw-r--r--test/unit/data/composite/data/withPropertyFromObject.js23
-rw-r--r--test/unit/data/composite/data/withUniqueItemsOnly.js23
15 files changed, 438 insertions, 453 deletions
diff --git a/package-lock.json b/package-lock.json
index 7d5d3152..11bc6a41 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,7 +7,7 @@
         "": {
             "name": "hsmusic-wiki",
             "version": "0.1.0",
-            "license": "GPL-3.0",
+            "license": "MIT",
             "dependencies": {
                 "@js-temporal/polyfill": "^0.4.4",
                 "chroma-js": "^2.4.2",
@@ -20,6 +20,7 @@
                 "js-yaml": "^4.1.0",
                 "marked": "^10.0.0",
                 "msgpackr": "^1.10.2",
+                "rimraf": "^5.0.7",
                 "striptags": "^4.0.0-alpha.4",
                 "word-wrap": "^1.2.3"
             },
@@ -170,7 +171,6 @@
             "version": "8.0.2",
             "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
             "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
-            "dev": true,
             "dependencies": {
                 "string-width": "^5.1.2",
                 "string-width-cjs": "npm:string-width@^4.2.0",
@@ -187,7 +187,6 @@
             "version": "6.0.1",
             "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
             "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
-            "dev": true,
             "engines": {
                 "node": ">=12"
             },
@@ -199,7 +198,6 @@
             "version": "7.1.0",
             "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
             "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
-            "dev": true,
             "dependencies": {
                 "ansi-regex": "^6.0.1"
             },
@@ -658,7 +656,6 @@
             "version": "0.11.0",
             "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
             "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
-            "dev": true,
             "optional": true,
             "engines": {
                 "node": ">=14"
@@ -954,70 +951,6 @@
                 "@tapjs/core": "2.0.1"
             }
         },
-        "node_modules/@tapjs/fixture/node_modules/brace-expansion": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
-            "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
-            "dev": true,
-            "dependencies": {
-                "balanced-match": "^1.0.0"
-            }
-        },
-        "node_modules/@tapjs/fixture/node_modules/glob": {
-            "version": "10.4.1",
-            "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz",
-            "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==",
-            "dev": true,
-            "dependencies": {
-                "foreground-child": "^3.1.0",
-                "jackspeak": "^3.1.2",
-                "minimatch": "^9.0.4",
-                "minipass": "^7.1.2",
-                "path-scurry": "^1.11.1"
-            },
-            "bin": {
-                "glob": "dist/esm/bin.mjs"
-            },
-            "engines": {
-                "node": ">=16 || 14 >=14.18"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/isaacs"
-            }
-        },
-        "node_modules/@tapjs/fixture/node_modules/minimatch": {
-            "version": "9.0.4",
-            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
-            "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
-            "dev": true,
-            "dependencies": {
-                "brace-expansion": "^2.0.1"
-            },
-            "engines": {
-                "node": ">=16 || 14 >=14.17"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/isaacs"
-            }
-        },
-        "node_modules/@tapjs/fixture/node_modules/rimraf": {
-            "version": "5.0.7",
-            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz",
-            "integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==",
-            "dev": true,
-            "dependencies": {
-                "glob": "^10.3.7"
-            },
-            "bin": {
-                "rimraf": "dist/esm/bin.mjs"
-            },
-            "engines": {
-                "node": ">=14.18"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/isaacs"
-            }
-        },
         "node_modules/@tapjs/intercept": {
             "version": "2.0.1",
             "resolved": "https://registry.npmjs.org/@tapjs/intercept/-/intercept-2.0.1.tgz",
@@ -1264,24 +1197,6 @@
                 "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/@tapjs/run/node_modules/rimraf": {
-            "version": "5.0.7",
-            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz",
-            "integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==",
-            "dev": true,
-            "dependencies": {
-                "glob": "^10.3.7"
-            },
-            "bin": {
-                "rimraf": "dist/esm/bin.mjs"
-            },
-            "engines": {
-                "node": ">=14.18"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/isaacs"
-            }
-        },
         "node_modules/@tapjs/run/node_modules/tcompare": {
             "version": "7.0.1",
             "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-7.0.1.tgz",
@@ -1468,24 +1383,6 @@
                 "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/@tapjs/test/node_modules/rimraf": {
-            "version": "5.0.7",
-            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz",
-            "integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==",
-            "dev": true,
-            "dependencies": {
-                "glob": "^10.3.7"
-            },
-            "bin": {
-                "rimraf": "dist/esm/bin.mjs"
-            },
-            "engines": {
-                "node": ">=14.18"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/isaacs"
-            }
-        },
         "node_modules/@tapjs/typescript": {
             "version": "1.4.6",
             "resolved": "https://registry.npmjs.org/@tapjs/typescript/-/typescript-1.4.6.tgz",
@@ -1788,12 +1685,13 @@
             }
         },
         "node_modules/braces": {
-            "version": "3.0.2",
-            "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-            "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+            "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "fill-range": "^7.0.1"
+                "fill-range": "^7.1.1"
             },
             "engines": {
                 "node": ">=8"
@@ -2236,14 +2134,12 @@
         "node_modules/eastasianwidth": {
             "version": "0.2.0",
             "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
-            "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
-            "dev": true
+            "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
         },
         "node_modules/emoji-regex": {
             "version": "9.2.2",
             "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
-            "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
-            "dev": true
+            "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
         },
         "node_modules/encoding": {
             "version": "0.1.13",
@@ -2473,10 +2369,11 @@
             }
         },
         "node_modules/fill-range": {
-            "version": "7.0.1",
-            "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-            "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+            "version": "7.1.1",
+            "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+            "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "to-regex-range": "^5.0.1"
             },
@@ -2511,6 +2408,22 @@
                 "node": "^10.12.0 || >=12.0.0"
             }
         },
+        "node_modules/flat-cache/node_modules/rimraf": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+            "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+            "deprecated": "Rimraf versions prior to v4 are no longer supported",
+            "license": "ISC",
+            "dependencies": {
+                "glob": "^7.1.3"
+            },
+            "bin": {
+                "rimraf": "bin.js"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            }
+        },
         "node_modules/flatted": {
             "version": "3.2.5",
             "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz",
@@ -2525,7 +2438,6 @@
             "version": "3.1.1",
             "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
             "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
-            "dev": true,
             "dependencies": {
                 "cross-spawn": "^7.0.0",
                 "signal-exit": "^4.0.1"
@@ -3050,6 +2962,7 @@
             "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
             "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
             "dev": true,
+            "license": "MIT",
             "engines": {
                 "node": ">=0.12.0"
             }
@@ -3125,7 +3038,6 @@
             "version": "3.1.2",
             "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz",
             "integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==",
-            "dev": true,
             "dependencies": {
                 "@isaacs/cliui": "^8.0.2"
             },
@@ -3257,7 +3169,6 @@
             "version": "10.2.2",
             "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz",
             "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==",
-            "dev": true,
             "engines": {
                 "node": "14 || >=16.14"
             }
@@ -3341,7 +3252,6 @@
             "version": "7.1.2",
             "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
             "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
-            "dev": true,
             "engines": {
                 "node": ">=16 || 14 >=14.17"
             }
@@ -3975,7 +3885,6 @@
             "version": "1.11.1",
             "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
             "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
-            "dev": true,
             "dependencies": {
                 "lru-cache": "^10.2.0",
                 "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
@@ -4411,14 +4320,64 @@
             }
         },
         "node_modules/rimraf": {
-            "version": "3.0.2",
-            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
-            "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+            "version": "5.0.7",
+            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz",
+            "integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==",
+            "license": "ISC",
             "dependencies": {
-                "glob": "^7.1.3"
+                "glob": "^10.3.7"
             },
             "bin": {
-                "rimraf": "bin.js"
+                "rimraf": "dist/esm/bin.mjs"
+            },
+            "engines": {
+                "node": ">=14.18"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            }
+        },
+        "node_modules/rimraf/node_modules/brace-expansion": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+            "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+            "license": "MIT",
+            "dependencies": {
+                "balanced-match": "^1.0.0"
+            }
+        },
+        "node_modules/rimraf/node_modules/glob": {
+            "version": "10.4.1",
+            "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz",
+            "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==",
+            "license": "ISC",
+            "dependencies": {
+                "foreground-child": "^3.1.0",
+                "jackspeak": "^3.1.2",
+                "minimatch": "^9.0.4",
+                "minipass": "^7.1.2",
+                "path-scurry": "^1.11.1"
+            },
+            "bin": {
+                "glob": "dist/esm/bin.mjs"
+            },
+            "engines": {
+                "node": ">=16 || 14 >=14.18"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            }
+        },
+        "node_modules/rimraf/node_modules/minimatch": {
+            "version": "9.0.4",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
+            "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
+            "license": "ISC",
+            "dependencies": {
+                "brace-expansion": "^2.0.1"
+            },
+            "engines": {
+                "node": ">=16 || 14 >=14.17"
             },
             "funding": {
                 "url": "https://github.com/sponsors/isaacs"
@@ -4497,7 +4456,6 @@
             "version": "4.1.0",
             "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
             "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
-            "dev": true,
             "engines": {
                 "node": ">=14"
             },
@@ -4705,7 +4663,6 @@
             "version": "5.1.2",
             "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
             "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
-            "dev": true,
             "dependencies": {
                 "eastasianwidth": "^0.2.0",
                 "emoji-regex": "^9.2.2",
@@ -4723,7 +4680,6 @@
             "version": "4.2.3",
             "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
             "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-            "dev": true,
             "dependencies": {
                 "emoji-regex": "^8.0.0",
                 "is-fullwidth-code-point": "^3.0.0",
@@ -4736,14 +4692,12 @@
         "node_modules/string-width-cjs/node_modules/emoji-regex": {
             "version": "8.0.0",
             "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
-            "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
-            "dev": true
+            "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
         },
         "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": {
             "version": "3.0.0",
             "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
             "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-            "dev": true,
             "engines": {
                 "node": ">=8"
             }
@@ -4752,7 +4706,6 @@
             "version": "6.0.1",
             "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
             "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
-            "dev": true,
             "engines": {
                 "node": ">=12"
             },
@@ -4764,7 +4717,6 @@
             "version": "7.1.0",
             "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
             "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
-            "dev": true,
             "dependencies": {
                 "ansi-regex": "^6.0.1"
             },
@@ -4791,7 +4743,6 @@
             "version": "6.0.1",
             "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
             "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-            "dev": true,
             "dependencies": {
                 "ansi-regex": "^5.0.1"
             },
@@ -4893,24 +4844,6 @@
                 "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/sync-content/node_modules/rimraf": {
-            "version": "5.0.7",
-            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz",
-            "integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==",
-            "dev": true,
-            "dependencies": {
-                "glob": "^10.3.7"
-            },
-            "bin": {
-                "rimraf": "dist/esm/bin.mjs"
-            },
-            "engines": {
-                "node": ">=14.18"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/isaacs"
-            }
-        },
         "node_modules/tap": {
             "version": "19.0.2",
             "resolved": "https://registry.npmjs.org/tap/-/tap-19.0.2.tgz",
@@ -5075,6 +5008,7 @@
             "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
             "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "is-number": "^7.0.0"
             },
@@ -5137,28 +5071,6 @@
                 "url": "https://github.com/chalk/chalk?sponsor=1"
             }
         },
-        "node_modules/tshy/node_modules/glob": {
-            "version": "10.4.1",
-            "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz",
-            "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==",
-            "dev": true,
-            "dependencies": {
-                "foreground-child": "^3.1.0",
-                "jackspeak": "^3.1.2",
-                "minimatch": "^9.0.4",
-                "minipass": "^7.1.2",
-                "path-scurry": "^1.11.1"
-            },
-            "bin": {
-                "glob": "dist/esm/bin.mjs"
-            },
-            "engines": {
-                "node": ">=16 || 14 >=14.18"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/isaacs"
-            }
-        },
         "node_modules/tshy/node_modules/minimatch": {
             "version": "9.0.4",
             "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
@@ -5174,24 +5086,6 @@
                 "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/tshy/node_modules/rimraf": {
-            "version": "5.0.7",
-            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz",
-            "integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==",
-            "dev": true,
-            "dependencies": {
-                "glob": "^10.3.7"
-            },
-            "bin": {
-                "rimraf": "dist/esm/bin.mjs"
-            },
-            "engines": {
-                "node": ">=14.18"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/isaacs"
-            }
-        },
         "node_modules/tslib": {
             "version": "2.6.2",
             "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
@@ -5390,7 +5284,6 @@
             "version": "8.1.0",
             "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
             "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
-            "dev": true,
             "dependencies": {
                 "ansi-styles": "^6.1.0",
                 "string-width": "^5.0.1",
@@ -5408,7 +5301,6 @@
             "version": "7.0.0",
             "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
             "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
-            "dev": true,
             "dependencies": {
                 "ansi-styles": "^4.0.0",
                 "string-width": "^4.1.0",
@@ -5424,14 +5316,12 @@
         "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
             "version": "8.0.0",
             "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
-            "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
-            "dev": true
+            "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
         },
         "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": {
             "version": "3.0.0",
             "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
             "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-            "dev": true,
             "engines": {
                 "node": ">=8"
             }
@@ -5440,7 +5330,6 @@
             "version": "4.2.3",
             "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
             "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-            "dev": true,
             "dependencies": {
                 "emoji-regex": "^8.0.0",
                 "is-fullwidth-code-point": "^3.0.0",
@@ -5454,7 +5343,6 @@
             "version": "6.0.1",
             "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
             "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
-            "dev": true,
             "engines": {
                 "node": ">=12"
             },
@@ -5466,7 +5354,6 @@
             "version": "6.2.1",
             "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
             "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
-            "dev": true,
             "engines": {
                 "node": ">=12"
             },
@@ -5478,7 +5365,6 @@
             "version": "7.1.0",
             "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
             "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
-            "dev": true,
             "dependencies": {
                 "ansi-regex": "^6.0.1"
             },
@@ -5495,10 +5381,11 @@
             "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
         },
         "node_modules/ws": {
-            "version": "8.17.0",
-            "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
-            "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
+            "version": "8.17.1",
+            "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+            "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
             "dev": true,
+            "license": "MIT",
             "engines": {
                 "node": ">=10.0.0"
             },
@@ -5727,7 +5614,6 @@
             "version": "8.0.2",
             "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
             "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
-            "dev": true,
             "requires": {
                 "string-width": "^5.1.2",
                 "string-width-cjs": "npm:string-width@^4.2.0",
@@ -5740,14 +5626,12 @@
                 "ansi-regex": {
                     "version": "6.0.1",
                     "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
-                    "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
-                    "dev": true
+                    "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA=="
                 },
                 "strip-ansi": {
                     "version": "7.1.0",
                     "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
                     "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
-                    "dev": true,
                     "requires": {
                         "ansi-regex": "^6.0.1"
                     }
@@ -6062,7 +5946,6 @@
             "version": "0.11.0",
             "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
             "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
-            "dev": true,
             "optional": true
         },
         "@sigstore/bundle": {
@@ -6260,48 +6143,6 @@
             "requires": {
                 "mkdirp": "^3.0.0",
                 "rimraf": "^5.0.5"
-            },
-            "dependencies": {
-                "brace-expansion": {
-                    "version": "2.0.1",
-                    "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
-                    "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
-                    "dev": true,
-                    "requires": {
-                        "balanced-match": "^1.0.0"
-                    }
-                },
-                "glob": {
-                    "version": "10.4.1",
-                    "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz",
-                    "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==",
-                    "dev": true,
-                    "requires": {
-                        "foreground-child": "^3.1.0",
-                        "jackspeak": "^3.1.2",
-                        "minimatch": "^9.0.4",
-                        "minipass": "^7.1.2",
-                        "path-scurry": "^1.11.1"
-                    }
-                },
-                "minimatch": {
-                    "version": "9.0.4",
-                    "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
-                    "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
-                    "dev": true,
-                    "requires": {
-                        "brace-expansion": "^2.0.1"
-                    }
-                },
-                "rimraf": {
-                    "version": "5.0.7",
-                    "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz",
-                    "integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==",
-                    "dev": true,
-                    "requires": {
-                        "glob": "^10.3.7"
-                    }
-                }
             }
         },
         "@tapjs/intercept": {
@@ -6471,15 +6312,6 @@
                         "brace-expansion": "^2.0.1"
                     }
                 },
-                "rimraf": {
-                    "version": "5.0.7",
-                    "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz",
-                    "integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==",
-                    "dev": true,
-                    "requires": {
-                        "glob": "^10.3.7"
-                    }
-                },
                 "tcompare": {
                     "version": "7.0.1",
                     "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-7.0.1.tgz",
@@ -6608,15 +6440,6 @@
                     "requires": {
                         "brace-expansion": "^2.0.1"
                     }
-                },
-                "rimraf": {
-                    "version": "5.0.7",
-                    "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz",
-                    "integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==",
-                    "dev": true,
-                    "requires": {
-                        "glob": "^10.3.7"
-                    }
                 }
             }
         },
@@ -6846,12 +6669,12 @@
             }
         },
         "braces": {
-            "version": "3.0.2",
-            "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-            "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+            "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
             "dev": true,
             "requires": {
-                "fill-range": "^7.0.1"
+                "fill-range": "^7.1.1"
             }
         },
         "c8": {
@@ -7169,14 +6992,12 @@
         "eastasianwidth": {
             "version": "0.2.0",
             "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
-            "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
-            "dev": true
+            "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
         },
         "emoji-regex": {
             "version": "9.2.2",
             "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
-            "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
-            "dev": true
+            "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
         },
         "encoding": {
             "version": "0.1.13",
@@ -7352,9 +7173,9 @@
             }
         },
         "fill-range": {
-            "version": "7.0.1",
-            "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-            "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+            "version": "7.1.1",
+            "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+            "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
             "dev": true,
             "requires": {
                 "to-regex-range": "^5.0.1"
@@ -7376,6 +7197,16 @@
             "requires": {
                 "flatted": "^3.1.0",
                 "rimraf": "^3.0.2"
+            },
+            "dependencies": {
+                "rimraf": {
+                    "version": "3.0.2",
+                    "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+                    "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+                    "requires": {
+                        "glob": "^7.1.3"
+                    }
+                }
             }
         },
         "flatted": {
@@ -7392,7 +7223,6 @@
             "version": "3.1.1",
             "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
             "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
-            "dev": true,
             "requires": {
                 "cross-spawn": "^7.0.0",
                 "signal-exit": "^4.0.1"
@@ -7821,7 +7651,6 @@
             "version": "3.1.2",
             "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz",
             "integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==",
-            "dev": true,
             "requires": {
                 "@isaacs/cliui": "^8.0.2",
                 "@pkgjs/parseargs": "^0.11.0"
@@ -7919,8 +7748,7 @@
         "lru-cache": {
             "version": "10.2.2",
             "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz",
-            "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==",
-            "dev": true
+            "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ=="
         },
         "make-dir": {
             "version": "4.0.0",
@@ -7979,8 +7807,7 @@
         "minipass": {
             "version": "7.1.2",
             "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
-            "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
-            "dev": true
+            "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="
         },
         "minipass-collect": {
             "version": "2.0.1",
@@ -8448,7 +8275,6 @@
             "version": "1.11.1",
             "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
             "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
-            "dev": true,
             "requires": {
                 "lru-cache": "^10.2.0",
                 "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
@@ -8746,11 +8572,41 @@
             "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="
         },
         "rimraf": {
-            "version": "3.0.2",
-            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
-            "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+            "version": "5.0.7",
+            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz",
+            "integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==",
             "requires": {
-                "glob": "^7.1.3"
+                "glob": "^10.3.7"
+            },
+            "dependencies": {
+                "brace-expansion": {
+                    "version": "2.0.1",
+                    "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+                    "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+                    "requires": {
+                        "balanced-match": "^1.0.0"
+                    }
+                },
+                "glob": {
+                    "version": "10.4.1",
+                    "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz",
+                    "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==",
+                    "requires": {
+                        "foreground-child": "^3.1.0",
+                        "jackspeak": "^3.1.2",
+                        "minimatch": "^9.0.4",
+                        "minipass": "^7.1.2",
+                        "path-scurry": "^1.11.1"
+                    }
+                },
+                "minimatch": {
+                    "version": "9.0.4",
+                    "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
+                    "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
+                    "requires": {
+                        "brace-expansion": "^2.0.1"
+                    }
+                }
             }
         },
         "run-parallel": {
@@ -8799,8 +8655,7 @@
         "signal-exit": {
             "version": "4.1.0",
             "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
-            "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
-            "dev": true
+            "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="
         },
         "sigstore": {
             "version": "2.3.1",
@@ -8955,7 +8810,6 @@
             "version": "5.1.2",
             "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
             "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
-            "dev": true,
             "requires": {
                 "eastasianwidth": "^0.2.0",
                 "emoji-regex": "^9.2.2",
@@ -8965,14 +8819,12 @@
                 "ansi-regex": {
                     "version": "6.0.1",
                     "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
-                    "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
-                    "dev": true
+                    "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA=="
                 },
                 "strip-ansi": {
                     "version": "7.1.0",
                     "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
                     "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
-                    "dev": true,
                     "requires": {
                         "ansi-regex": "^6.0.1"
                     }
@@ -8983,7 +8835,6 @@
             "version": "npm:string-width@4.2.3",
             "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
             "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-            "dev": true,
             "requires": {
                 "emoji-regex": "^8.0.0",
                 "is-fullwidth-code-point": "^3.0.0",
@@ -8993,14 +8844,12 @@
                 "emoji-regex": {
                     "version": "8.0.0",
                     "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
-                    "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
-                    "dev": true
+                    "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
                 },
                 "is-fullwidth-code-point": {
                     "version": "3.0.0",
                     "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
-                    "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-                    "dev": true
+                    "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
                 }
             }
         },
@@ -9016,7 +8865,6 @@
             "version": "npm:strip-ansi@6.0.1",
             "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
             "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-            "dev": true,
             "requires": {
                 "ansi-regex": "^5.0.1"
             }
@@ -9081,15 +8929,6 @@
                     "requires": {
                         "brace-expansion": "^2.0.1"
                     }
-                },
-                "rimraf": {
-                    "version": "5.0.7",
-                    "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz",
-                    "integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==",
-                    "dev": true,
-                    "requires": {
-                        "glob": "^10.3.7"
-                    }
                 }
             }
         },
@@ -9263,19 +9102,6 @@
                     "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
                     "dev": true
                 },
-                "glob": {
-                    "version": "10.4.1",
-                    "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz",
-                    "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==",
-                    "dev": true,
-                    "requires": {
-                        "foreground-child": "^3.1.0",
-                        "jackspeak": "^3.1.2",
-                        "minimatch": "^9.0.4",
-                        "minipass": "^7.1.2",
-                        "path-scurry": "^1.11.1"
-                    }
-                },
                 "minimatch": {
                     "version": "9.0.4",
                     "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
@@ -9284,15 +9110,6 @@
                     "requires": {
                         "brace-expansion": "^2.0.1"
                     }
-                },
-                "rimraf": {
-                    "version": "5.0.7",
-                    "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz",
-                    "integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==",
-                    "dev": true,
-                    "requires": {
-                        "glob": "^10.3.7"
-                    }
                 }
             }
         },
@@ -9447,7 +9264,6 @@
             "version": "8.1.0",
             "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
             "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
-            "dev": true,
             "requires": {
                 "ansi-styles": "^6.1.0",
                 "string-width": "^5.0.1",
@@ -9457,20 +9273,17 @@
                 "ansi-regex": {
                     "version": "6.0.1",
                     "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
-                    "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
-                    "dev": true
+                    "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA=="
                 },
                 "ansi-styles": {
                     "version": "6.2.1",
                     "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
-                    "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
-                    "dev": true
+                    "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="
                 },
                 "strip-ansi": {
                     "version": "7.1.0",
                     "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
                     "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
-                    "dev": true,
                     "requires": {
                         "ansi-regex": "^6.0.1"
                     }
@@ -9481,7 +9294,6 @@
             "version": "npm:wrap-ansi@7.0.0",
             "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
             "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
-            "dev": true,
             "requires": {
                 "ansi-styles": "^4.0.0",
                 "string-width": "^4.1.0",
@@ -9491,20 +9303,17 @@
                 "emoji-regex": {
                     "version": "8.0.0",
                     "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
-                    "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
-                    "dev": true
+                    "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
                 },
                 "is-fullwidth-code-point": {
                     "version": "3.0.0",
                     "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
-                    "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-                    "dev": true
+                    "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
                 },
                 "string-width": {
                     "version": "4.2.3",
                     "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
                     "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-                    "dev": true,
                     "requires": {
                         "emoji-regex": "^8.0.0",
                         "is-fullwidth-code-point": "^3.0.0",
@@ -9519,9 +9328,9 @@
             "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
         },
         "ws": {
-            "version": "8.17.0",
-            "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
-            "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
+            "version": "8.17.1",
+            "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+            "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
             "dev": true,
             "requires": {}
         },
diff --git a/package.json b/package.json
index 95fdafba..5ae6772f 100644
--- a/package.json
+++ b/package.json
@@ -68,6 +68,7 @@
         "js-yaml": "^4.1.0",
         "marked": "^10.0.0",
         "msgpackr": "^1.10.2",
+        "rimraf": "^5.0.7",
         "striptags": "^4.0.0-alpha.4",
         "word-wrap": "^1.2.3"
     },
diff --git a/src/content/dependencies/generateAlbumReleaseInfo.js b/src/content/dependencies/generateAlbumReleaseInfo.js
index 1cd638ce..26e2e160 100644
--- a/src/content/dependencies/generateAlbumReleaseInfo.js
+++ b/src/content/dependencies/generateAlbumReleaseInfo.js
@@ -1,4 +1,4 @@
-import {accumulateSum} from '#sugar';
+import {accumulateSum, empty} from '#sugar';
 
 export default {
   contentDependencies: [
@@ -41,8 +41,18 @@ export default {
       data.coverArtDate = album.coverArtDate;
     }
 
-    data.duration = accumulateSum(album.tracks, track => track.duration);
-    data.durationApproximate = album.tracks.length > 1;
+    const durationTerms =
+      album.tracks
+        .map(track => track.duration)
+        .filter(value => value > 0);
+
+    if (empty(durationTerms)) {
+      data.duration = null;
+      data.durationApproximate = null;
+    } else {
+      data.duration = accumulateSum(durationTerms);
+      data.durationApproximate = album.tracks.length > 1;
+    }
 
     data.numTracks = album.tracks.length;
 
diff --git a/src/static/js/search-worker.js b/src/static/js/search-worker.js
index 8e83fe02..8d987a74 100644
--- a/src/static/js/search-worker.js
+++ b/src/static/js/search-worker.js
@@ -409,6 +409,7 @@ function queryGenericIndex(index, query, options) {
     // flexsearch matching multiple field values in a single query.
     ['artTags', 'artTags'],
 
+    ['contributors', 'parentName'],
     ['contributors', 'groups'],
     ['primaryName', 'contributors'],
     ['primaryName'],
diff --git a/src/url-spec.js b/src/url-spec.js
index dc4673e5..64423a52 100644
--- a/src/url-spec.js
+++ b/src/url-spec.js
@@ -4,7 +4,7 @@ import {withEntries} from '#sugar';
 // part of a build. This is so that multiple builds of a wiki can coexist
 // served from the same server / file system root: older builds' HTML files
 // refer to earlier values of STATIC_VERSION, avoiding name collisions.
-const STATIC_VERSION = '2p2';
+const STATIC_VERSION = '3r2';
 
 const genericPaths = {
   root: '',
diff --git a/src/util/search-spec.js b/src/util/search-spec.js
index 79e8ee95..bc24e1a1 100644
--- a/src/util/search-spec.js
+++ b/src/util/search-spec.js
@@ -118,12 +118,15 @@ export const searchSpec = {
       fields.primaryName =
         thing.name;
 
+      const kind =
+        thing.constructor[Symbol.for('Thing.referenceType')];
+
       fields.parentName =
-        (fields.kind === 'track'
+        (kind === 'track'
           ? thing.album.name
-       : fields.kind === 'group'
+       : kind === 'group'
           ? thing.category.name
-       : fields.kind === 'flash'
+       : kind === 'flash'
           ? thing.act.name
           : null);
 
diff --git a/src/web-routes.js b/src/web-routes.js
index 7e08d06f..762b26c3 100644
--- a/src/web-routes.js
+++ b/src/web-routes.js
@@ -18,21 +18,25 @@ export const stationaryCodeRoutes = [
   {
     from: path.join(codeSrcPath, 'static', 'css'),
     to: ['staticCSS.root'],
+    statically: 'copy',
   },
 
   {
     from: path.join(codeSrcPath, 'static', 'js'),
     to: ['staticJS.root'],
+    statically: 'copy',
   },
 
   {
     from: path.join(codeSrcPath, 'static', 'misc'),
     to: ['staticMisc.root'],
+    statically: 'copy',
   },
 
   {
     from: path.join(codeSrcPath, 'util'),
     to: ['staticSharedUtil.root'],
+    statically: 'copy',
   },
 ];
 
@@ -50,6 +54,8 @@ function quickNodeDependency({
           : root),
 
       to: ['staticLib.path', name],
+
+      statically: 'copy',
     },
   ];
 }
@@ -86,8 +92,17 @@ export async function identifyDynamicWebRoutes({
 }) {
   const routeFunctions = [
     () => Promise.resolve([
-      {from: path.resolve(mediaPath), to: ['media.root']},
-      {from: path.resolve(mediaCachePath), to: ['thumb.root']},
+      {
+        from: path.resolve(mediaPath),
+        to: ['media.root'],
+        statically: 'symlink',
+      },
+
+      {
+        from: path.resolve(mediaCachePath),
+        to: ['thumb.root'],
+        statically: 'symlink',
+      },
     ]),
 
     () => {
@@ -98,7 +113,12 @@ export async function identifyDynamicWebRoutes({
 
       return (
         readdir(from).then(
-          () => [{from, to: ['searchData.root']}],
+          () => [
+            {
+              from,
+              to: ['searchData.root'],
+              statically: 'copy',
+            }],
           () => []));
     },
   ];
diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js
index 1ab0604e..86e3da0f 100644
--- a/src/write/build-modes/static-build.js
+++ b/src/write/build-modes/static-build.js
@@ -2,6 +2,7 @@ import * as path from 'node:path';
 
 import {
   copyFile,
+  cp,
   mkdir,
   stat,
   symlink,
@@ -9,6 +10,8 @@ import {
   unlink,
 } from 'node:fs/promises';
 
+import {rimraf} from 'rimraf';
+
 import {quickLoadContentDependencies} from '#content-dependencies';
 import {quickEvaluate} from '#content-function';
 import * as html from '#html';
@@ -159,6 +162,11 @@ export async function go({
     webRoutes,
   });
 
+  await writeWebRouteCopies({
+    outputPath,
+    webRoutes,
+  });
+
   if (writeAll) {
     await writeFavicon({
       mediaPath,
@@ -438,8 +446,11 @@ function writeWebRouteSymlinks({
   outputPath,
   webRoutes,
 }) {
+  const symlinkRoutes =
+    webRoutes.filter(route => route.statically === 'symlink');
+
   const promises =
-    webRoutes.map(async route => {
+    symlinkRoutes.map(async route => {
       const parts = route.to.split('/');
       const parentDirectoryParts = parts.slice(0, -1);
       const symlinkNamePart = parts.at(-1);
@@ -471,6 +482,113 @@ function writeWebRouteSymlinks({
   return progressPromiseAll(`Writing web route symlinks.`, promises);
 }
 
+async function writeWebRouteCopies({
+  outputPath,
+  webRoutes,
+}) {
+  const copyRoutes =
+    webRoutes.filter(route => route.statically === 'copy');
+
+  const promises =
+    copyRoutes.map(async route => {
+      const permissionName = '__hsmusic-ok-for-deletion.txt';
+
+      const parts = route.to.split('/');
+      const parentDirectoryParts = parts.slice(0, -1);
+      const copyNamePart = parts.at(-1);
+
+      const parentDirectory = path.join(outputPath, ...parentDirectoryParts);
+      const copyPath = path.join(parentDirectory, copyNamePart);
+
+      // We're going to do a rimraf call! This is freaking terrifying,
+      // so nope out on a couple important conditions.
+
+      let needsDelete;
+      try {
+        await stat(copyPath);
+        needsDelete = true;
+      } catch (error) {
+        if (error.code === 'ENOENT') {
+          needsDelete = false;
+        } else {
+          throw error;
+        }
+      }
+
+      if (needsDelete) {
+        // First remove it directly, in case it's a symlink.
+        try {
+          await unlink(copyPath);
+          needsDelete = false;
+        } catch (error) {
+          // EPERM is POSIX, but libuv may or may not flat-out just raise
+          // the system error (which is ostensibly EISDIR on Linux).
+          // https://github.com/nodejs/node-v0.x-archive/issues/5791
+          // https://man7.org/linux/man-pages/man2/unlink.2.html
+          //
+          // Both of these indidcate "a directory, probably" and we'll
+          // still check for the deletion permission file where we expect
+          // it before actually touching anything.
+          if (error.code !== 'EPERM' && error.code !== 'EISDIR') {
+            throw error;
+          }
+        }
+      }
+
+      if (needsDelete) {
+        // Then check that the deletion permission file exists
+        // where we expect it.
+        try {
+          await stat(path.join(copyPath, permissionName));
+        } catch (error) {
+          if (error.code === 'ENOENT') {
+            throw new Error(`Couldn't find ${permissionName} in ${copyPath} - please delete or move away this folder manually`);
+          } else {
+            throw error;
+          }
+        }
+
+        // And *then* actually delete that directory.
+        await rimraf(copyPath);
+      }
+
+      // Actually copy the source path where it's wanted.
+      await cp(route.from, copyPath, {recursive: true});
+
+      // And certify that it's OK to delete this path, next time around.
+      await writeFile(path.join(copyPath, permissionName),
+        `The presence of this file (by its name, not its contents)\n` +
+        `indicates hsmusic may delete everything contained in this\n` +
+        `directory (the one which directly contains this file, *not*\n` +
+        `any further-up parent directories).\n` +
+        `\n` +
+        `If you make edits, or add any files, they will be deleted or\n` +
+        `overwritten the next time you run the build.\n` +
+        `\n` +
+        `If you delete *this* file, hsmusic will error during the next\n` +
+        `build, and will ask that you delete the containing directory\n` +
+        `yourself.\n`);
+    });
+
+  const results =
+    await Promise.allSettled(promises);
+
+  const errors =
+    results
+      .filter(({status}) => status === 'rejected')
+      .map(({reason}) => reason)
+      .map(err =>
+        (err.message.startsWith(`Couldn't find`)
+          ? err.message
+          : err));
+
+  if (empty(errors)) {
+    logInfo`Wrote web route copies.`;
+  } else {
+    throw new AggregateError(errors, `Errors copying internal files ("web routes")`);
+  }
+}
+
 async function writeFavicon({
   mediaPath,
   outputPath,
diff --git a/tap-snapshots/test/snapshot/generateAlbumTrackList.js.test.cjs b/tap-snapshots/test/snapshot/generateAlbumTrackList.js.test.cjs
index b338f293..091f1b9f 100644
--- a/tap-snapshots/test/snapshot/generateAlbumTrackList.js.test.cjs
+++ b/tap-snapshots/test/snapshot/generateAlbumTrackList.js.test.cjs
@@ -16,7 +16,10 @@ exports[`test/snapshot/generateAlbumTrackList.js > TAP > generateAlbumTrackList
 
 exports[`test/snapshot/generateAlbumTrackList.js > TAP > generateAlbumTrackList (snapshot) > basic behavior, with track sections 1`] = `
 <dl class="album-group-list">
-    <dt class="content-heading" tabindex="0"><span class="content-heading-main-title">First section: (~1:00)</span></dt>
+    <dt class="content-heading" tabindex="0">
+        <span class="content-heading-main-title">First section: (~1:00)</span>
+        <template class="content-heading-sticky-title">First section:</template>
+    </dt>
     <dd>
         <ul>
             <li>(0:20) <a href="track/t1/">Track 1</a></li>
@@ -24,14 +27,20 @@ exports[`test/snapshot/generateAlbumTrackList.js > TAP > generateAlbumTrackList
             <li>(0:40) <a href="track/t3/">Track 3</a></li>
         </ul>
     </dd>
-    <dt class="content-heading" tabindex="0"><span class="content-heading-main-title">Second section:</span></dt>
+    <dt class="content-heading" tabindex="0">
+        <span class="content-heading-main-title">Second section:</span>
+        <template class="content-heading-sticky-title">Second section:</template>
+    </dt>
     <dd><ul><li style="--primary-color: #ea2e83">[mocked: generateAlbumTrackListMissingDuration - slots: {}] <a href="track/t4/">Track 4</a> <span class="by"><span class="chunkwrap">by <a href="artist/apricot/">Apricot</a>,</span> <span class="chunkwrap"><a href="artist/peach/">Peach</a>,</span> <span class="chunkwrap">and <a href="artist/cerise/">Cerise</a></span></span></li></ul></dd>
 </dl>
 `
 
 exports[`test/snapshot/generateAlbumTrackList.js > TAP > generateAlbumTrackList (snapshot) > collapseDurationScope: album 1`] = `
 <dl class="album-group-list">
-    <dt class="content-heading" tabindex="0"><span class="content-heading-main-title">First section: (~1:00)</span></dt>
+    <dt class="content-heading" tabindex="0">
+        <span class="content-heading-main-title">First section: (~1:00)</span>
+        <template class="content-heading-sticky-title">First section:</template>
+    </dt>
     <dd>
         <ul>
             <li>(0:20) <a href="track/t1/">Track 1</a></li>
@@ -39,7 +48,10 @@ exports[`test/snapshot/generateAlbumTrackList.js > TAP > generateAlbumTrackList
             <li>(0:40) <a href="track/t3/">Track 3</a></li>
         </ul>
     </dd>
-    <dt class="content-heading" tabindex="0"><span class="content-heading-main-title">Second section:</span></dt>
+    <dt class="content-heading" tabindex="0">
+        <span class="content-heading-main-title">Second section:</span>
+        <template class="content-heading-sticky-title">Second section:</template>
+    </dt>
     <dd><ul><li style="--primary-color: #ea2e83">[mocked: generateAlbumTrackListMissingDuration - slots: {}] <a href="track/t4/">Track 4</a> <span class="by"><span class="chunkwrap">by <a href="artist/apricot/">Apricot</a>,</span> <span class="chunkwrap"><a href="artist/peach/">Peach</a>,</span> <span class="chunkwrap">and <a href="artist/cerise/">Cerise</a></span></span></li></ul></dd>
 </dl>
 <ul>
@@ -56,7 +68,10 @@ exports[`test/snapshot/generateAlbumTrackList.js > TAP > generateAlbumTrackList
 
 exports[`test/snapshot/generateAlbumTrackList.js > TAP > generateAlbumTrackList (snapshot) > collapseDurationScope: never 1`] = `
 <dl class="album-group-list">
-    <dt class="content-heading" tabindex="0"><span class="content-heading-main-title">First section: (~1:00)</span></dt>
+    <dt class="content-heading" tabindex="0">
+        <span class="content-heading-main-title">First section: (~1:00)</span>
+        <template class="content-heading-sticky-title">First section:</template>
+    </dt>
     <dd>
         <ul>
             <li>(0:20) <a href="track/t1/">Track 1</a></li>
@@ -64,7 +79,10 @@ exports[`test/snapshot/generateAlbumTrackList.js > TAP > generateAlbumTrackList
             <li>(0:40) <a href="track/t3/">Track 3</a></li>
         </ul>
     </dd>
-    <dt class="content-heading" tabindex="0"><span class="content-heading-main-title">Second section:</span></dt>
+    <dt class="content-heading" tabindex="0">
+        <span class="content-heading-main-title">Second section:</span>
+        <template class="content-heading-sticky-title">Second section:</template>
+    </dt>
     <dd><ul><li style="--primary-color: #ea2e83">[mocked: generateAlbumTrackListMissingDuration - slots: {}] <a href="track/t4/">Track 4</a> <span class="by"><span class="chunkwrap">by <a href="artist/apricot/">Apricot</a>,</span> <span class="chunkwrap"><a href="artist/peach/">Peach</a>,</span> <span class="chunkwrap">and <a href="artist/cerise/">Cerise</a></span></span></li></ul></dd>
 </dl>
 <ul>
@@ -81,7 +99,10 @@ exports[`test/snapshot/generateAlbumTrackList.js > TAP > generateAlbumTrackList
 
 exports[`test/snapshot/generateAlbumTrackList.js > TAP > generateAlbumTrackList (snapshot) > collapseDurationScope: section 1`] = `
 <dl class="album-group-list">
-    <dt class="content-heading" tabindex="0"><span class="content-heading-main-title">First section: (~1:00)</span></dt>
+    <dt class="content-heading" tabindex="0">
+        <span class="content-heading-main-title">First section: (~1:00)</span>
+        <template class="content-heading-sticky-title">First section:</template>
+    </dt>
     <dd>
         <ul>
             <li>(0:20) <a href="track/t1/">Track 1</a></li>
@@ -89,7 +110,10 @@ exports[`test/snapshot/generateAlbumTrackList.js > TAP > generateAlbumTrackList
             <li>(0:40) <a href="track/t3/">Track 3</a></li>
         </ul>
     </dd>
-    <dt class="content-heading" tabindex="0"><span class="content-heading-main-title">Second section:</span></dt>
+    <dt class="content-heading" tabindex="0">
+        <span class="content-heading-main-title">Second section:</span>
+        <template class="content-heading-sticky-title">Second section:</template>
+    </dt>
     <dd><ul><li style="--primary-color: #ea2e83"><a href="track/t4/">Track 4</a> <span class="by"><span class="chunkwrap">by <a href="artist/apricot/">Apricot</a>,</span> <span class="chunkwrap"><a href="artist/peach/">Peach</a>,</span> <span class="chunkwrap">and <a href="artist/cerise/">Cerise</a></span></span></li></ul></dd>
 </dl>
 <ul>
@@ -106,7 +130,10 @@ exports[`test/snapshot/generateAlbumTrackList.js > TAP > generateAlbumTrackList
 
 exports[`test/snapshot/generateAlbumTrackList.js > TAP > generateAlbumTrackList (snapshot) > collapseDurationScope: track 1`] = `
 <dl class="album-group-list">
-    <dt class="content-heading" tabindex="0"><span class="content-heading-main-title">First section: (~1:00)</span></dt>
+    <dt class="content-heading" tabindex="0">
+        <span class="content-heading-main-title">First section: (~1:00)</span>
+        <template class="content-heading-sticky-title">First section:</template>
+    </dt>
     <dd>
         <ul>
             <li>(0:20) <a href="track/t1/">Track 1</a></li>
@@ -114,7 +141,10 @@ exports[`test/snapshot/generateAlbumTrackList.js > TAP > generateAlbumTrackList
             <li>(0:40) <a href="track/t3/">Track 3</a></li>
         </ul>
     </dd>
-    <dt class="content-heading" tabindex="0"><span class="content-heading-main-title">Second section:</span></dt>
+    <dt class="content-heading" tabindex="0">
+        <span class="content-heading-main-title">Second section:</span>
+        <template class="content-heading-sticky-title">Second section:</template>
+    </dt>
     <dd><ul><li style="--primary-color: #ea2e83"><a href="track/t4/">Track 4</a> <span class="by"><span class="chunkwrap">by <a href="artist/apricot/">Apricot</a>,</span> <span class="chunkwrap"><a href="artist/peach/">Peach</a>,</span> <span class="chunkwrap">and <a href="artist/cerise/">Cerise</a></span></span></li></ul></dd>
 </dl>
 <ul>
diff --git a/test/lib/composite.js b/test/lib/composite.js
new file mode 100644
index 00000000..359d364d
--- /dev/null
+++ b/test/lib/composite.js
@@ -0,0 +1,33 @@
+import {compositeFrom} from '#composite';
+
+export function quickCheckCompositeOutputs(t, dependencies) {
+  return (step, outputDict) => {
+    t.same(
+      Object.keys(step.toDescription().outputs),
+      Object.keys(outputDict));
+
+    const composite = compositeFrom({
+      compose: false,
+      steps: [
+        step,
+
+        {
+          dependencies: Object.keys(outputDict),
+
+          // Access all dependencies by their expected keys -
+          // the composition runner actually provides a proxy
+          // and is checking that *we* access the dependencies
+          // we've specified.
+          compute: dependencies =>
+            Object.fromEntries(
+              Object.keys(outputDict)
+                .map(key => [key, dependencies[key]])),
+        },
+      ],
+    });
+
+    t.same(
+      composite.expose.compute(dependencies),
+      outputDict);
+  };
+}
diff --git a/test/lib/index.js b/test/lib/index.js
index 5fb5bf78..4c9ee23f 100644
--- a/test/lib/index.js
+++ b/test/lib/index.js
@@ -1,5 +1,6 @@
 Error.stackTraceLimit = Infinity;
 
+export * from './composite.js';
 export * from './content-function.js';
 export * from './generic-mock.js';
 export * from './wiki-data.js';
diff --git a/test/unit/data/composite/control-flow/withResultOfAvailabilityCheck.js b/test/unit/data/composite/control-flow/withResultOfAvailabilityCheck.js
index 2bcabb4f..9d588e4c 100644
--- a/test/unit/data/composite/control-flow/withResultOfAvailabilityCheck.js
+++ b/test/unit/data/composite/control-flow/withResultOfAvailabilityCheck.js
@@ -177,10 +177,11 @@ t.test(`withResultOfAvailabilityCheck: validate dynamic inputs`, t => {
       mode: 'banana',
     }),
     {message: `Error computing composition`, cause:
-      {message: `Error computing composition withResultOfAvailabilityCheck`, cause:
-        {message: `Errors in input values provided to withResultOfAvailabilityCheck`, errors: [
-          {message: `mode: Expected one of null empty falsy index, got banana`},
-        ]}}});
+      {message: `Error in step 1 of 2, withResultOfAvailabilityCheck`, cause:
+        {message: `Error computing composition withResultOfAvailabilityCheck`, cause:
+          {message: `Errors in input values provided to withResultOfAvailabilityCheck`, errors: [
+            {message: `mode: Expected one of null empty falsy index, got banana`},
+          ]}}}});
 
   t.throws(
     () => composite.expose.compute({
@@ -188,8 +189,9 @@ t.test(`withResultOfAvailabilityCheck: validate dynamic inputs`, t => {
       mode: null,
     }),
     {message: `Error computing composition`, cause:
-      {message: `Error computing composition withResultOfAvailabilityCheck`, cause:
-        {message: `Errors in input values provided to withResultOfAvailabilityCheck`, errors: [
-          {message: `mode: Expected a value, got null`},
-        ]}}});
+      {message: `Error in step 1 of 2, withResultOfAvailabilityCheck`, cause:
+        {message: `Error computing composition withResultOfAvailabilityCheck`, cause:
+          {message: `Errors in input values provided to withResultOfAvailabilityCheck`, errors: [
+            {message: `mode: Expected a value, got null`},
+          ]}}}});
 });
diff --git a/test/unit/data/composite/data/withPropertiesFromObject.js b/test/unit/data/composite/data/withPropertiesFromObject.js
index 750dc8c4..b81d51a5 100644
--- a/test/unit/data/composite/data/withPropertiesFromObject.js
+++ b/test/unit/data/composite/data/withPropertiesFromObject.js
@@ -1,4 +1,5 @@
 import t from 'tap';
+import {quickCheckCompositeOutputs} from '#test-lib';
 
 import {compositeFrom, input} from '#composite';
 import {exposeDependency} from '#composite/control-flow';
@@ -62,6 +63,8 @@ t.test(`withPropertiesFromObject: output shapes & values`, t => {
       ['foo', 'baz', 'missing3'],
   };
 
+  const qcco = quickCheckCompositeOutputs(t, dependencies);
+
   const mapLevel1 = [
     [input.value('prefix_value'), [
       ['object_dependency', [
@@ -153,28 +156,10 @@ t.test(`withPropertiesFromObject: output shapes & values`, t => {
           properties: propertiesInput,
         });
 
-        quickCheckOutputs(step, outputDict);
+        qcco(step, outputDict);
       }
     }
   }
-
-  function quickCheckOutputs(step, outputDict) {
-    t.same(
-      Object.keys(step.toDescription().outputs),
-      Object.keys(outputDict));
-
-    const composite = compositeFrom({
-      compose: false,
-      steps: [step, {
-        dependencies: Object.keys(outputDict),
-        compute: dependencies => dependencies,
-      }],
-    });
-
-    t.same(
-      composite.expose.compute(dependencies),
-      outputDict);
-  }
 });
 
 t.test(`withPropertiesFromObject: validate static inputs`, t => {
@@ -226,11 +211,12 @@ t.test(`withPropertiesFromObject: validate dynamic inputs`, t => {
       properties: 'onceMore',
     }),
     {message: `Error computing composition`, cause:
-      {message: `Error computing composition withPropertiesFromObject`, cause:
-        {message: `Errors in input values provided to withPropertiesFromObject`, errors: [
-          {message: `object: Expected an object, got string`},
-          {message: `properties: Expected an array, got string`},
-        ]}}});
+      {message: `Error in step 1 of 2, withPropertiesFromObject`, cause:
+        {message: `Error computing composition withPropertiesFromObject`, cause:
+          {message: `Errors in input values provided to withPropertiesFromObject`, errors: [
+            {message: `object: Expected an object, got string`},
+            {message: `properties: Expected an array, got string`},
+          ]}}}});
 
   t.throws(
     () => composite.expose.compute({
@@ -238,17 +224,18 @@ t.test(`withPropertiesFromObject: validate dynamic inputs`, t => {
       properties: ['abc', 'def', 123],
     }),
     {message: `Error computing composition`, cause:
-      {message: `Error computing composition withPropertiesFromObject`, cause:
-        {message: `Errors in input values provided to withPropertiesFromObject`, errors: [
-          {message: `object: Expected an object, got array`},
-          {message: `properties: Errors validating array items`, errors: [
-            {
-              [Symbol.for('hsmusic.annotateError.indexInSourceArray')]: 2,
-              message: `Error at zero-index 2: 123`,
-              cause: {
-                message: `Expected a string, got number`,
+      {message: `Error in step 1 of 2, withPropertiesFromObject`, cause:
+        {message: `Error computing composition withPropertiesFromObject`, cause:
+          {message: `Errors in input values provided to withPropertiesFromObject`, errors: [
+            {message: `object: Expected an object, got array`},
+            {message: `properties: Errors validating array items`, errors: [
+              {
+                [Symbol.for('hsmusic.annotateError.indexInSourceArray')]: 2,
+                message: `Error at zero-index 2: 123`,
+                cause: {
+                  message: `Expected a string, got number`,
+                },
               },
-            },
-          ]},
-        ]}}});
+            ]},
+          ]}}}});
 });
diff --git a/test/unit/data/composite/data/withPropertyFromObject.js b/test/unit/data/composite/data/withPropertyFromObject.js
index 6a772c36..50ee835c 100644
--- a/test/unit/data/composite/data/withPropertyFromObject.js
+++ b/test/unit/data/composite/data/withPropertyFromObject.js
@@ -1,4 +1,5 @@
 import t from 'tap';
+import {quickCheckCompositeOutputs} from '#test-lib';
 
 import {compositeFrom, input} from '#composite';
 import {exposeDependency} from '#composite/control-flow';
@@ -56,6 +57,8 @@ t.test(`withPropertyFromObject: output shapes & values`, t => {
       'baz',
   };
 
+  const qcco = quickCheckCompositeOutputs(t, dependencies);
+
   const mapLevel1 = [
     ['object_dependency', [
       ['property_dependency', {
@@ -98,25 +101,7 @@ t.test(`withPropertyFromObject: output shapes & values`, t => {
         property: propertyInput,
       });
 
-      quickCheckOutputs(step, outputDict);
+      qcco(step, outputDict);
     }
   }
-
-  function quickCheckOutputs(step, outputDict) {
-    t.same(
-      Object.keys(step.toDescription().outputs),
-      Object.keys(outputDict));
-
-    const composite = compositeFrom({
-      compose: false,
-      steps: [step, {
-        dependencies: Object.keys(outputDict),
-        compute: dependencies => dependencies,
-      }],
-    });
-
-    t.same(
-      composite.expose.compute(dependencies),
-      outputDict);
-  }
 });
diff --git a/test/unit/data/composite/data/withUniqueItemsOnly.js b/test/unit/data/composite/data/withUniqueItemsOnly.js
index 965b14b5..50b16f43 100644
--- a/test/unit/data/composite/data/withUniqueItemsOnly.js
+++ b/test/unit/data/composite/data/withUniqueItemsOnly.js
@@ -1,4 +1,5 @@
 import t from 'tap';
+import {quickCheckCompositeOutputs} from '#test-lib';
 
 import {compositeFrom, input} from '#composite';
 import {exposeDependency} from '#composite/control-flow';
@@ -44,6 +45,8 @@ t.test(`withUniqueItemsOnly: output shapes & values`, t => {
       [8, 8, 7, 6, 6, 5, 'bar', true, true, 5],
   };
 
+  const qcco = quickCheckCompositeOutputs(t, dependencies);
+
   const mapLevel1 = [
     ['list_dependency', {
       '#list_dependency': [1, 2, 3, 4, 'foo', false],
@@ -61,24 +64,6 @@ t.test(`withUniqueItemsOnly: output shapes & values`, t => {
       list: listInput,
     });
 
-    quickCheckOutputs(step, outputDict);
-  }
-
-  function quickCheckOutputs(step, outputDict) {
-    t.same(
-      Object.keys(step.toDescription().outputs),
-      Object.keys(outputDict));
-
-    const composite = compositeFrom({
-      compose: false,
-      steps: [step, {
-        dependencies: Object.keys(outputDict),
-        compute: dependencies => dependencies,
-      }],
-    });
-
-    t.same(
-      composite.expose.compute(dependencies),
-      outputDict);
+    qcco(step, outputDict);
   }
 });