« get me outta code hell

Merge branch 'preview' into listing-tweaks - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2023-10-29 09:26:59 -0300
committer(quasar) nebula <qznebula@protonmail.com>2023-10-29 09:26:59 -0300
commitbfa1953e79a562ee675940b7acc52b5b29d22d8f (patch)
tree5c1cd2f4050c801a60f4b65b367a714ed0979759
parentc4ef4ced62d659d217873c6c48dd8038dbf765af (diff)
parent940b2cbf8b68eb0b446cca0feeb507840c486394 (diff)
Merge branch 'preview' into listing-tweaks
-rw-r--r--.gitignore4
-rw-r--r--.taprc3
-rw-r--r--README.md30
-rw-r--r--coverage-map.js71
-rw-r--r--data-tests/index.js4
-rw-r--r--package-lock.json11380
-rw-r--r--package.json13
-rw-r--r--src/content/dependencies/generateAlbumCommentaryPage.js44
-rw-r--r--src/content/dependencies/generateAlbumGalleryNoTrackArtworksLine.js7
-rw-r--r--src/content/dependencies/generateAlbumGalleryPage.js89
-rw-r--r--src/content/dependencies/generateAlbumInfoPage.js4
-rw-r--r--src/content/dependencies/generateAlbumNavAccent.js16
-rw-r--r--src/content/dependencies/generateAlbumSecondaryNav.js6
-rw-r--r--src/content/dependencies/generateAlbumSidebarTrackSection.js48
-rw-r--r--src/content/dependencies/generateAlbumStyleRules.js73
-rw-r--r--src/content/dependencies/generateAlbumTrackListItem.js20
-rw-r--r--src/content/dependencies/generateArtistInfoPageChunkItem.js4
-rw-r--r--src/content/dependencies/generateArtistInfoPageTracksChunkedList.js25
-rw-r--r--src/content/dependencies/generateCoverArtwork.js15
-rw-r--r--src/content/dependencies/generateCoverGrid.js12
-rw-r--r--src/content/dependencies/generateFlashActGalleryPage.js91
-rw-r--r--src/content/dependencies/generateFlashActNavAccent.js74
-rw-r--r--src/content/dependencies/generateFlashActSidebar.js194
-rw-r--r--src/content/dependencies/generateFlashIndexPage.js21
-rw-r--r--src/content/dependencies/generateFlashInfoPage.js17
-rw-r--r--src/content/dependencies/generateFlashNavAccent.js7
-rw-r--r--src/content/dependencies/generateFlashSidebar.js236
-rw-r--r--src/content/dependencies/generateFooterLocalizationLinks.js2
-rw-r--r--src/content/dependencies/generateGroupGalleryPage.js35
-rw-r--r--src/content/dependencies/generateGroupInfoPage.js6
-rw-r--r--src/content/dependencies/generateGroupNavLinks.js44
-rw-r--r--src/content/dependencies/generateGroupSecondaryNav.js99
-rw-r--r--src/content/dependencies/generatePageLayout.js53
-rw-r--r--src/content/dependencies/generateTrackInfoPage.js9
-rw-r--r--src/content/dependencies/generateTrackList.js59
-rw-r--r--src/content/dependencies/generateWikiHomeAlbumsRow.js2
-rw-r--r--src/content/dependencies/image.js134
-rw-r--r--src/content/dependencies/index.js24
-rw-r--r--src/content/dependencies/linkAlbumDynamically.js14
-rw-r--r--src/content/dependencies/linkFlashAct.js14
-rw-r--r--src/content/dependencies/linkGroupDynamically.js14
-rw-r--r--src/content/dependencies/linkTemplate.js39
-rw-r--r--src/content/dependencies/linkThing.js10
-rw-r--r--src/content/dependencies/listArtTagNetwork.js1
-rw-r--r--src/content/dependencies/listTracksWithExtra.js12
-rw-r--r--src/content/dependencies/transformContent.js5
-rw-r--r--src/data/composite/control-flow/exitWithoutDependency.js35
-rw-r--r--src/data/composite/control-flow/exitWithoutUpdateValue.js24
-rw-r--r--src/data/composite/control-flow/exposeConstant.js26
-rw-r--r--src/data/composite/control-flow/exposeDependency.js28
-rw-r--r--src/data/composite/control-flow/exposeDependencyOrContinue.js34
-rw-r--r--src/data/composite/control-flow/exposeUpdateValueOrContinue.js40
-rw-r--r--src/data/composite/control-flow/index.js9
-rw-r--r--src/data/composite/control-flow/inputAvailabilityCheckMode.js9
-rw-r--r--src/data/composite/control-flow/raiseOutputWithoutDependency.js39
-rw-r--r--src/data/composite/control-flow/raiseOutputWithoutUpdateValue.js47
-rw-r--r--src/data/composite/control-flow/withResultOfAvailabilityCheck.js71
-rw-r--r--src/data/composite/data/excludeFromList.js56
-rw-r--r--src/data/composite/data/fillMissingListItems.js51
-rw-r--r--src/data/composite/data/index.js8
-rw-r--r--src/data/composite/data/withFlattenedList.js47
-rw-r--r--src/data/composite/data/withPropertiesFromList.js92
-rw-r--r--src/data/composite/data/withPropertiesFromObject.js87
-rw-r--r--src/data/composite/data/withPropertyFromList.js82
-rw-r--r--src/data/composite/data/withPropertyFromObject.js69
-rw-r--r--src/data/composite/data/withUnflattenedList.js62
-rw-r--r--src/data/composite/things/album/index.js2
-rw-r--r--src/data/composite/things/album/withTrackSections.js128
-rw-r--r--src/data/composite/things/album/withTracks.js51
-rw-r--r--src/data/composite/things/flash/index.js1
-rw-r--r--src/data/composite/things/flash/withFlashAct.js108
-rw-r--r--src/data/composite/things/track/exitWithoutUniqueCoverArt.js26
-rw-r--r--src/data/composite/things/track/index.js9
-rw-r--r--src/data/composite/things/track/inheritFromOriginalRelease.js43
-rw-r--r--src/data/composite/things/track/trackReverseReferenceList.js38
-rw-r--r--src/data/composite/things/track/withAlbum.js108
-rw-r--r--src/data/composite/things/track/withAlwaysReferenceByDirectory.js91
-rw-r--r--src/data/composite/things/track/withContainingTrackSection.js63
-rw-r--r--src/data/composite/things/track/withHasUniqueCoverArt.js61
-rw-r--r--src/data/composite/things/track/withOriginalRelease.js59
-rw-r--r--src/data/composite/things/track/withOtherReleases.js40
-rw-r--r--src/data/composite/things/track/withPropertyFromAlbum.js49
-rw-r--r--src/data/composite/wiki-data/exitWithoutContribs.js47
-rw-r--r--src/data/composite/wiki-data/index.js7
-rw-r--r--src/data/composite/wiki-data/inputThingClass.js23
-rw-r--r--src/data/composite/wiki-data/inputWikiData.js17
-rw-r--r--src/data/composite/wiki-data/withResolvedContribs.js77
-rw-r--r--src/data/composite/wiki-data/withResolvedReference.js73
-rw-r--r--src/data/composite/wiki-data/withResolvedReferenceList.js101
-rw-r--r--src/data/composite/wiki-data/withReverseReferenceList.js41
-rw-r--r--src/data/composite/wiki-properties/additionalFiles.js30
-rw-r--r--src/data/composite/wiki-properties/color.js12
-rw-r--r--src/data/composite/wiki-properties/commentary.js12
-rw-r--r--src/data/composite/wiki-properties/commentatorArtists.js55
-rw-r--r--src/data/composite/wiki-properties/contribsPresent.js30
-rw-r--r--src/data/composite/wiki-properties/contributionList.js35
-rw-r--r--src/data/composite/wiki-properties/dimensions.js13
-rw-r--r--src/data/composite/wiki-properties/directory.js23
-rw-r--r--src/data/composite/wiki-properties/duration.js13
-rw-r--r--src/data/composite/wiki-properties/externalFunction.js11
-rw-r--r--src/data/composite/wiki-properties/fileExtension.js13
-rw-r--r--src/data/composite/wiki-properties/flag.js19
-rw-r--r--src/data/composite/wiki-properties/index.js20
-rw-r--r--src/data/composite/wiki-properties/name.js11
-rw-r--r--src/data/composite/wiki-properties/referenceList.js47
-rw-r--r--src/data/composite/wiki-properties/reverseReferenceList.js30
-rw-r--r--src/data/composite/wiki-properties/simpleDate.js14
-rw-r--r--src/data/composite/wiki-properties/simpleString.js14
-rw-r--r--src/data/composite/wiki-properties/singleReference.js47
-rw-r--r--src/data/composite/wiki-properties/urls.js14
-rw-r--r--src/data/composite/wiki-properties/wikiData.js17
-rw-r--r--src/data/things/album.js280
-rw-r--r--src/data/things/art-tag.js48
-rw-r--r--src/data/things/artist.js95
-rw-r--r--src/data/things/cacheable-object.js82
-rw-r--r--src/data/things/composite.js1301
-rw-r--r--src/data/things/flash.js136
-rw-r--r--src/data/things/group.js74
-rw-r--r--src/data/things/homepage-layout.js113
-rw-r--r--src/data/things/index.js29
-rw-r--r--src/data/things/language.js170
-rw-r--r--src/data/things/news-entry.js16
-rw-r--r--src/data/things/static-page.js23
-rw-r--r--src/data/things/thing.js394
-rw-r--r--src/data/things/track.js718
-rw-r--r--src/data/things/validators.js89
-rw-r--r--src/data/things/wiki-info.js55
-rw-r--r--src/data/yaml.js623
-rw-r--r--src/file-size-preloader.js3
-rw-r--r--src/find.js222
-rw-r--r--src/gen-thumbs.js329
-rw-r--r--src/listing-spec.js12
-rw-r--r--src/page/album.js3
-rw-r--r--src/page/artist-alias.js6
-rw-r--r--src/page/flash-act.js23
-rw-r--r--src/page/flash.js2
-rw-r--r--src/page/index.js1
-rw-r--r--src/repl.js3
-rw-r--r--src/static/client2.js1049
-rw-r--r--src/static/site5.css (renamed from src/static/site4.css)88
-rw-r--r--src/strings-default.json15
-rwxr-xr-xsrc/upd8.js607
-rw-r--r--src/url-spec.js2
-rw-r--r--src/util/cli.js10
-rw-r--r--src/util/html.js63
-rw-r--r--src/util/replacer.js5
-rw-r--r--src/util/sugar.js100
-rw-r--r--src/util/urls.js21
-rw-r--r--src/util/wiki-data.js85
-rw-r--r--src/write/bind-utilities.js27
-rw-r--r--src/write/build-modes/live-dev-server.js55
-rw-r--r--src/write/build-modes/static-build.js61
-rw-r--r--tap-snapshots/test/snapshot/generateAdditionalFilesList.js.test.cjs4
-rw-r--r--tap-snapshots/test/snapshot/generateAdditionalFilesShortcut.js.test.cjs4
-rw-r--r--tap-snapshots/test/snapshot/generateAlbumBanner.js.test.cjs6
-rw-r--r--tap-snapshots/test/snapshot/generateAlbumCoverArtwork.js.test.cjs41
-rw-r--r--tap-snapshots/test/snapshot/generateAlbumReleaseInfo.js.test.cjs14
-rw-r--r--tap-snapshots/test/snapshot/generateAlbumSecondaryNav.js.test.cjs22
-rw-r--r--tap-snapshots/test/snapshot/generateAlbumSidebarGroupBox.js.test.cjs6
-rw-r--r--tap-snapshots/test/snapshot/generateAlbumTrackList.js.test.cjs4
-rw-r--r--tap-snapshots/test/snapshot/generateBanner.js.test.cjs4
-rw-r--r--tap-snapshots/test/snapshot/generateCoverArtwork.js.test.cjs41
-rw-r--r--tap-snapshots/test/snapshot/generatePreviousNextLinks.js.test.cjs22
-rw-r--r--tap-snapshots/test/snapshot/generateTrackCoverArtwork.js.test.cjs53
-rw-r--r--tap-snapshots/test/snapshot/generateTrackReleaseInfo.js.test.cjs16
-rw-r--r--tap-snapshots/test/snapshot/image.js.test.cjs40
-rw-r--r--tap-snapshots/test/snapshot/linkArtist.js.test.cjs4
-rw-r--r--tap-snapshots/test/snapshot/linkContribution.js.test.cjs22
-rw-r--r--tap-snapshots/test/snapshot/linkExternal.js.test.cjs10
-rw-r--r--tap-snapshots/test/snapshot/linkExternalFlash.js.test.cjs4
-rw-r--r--tap-snapshots/test/snapshot/linkTemplate.js.test.cjs10
-rw-r--r--tap-snapshots/test/snapshot/linkThing.js.test.cjs39
-rw-r--r--tap-snapshots/test/snapshot/transformContent.js.test.cjs28
-rw-r--r--test/lib/content-function.js96
-rw-r--r--test/lib/index.js3
-rw-r--r--test/lib/wiki-data.js24
-rw-r--r--test/snapshot/generateAlbumCoverArtwork.js14
-rw-r--r--test/snapshot/generateAlbumSecondaryNav.js4
-rw-r--r--test/snapshot/generateCoverArtwork.js12
-rw-r--r--test/snapshot/generateTrackCoverArtwork.js14
-rw-r--r--test/snapshot/image.js53
-rw-r--r--test/snapshot/linkThing.js87
-rw-r--r--test/snapshot/transformContent.js17
-rw-r--r--test/unit/data/cacheable-object.js (renamed from test/unit/data/things/cacheable-object.js)30
-rw-r--r--test/unit/data/composite/control-flow/exposeConstant.js42
-rw-r--r--test/unit/data/composite/control-flow/exposeDependency.js64
-rw-r--r--test/unit/data/composite/control-flow/withResultOfAvailabilityCheck.js195
-rw-r--r--test/unit/data/composite/data/withPropertiesFromObject.js248
-rw-r--r--test/unit/data/composite/data/withPropertyFromObject.js122
-rw-r--r--test/unit/data/composite/things/track/withAlbum.js144
-rw-r--r--test/unit/data/compositeFrom.js345
-rw-r--r--test/unit/data/templateCompositeFrom.js209
-rw-r--r--test/unit/data/things/album.js411
-rw-r--r--test/unit/data/things/art-tag.js71
-rw-r--r--test/unit/data/things/flash.js55
-rw-r--r--test/unit/data/things/track.js685
196 files changed, 18061 insertions, 8059 deletions
diff --git a/.gitignore b/.gitignore
index 6f9fe6b..a9dda76 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,3 @@
-node_modules
+/node_modules
+/.tap
 .DS_Store
-.nyc_output
diff --git a/.taprc b/.taprc
new file mode 100644
index 0000000..44e24f5
--- /dev/null
+++ b/.taprc
@@ -0,0 +1,3 @@
+coverage-map: coverage-map.js
+exclude:
+  - test/lib/*
diff --git a/README.md b/README.md
index 62dd64d..a7fc582 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ $ git clone https://github.com/hsmusic/hsmusic-wiki code
 Cloning into 'code'...
 $ git clone https://github.com/hsmusic/hsmusic-data data
 Cloning into 'data'...
-$ git clone https://nebula.ed1.club/git/hsmusic-media media
+$ git clone https://github.com/hsmusic/hsmusic-media media
 Cloning into 'media'...
 ```
 
@@ -106,7 +106,7 @@ HSMusic works using a number of repositories in tandem:
   - This repo covers albums, tracks, artists, groups, and a variety of other things which make up the content of a music wiki.
   - The data repo also contains all the metadata which makes one wiki unique from another: layout info, static pages (like "About & Credits"), whether or not certain site features are enabled (like "Flashes & Games" or UI for browsing groups), and so on.
   - All data is written and accessed in the YAML format, and every file follows a specific structure described within this (code) repository. See below and the `src/data` directory for details.
-- [`hsmusic-media`][ed1-media]: The media repository, holding all album, track, and layout media used across the site in one place. Media and organization directly corresponds to entries in the data repository; generally the data and media repositories go together and are swapped out for another together.
+- [`hsmusic-media`][github-media]: The media repository, holding all album, track, and layout media used across the site in one place. Media and organization directly corresponds to entries in the data repository; generally the data and media repositories go together and are swapped out for another together.
 - *Language repo:* The language repository, holding up-to-date strings and other localization info for HSMusic. NB: This repo isn't currently online as its structure and tooling haven't been polished or properly put together yet, but it's not required for building the site.
   - Strings and language info are stored in top-level JSON files within this repository. They're based off the `src/strings-default.json` file within the code repo (and don't need to provide translations for all strings to be used for site building).
 
@@ -139,16 +139,18 @@ The source code for HSMusic is divided across a number of source files, loosely
 
 - `src/`
   - `data/`
-    - `cacheable-object.js`: Backbone of how data objects (colloquially "things") store, share, and compute their properties
-    - `things.js`: Descriptors for all "thing" classes used across the wiki: albums, tracks, artists, groups, etc
-    - `validators.js`: Convenient error-throwing utilities which help ensure properties set on things follow the right format
-    - `yaml.js`: Mappings from YAML documents (the format used in `hsmusic-data`) to things (actual data objects), and a full set of utilities used to actually load that data from scratch
-  - `page/`
-    - All page templates (HTML content and layout metadata) are kept in source files under this directory
-  - `static/`
-    - Purely client-side files are kept here, e.g. site CSS, icon SVGs, and client-side JS
-  - `util/`
-    - Common utilities which generally may be accessed from both Node.js or the client (web browser)
+    - `things/`: Descriptors for individual types of data objects used across the wiki, notably including:
+      - `cacheable-object.js`: Backbone of how data objects (colloquially "things") store, share, and compute their properties
+      - `thing.js`: Common superclass for most data objects, with a bunch of utilities and common behavior
+      - `validators.js`: Convenient error-throwing utilities which help ensure properties set on data objects follow the right format
+    - `yaml.js`: Mappings from YAML documents (the format used in `hsmusic-data`) to things (actual data objects), and a full suite of utilities used to actually load that data from scratch
+  - `content/`: Functions which generate HTML content; these go from bite-sized, commonly reused utilities (like `linkTemplate`) all the way up to entire page definitions (like `generateArtistInfoPage`)
+  - `page/`: Definitions for page paths, mapping data objects to paths served over an HTTP server (or written to an output folder) and to the functions which actually generate those pages' content
+  - `write/`: Common utilities and output methods for controlling what hsmusic does to turn data and media into something you actually visit; these each define a variety of command-line arguments and are basically the interchangeable  "second half" of upd8.js
+    - `live-dev-server.js`: Gets the site available for viewing as quickly as possible, generating and serving pages as they are requested from a local HTTP server; reacts live to code changes in the `content` directory
+    - `static-build.js`: Builds the entire site at once, writing all the output to one self-contained folder which can be uploaded to a static file server
+  - `static/`: Purely client-side files are kept here, e.g. site CSS, icon SVGs, and client-side JS
+  - `util/`: Common utilities which generally may be accessed from both Node.js or the client (web browser)
   - `upd8.js`: Main entry point which controls and directs site generation from start to finish
   - `gen-thumbs.js`: Standalone utility also called every time HSMusic is run (unless `--skip-thumbs` is provided) which keeps a persistent cache of media MD5s and (re)generates thumbnails for new or updated image files
   - `repl.js`: Standalone utility for loading all wiki data and providing a convenient REPL to run filters and transformations on data objects right from the Node.js command line
@@ -165,16 +167,16 @@ hsmusic is a relatively generic music wiki software, so you're more than encoura
 
 As mentioned, part of the focus of the hsmusic.wiki release, as well as most development since, has been to create a more modular and developer-friendly repository. So, on the curious chance anyone would like to contribute code to the repo, that's more possible now than it used to be!
 
-Still, for larger additions, we encourage you to [drop the main dev an email][feedback] or, better yet, [pop by the Discord][discord] before writing all the implementation code: besides code tips which might make your life a bit easier (questions are welcome), we also love to discuss feature designs and values while they're still being brainstormed! That way, nobody has to tell you there are fundamental ideas or implementation details that should be rebuilt from the ground up - the last thing we want is anyone putting hours into code that has to be replaced by another implementation before it ever ends up part of the wiki!
+Still, for larger additions, we encourage you to [drop the main devs an email][feedback] or, better yet, [pop by the Discord][discord] before writing all the implementation code: besides code tips which might make your life a bit easier (questions are welcome), we also love to discuss feature designs and values while they're still being brainstormed! That way, nobody has to tell you there are fundamental ideas or implementation details that should be rebuilt from the ground up - the last thing we want is anyone putting hours into code that has to be replaced by another implementation before it ever ends up part of the wiki!
 
 As ever, feedback is always welcome, and may be shared via the usual links. Thank you for checking the repository out!
 
-  [ed1-media]: https://nebula.ed1.club/git/hsmusic-media/
   [discord]: https://hsmusic.wiki/discord/
   [fandom]: https://homestuck-and-mspa-music.fandom.com/wiki/Homestuck_and_MSPA_Music_Wiki
   [feedback]: https://hsmusic.wiki/feedback/
   [github]: https://github.com/hsmusic/hsmusic-wiki
   [github-code]: https://github.com/hsmusic/hsmusic-wiki
   [github-data]: https://github.com/hsmusic/hsmusic-data
+  [github-media]: https://github.com/hsmusic/hsmusic-media
   [hsmusic]: https://hsmusic.wiki
   [nsnd]: https://homestuck.net/music/references.html
diff --git a/coverage-map.js b/coverage-map.js
new file mode 100644
index 0000000..beff9e8
--- /dev/null
+++ b/coverage-map.js
@@ -0,0 +1,71 @@
+// node-tap test -> src coverage map
+// https://node-tap.org/coverage/
+
+export default function map(F) {
+  let match;
+
+  // unit/content/...
+
+  match = F.match(/^test\/unit\/content\/(.*)$/);
+  if (match) {
+    const f = match[1];
+
+    match = f.match(/^dependencies\/(.*)\.js$/);
+    if (match) {
+      return `src/content/dependencies/${match[1]}.js`;
+    }
+  }
+
+  // unit/data/...
+
+  match = F.match(/^test\/unit\/data\/(.*)$/);
+  if (match) {
+    const f = match[1];
+
+    match = f.match(/^composite\/(.*)$/);
+    if (match) {
+      return `src/data/composite/${match[1]}`;
+    }
+
+    match = f.match(/^things\/(.*)\.js$/);
+    if (match) {
+      return `src/data/things/${match[1]}.js`;
+    }
+
+    match = f.match(/^cacheable-object\.js$/);
+    if (match) {
+      return `src/data/things/cacheable-object.js`;
+    }
+
+    match = f.match(/^(templateCompositeFrom|compositeFrom)\.js$/);
+    if (match) {
+      return `src/data/things/composite.js`;
+    }
+  }
+
+  // unit/util/...
+
+  match = F.match(/^test\/unit\/util\/(.*)$/);
+  if (match) {
+    const f = match[1];
+
+    switch (f) {
+      case 'html.js':
+        return 'src/util/html.js';
+    }
+  }
+
+  // snapshot/...
+
+  match = F.match(/^test\/snapshot\/(.*)$/);
+  if (match) {
+    const f = match[1];
+
+    match = f.match(/^(.*)\.js$/);
+    if (match) {
+      return `src/content/dependencies/${match[1]}.js`;
+    }
+  }
+
+  return null;
+}
diff --git a/data-tests/index.js b/data-tests/index.js
index b05de9e..d077090 100644
--- a/data-tests/index.js
+++ b/data-tests/index.js
@@ -3,7 +3,7 @@ import {fileURLToPath} from 'node:url';
 
 import chokidar from 'chokidar';
 
-import {color, logError, logInfo, logWarn, parseOptions} from '#cli';
+import {colors, logError, logInfo, logWarn, parseOptions} from '#cli';
 import {isMain} from '#node-utils';
 import {getContextAssignments} from '#repl';
 import {bindOpts, showAggregate} from '#sugar';
@@ -24,7 +24,7 @@ async function main() {
   }
 
   console.log(`HSMusic automated data tests`);
-  console.log(`${color.bright(color.yellow(`:star:`))} Now featuring quick-reloading! ${color.bright(color.cyan(`:earth:`))}`);
+  console.log(`${colors.bright(colors.yellow(`:star:`))} Now featuring quick-reloading! ${colors.bright(colors.cyan(`:earth:`))}`);
 
   // Watch adjacent files in data-tests directory
   const metaPath = fileURLToPath(import.meta.url);
diff --git a/package-lock.json b/package-lock.json
index d9ab5cd..6433ea1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
                 "command-exists": "^1.2.9",
                 "eslint": "^8.37.0",
                 "he": "^1.2.0",
+                "image-size": "^1.0.2",
                 "js-yaml": "^4.1.0",
                 "marked": "^5.0.2",
                 "striptags": "^4.0.0-alpha.4",
@@ -23,624 +24,1349 @@
             },
             "devDependencies": {
                 "chokidar": "^3.5.3",
-                "tap": "^16.3.4",
+                "tap": "^18.4.0",
                 "tcompare": "^6.0.0"
             }
         },
-        "node_modules/@ampproject/remapping": {
-            "version": "2.2.0",
-            "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
-            "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==",
+        "node_modules/@alcalzone/ansi-tokenize": {
+            "version": "0.1.3",
+            "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz",
+            "integrity": "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==",
             "dev": true,
             "dependencies": {
-                "@jridgewell/gen-mapping": "^0.1.0",
-                "@jridgewell/trace-mapping": "^0.3.9"
+                "ansi-styles": "^6.2.1",
+                "is-fullwidth-code-point": "^4.0.0"
             },
             "engines": {
-                "node": ">=6.0.0"
+                "node": ">=14.13.1"
             }
         },
-        "node_modules/@babel/code-frame": {
-            "version": "7.18.6",
-            "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
-            "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
+        "node_modules/@alcalzone/ansi-tokenize/node_modules/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,
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+            }
+        },
+        "node_modules/@base2/pretty-print-object": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz",
+            "integrity": "sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==",
+            "dev": true
+        },
+        "node_modules/@bcoe/v8-coverage": {
+            "version": "0.2.3",
+            "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
+            "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
+            "dev": true
+        },
+        "node_modules/@cspotcode/source-map-support": {
+            "version": "0.8.1",
+            "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+            "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+            "dev": true,
+            "dependencies": {
+                "@jridgewell/trace-mapping": "0.3.9"
+            },
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@eslint-community/eslint-utils": {
+            "version": "4.4.0",
+            "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+            "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+            "dependencies": {
+                "eslint-visitor-keys": "^3.3.0"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "peerDependencies": {
+                "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+            }
+        },
+        "node_modules/@eslint-community/regexpp": {
+            "version": "4.5.0",
+            "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz",
+            "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==",
+            "engines": {
+                "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+            }
+        },
+        "node_modules/@eslint/eslintrc": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz",
+            "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==",
             "dependencies": {
-                "@babel/highlight": "^7.18.6"
+                "ajv": "^6.12.4",
+                "debug": "^4.3.2",
+                "espree": "^9.5.1",
+                "globals": "^13.19.0",
+                "ignore": "^5.2.0",
+                "import-fresh": "^3.2.1",
+                "js-yaml": "^4.1.0",
+                "minimatch": "^3.1.2",
+                "strip-json-comments": "^3.1.1"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/eslint"
             }
         },
-        "node_modules/@babel/compat-data": {
-            "version": "7.21.0",
-            "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.0.tgz",
-            "integrity": "sha512-gMuZsmsgxk/ENC3O/fRw5QY8A9/uxQbbCEypnLIiYYc/qVJtEV7ouxC3EllIIwNzMqAQee5tanFabWsUOutS7g==",
+        "node_modules/@eslint/js": {
+            "version": "8.37.0",
+            "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.37.0.tgz",
+            "integrity": "sha512-x5vzdtOOGgFVDCUs81QRB2+liax8rFg3+7hqM+QhBG0/G3F1ZsoYl97UrqgHgQ9KKT7G6c4V+aTUCgu/n22v1A==",
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            }
+        },
+        "node_modules/@humanwhocodes/config-array": {
+            "version": "0.11.8",
+            "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
+            "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
+            "dependencies": {
+                "@humanwhocodes/object-schema": "^1.2.1",
+                "debug": "^4.1.1",
+                "minimatch": "^3.0.5"
+            },
+            "engines": {
+                "node": ">=10.10.0"
+            }
+        },
+        "node_modules/@humanwhocodes/module-importer": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+            "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+            "engines": {
+                "node": ">=12.22"
+            },
+            "funding": {
+                "type": "github",
+                "url": "https://github.com/sponsors/nzakas"
+            }
+        },
+        "node_modules/@humanwhocodes/object-schema": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+            "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA=="
+        },
+        "node_modules/@isaacs/cliui": {
+            "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",
+                "strip-ansi": "^7.0.1",
+                "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+                "wrap-ansi": "^8.1.0",
+                "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+            },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">=12"
             }
         },
-        "node_modules/@babel/core": {
-            "version": "7.21.3",
-            "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.3.tgz",
-            "integrity": "sha512-qIJONzoa/qiHghnm0l1n4i/6IIziDpzqc36FBs4pzMhDUraHqponwJLiAKm1hGLP3OSB/TVNz6rMwVGpwxxySw==",
+        "node_modules/@isaacs/cliui/node_modules/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,
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+            }
+        },
+        "node_modules/@isaacs/cliui/node_modules/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,
             "dependencies": {
-                "@ampproject/remapping": "^2.2.0",
-                "@babel/code-frame": "^7.18.6",
-                "@babel/generator": "^7.21.3",
-                "@babel/helper-compilation-targets": "^7.20.7",
-                "@babel/helper-module-transforms": "^7.21.2",
-                "@babel/helpers": "^7.21.0",
-                "@babel/parser": "^7.21.3",
-                "@babel/template": "^7.20.7",
-                "@babel/traverse": "^7.21.3",
-                "@babel/types": "^7.21.3",
-                "convert-source-map": "^1.7.0",
-                "debug": "^4.1.0",
-                "gensync": "^1.0.0-beta.2",
-                "json5": "^2.2.2",
-                "semver": "^6.3.0"
+                "ansi-regex": "^6.0.1"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">=12"
             },
             "funding": {
-                "type": "opencollective",
-                "url": "https://opencollective.com/babel"
+                "url": "https://github.com/chalk/strip-ansi?sponsor=1"
             }
         },
-        "node_modules/@babel/generator": {
-            "version": "7.21.3",
-            "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.3.tgz",
-            "integrity": "sha512-QS3iR1GYC/YGUnW7IdggFeN5c1poPUurnGttOV/bZgPGV+izC/D8HnD6DLwod0fsatNyVn1G3EVWMYIF0nHbeA==",
+        "node_modules/@istanbuljs/schema": {
+            "version": "0.1.3",
+            "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+            "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
             "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/@jridgewell/resolve-uri": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
+            "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
+            "dev": true,
+            "engines": {
+                "node": ">=6.0.0"
+            }
+        },
+        "node_modules/@jridgewell/sourcemap-codec": {
+            "version": "1.4.15",
+            "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
+            "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
+            "dev": true
+        },
+        "node_modules/@jridgewell/trace-mapping": {
+            "version": "0.3.9",
+            "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+            "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+            "dev": true,
+            "dependencies": {
+                "@jridgewell/resolve-uri": "^3.0.3",
+                "@jridgewell/sourcemap-codec": "^1.4.10"
+            }
+        },
+        "node_modules/@nodelib/fs.scandir": {
+            "version": "2.1.5",
+            "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+            "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+            "dependencies": {
+                "@nodelib/fs.stat": "2.0.5",
+                "run-parallel": "^1.1.9"
+            },
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/@nodelib/fs.stat": {
+            "version": "2.0.5",
+            "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+            "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/@nodelib/fs.walk": {
+            "version": "1.2.8",
+            "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+            "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
             "dependencies": {
-                "@babel/types": "^7.21.3",
-                "@jridgewell/gen-mapping": "^0.3.2",
-                "@jridgewell/trace-mapping": "^0.3.17",
-                "jsesc": "^2.5.1"
+                "@nodelib/fs.scandir": "2.1.5",
+                "fastq": "^1.6.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">= 8"
             }
         },
-        "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": {
-            "version": "0.3.2",
-            "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
-            "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
+        "node_modules/@npmcli/agent": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.1.1.tgz",
+            "integrity": "sha512-6RlbiOAi6L6uUYF4/CDEkDZQnKw0XDsFJVrEpnib8rAx2WRMOsUyAdgnvDpX/fdkDWxtqE+NHwF465llI2wR0g==",
             "dev": true,
             "dependencies": {
-                "@jridgewell/set-array": "^1.0.1",
-                "@jridgewell/sourcemap-codec": "^1.4.10",
-                "@jridgewell/trace-mapping": "^0.3.9"
+                "http-proxy-agent": "^7.0.0",
+                "https-proxy-agent": "^7.0.1",
+                "lru-cache": "^10.0.1",
+                "socks-proxy-agent": "^8.0.1"
             },
             "engines": {
-                "node": ">=6.0.0"
+                "node": "^16.14.0 || >=18.0.0"
             }
         },
-        "node_modules/@babel/helper-compilation-targets": {
-            "version": "7.20.7",
-            "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz",
-            "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==",
+        "node_modules/@npmcli/agent/node_modules/agent-base": {
+            "version": "7.1.0",
+            "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz",
+            "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==",
             "dev": true,
             "dependencies": {
-                "@babel/compat-data": "^7.20.5",
-                "@babel/helper-validator-option": "^7.18.6",
-                "browserslist": "^4.21.3",
-                "lru-cache": "^5.1.1",
-                "semver": "^6.3.0"
+                "debug": "^4.3.4"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">= 14"
+            }
+        },
+        "node_modules/@npmcli/agent/node_modules/http-proxy-agent": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz",
+            "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==",
+            "dev": true,
+            "dependencies": {
+                "agent-base": "^7.1.0",
+                "debug": "^4.3.4"
             },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0"
+            "engines": {
+                "node": ">= 14"
             }
         },
-        "node_modules/@babel/helper-environment-visitor": {
-            "version": "7.18.9",
-            "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz",
-            "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==",
+        "node_modules/@npmcli/agent/node_modules/https-proxy-agent": {
+            "version": "7.0.2",
+            "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz",
+            "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==",
             "dev": true,
+            "dependencies": {
+                "agent-base": "^7.0.2",
+                "debug": "4"
+            },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">= 14"
             }
         },
-        "node_modules/@babel/helper-function-name": {
-            "version": "7.21.0",
-            "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz",
-            "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==",
+        "node_modules/@npmcli/agent/node_modules/socks-proxy-agent": {
+            "version": "8.0.2",
+            "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz",
+            "integrity": "sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==",
             "dev": true,
             "dependencies": {
-                "@babel/template": "^7.20.7",
-                "@babel/types": "^7.21.0"
+                "agent-base": "^7.0.2",
+                "debug": "^4.3.4",
+                "socks": "^2.7.1"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">= 14"
             }
         },
-        "node_modules/@babel/helper-hoist-variables": {
-            "version": "7.18.6",
-            "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz",
-            "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==",
+        "node_modules/@npmcli/fs": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz",
+            "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==",
             "dev": true,
             "dependencies": {
-                "@babel/types": "^7.18.6"
+                "semver": "^7.3.5"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
             }
         },
-        "node_modules/@babel/helper-module-imports": {
-            "version": "7.18.6",
-            "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
-            "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
+        "node_modules/@npmcli/git": {
+            "version": "5.0.3",
+            "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.3.tgz",
+            "integrity": "sha512-UZp9NwK+AynTrKvHn5k3KviW/hA5eENmFsu3iAPe7sWRt0lFUdsY/wXIYjpDFe7cdSNwOIzbObfwgt6eL5/2zw==",
             "dev": true,
             "dependencies": {
-                "@babel/types": "^7.18.6"
+                "@npmcli/promise-spawn": "^7.0.0",
+                "lru-cache": "^10.0.1",
+                "npm-pick-manifest": "^9.0.0",
+                "proc-log": "^3.0.0",
+                "promise-inflight": "^1.0.1",
+                "promise-retry": "^2.0.1",
+                "semver": "^7.3.5",
+                "which": "^4.0.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^16.14.0 || >=18.0.0"
             }
         },
-        "node_modules/@babel/helper-module-transforms": {
-            "version": "7.21.2",
-            "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz",
-            "integrity": "sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==",
+        "node_modules/@npmcli/git/node_modules/isexe": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
+            "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
+            "dev": true,
+            "engines": {
+                "node": ">=16"
+            }
+        },
+        "node_modules/@npmcli/git/node_modules/which": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
+            "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
             "dev": true,
             "dependencies": {
-                "@babel/helper-environment-visitor": "^7.18.9",
-                "@babel/helper-module-imports": "^7.18.6",
-                "@babel/helper-simple-access": "^7.20.2",
-                "@babel/helper-split-export-declaration": "^7.18.6",
-                "@babel/helper-validator-identifier": "^7.19.1",
-                "@babel/template": "^7.20.7",
-                "@babel/traverse": "^7.21.2",
-                "@babel/types": "^7.21.2"
+                "isexe": "^3.1.1"
+            },
+            "bin": {
+                "node-which": "bin/which.js"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^16.13.0 || >=18.0.0"
             }
         },
-        "node_modules/@babel/helper-simple-access": {
-            "version": "7.20.2",
-            "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz",
-            "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==",
+        "node_modules/@npmcli/installed-package-contents": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.0.2.tgz",
+            "integrity": "sha512-xACzLPhnfD51GKvTOOuNX2/V4G4mz9/1I2MfDoye9kBM3RYe5g2YbscsaGoTlaWqkxeiapBWyseULVKpSVHtKQ==",
             "dev": true,
             "dependencies": {
-                "@babel/types": "^7.20.2"
+                "npm-bundled": "^3.0.0",
+                "npm-normalize-package-bin": "^3.0.0"
+            },
+            "bin": {
+                "installed-package-contents": "lib/index.js"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+            }
+        },
+        "node_modules/@npmcli/node-gyp": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz",
+            "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==",
+            "dev": true,
+            "engines": {
+                "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
             }
         },
-        "node_modules/@babel/helper-split-export-declaration": {
-            "version": "7.18.6",
-            "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz",
-            "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==",
+        "node_modules/@npmcli/promise-spawn": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.0.tgz",
+            "integrity": "sha512-wBqcGsMELZna0jDblGd7UXgOby45TQaMWmbFwWX+SEotk4HV6zG2t6rT9siyLhPk4P6YYqgfL1UO8nMWDBVJXQ==",
             "dev": true,
             "dependencies": {
-                "@babel/types": "^7.18.6"
+                "which": "^4.0.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^16.14.0 || >=18.0.0"
+            }
+        },
+        "node_modules/@npmcli/promise-spawn/node_modules/isexe": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
+            "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
+            "dev": true,
+            "engines": {
+                "node": ">=16"
             }
         },
-        "node_modules/@babel/helper-string-parser": {
-            "version": "7.19.4",
-            "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
-            "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
+        "node_modules/@npmcli/promise-spawn/node_modules/which": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
+            "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
             "dev": true,
+            "dependencies": {
+                "isexe": "^3.1.1"
+            },
+            "bin": {
+                "node-which": "bin/which.js"
+            },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^16.13.0 || >=18.0.0"
             }
         },
-        "node_modules/@babel/helper-validator-identifier": {
-            "version": "7.19.1",
-            "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
-            "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
+        "node_modules/@npmcli/run-script": {
+            "version": "7.0.1",
+            "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.1.tgz",
+            "integrity": "sha512-Od/JMrgkjZ8alyBE0IzeqZDiF1jgMez9Gkc/OYrCkHHiXNwM0wc6s7+h+xM7kYDZkS0tAoOLr9VvygyE5+2F7g==",
             "dev": true,
+            "dependencies": {
+                "@npmcli/node-gyp": "^3.0.0",
+                "@npmcli/promise-spawn": "^7.0.0",
+                "node-gyp": "^9.0.0",
+                "read-package-json-fast": "^3.0.0",
+                "which": "^4.0.0"
+            },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^16.14.0 || >=18.0.0"
             }
         },
-        "node_modules/@babel/helper-validator-option": {
-            "version": "7.21.0",
-            "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz",
-            "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==",
+        "node_modules/@npmcli/run-script/node_modules/isexe": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
+            "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
             "dev": true,
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">=16"
             }
         },
-        "node_modules/@babel/helpers": {
-            "version": "7.21.0",
-            "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.0.tgz",
-            "integrity": "sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==",
+        "node_modules/@npmcli/run-script/node_modules/which": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
+            "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
             "dev": true,
             "dependencies": {
-                "@babel/template": "^7.20.7",
-                "@babel/traverse": "^7.21.0",
-                "@babel/types": "^7.21.0"
+                "isexe": "^3.1.1"
+            },
+            "bin": {
+                "node-which": "bin/which.js"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^16.13.0 || >=18.0.0"
             }
         },
-        "node_modules/@babel/highlight": {
-            "version": "7.18.6",
-            "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
-            "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
+        "node_modules/@pkgjs/parseargs": {
+            "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"
+            }
+        },
+        "node_modules/@sigstore/bundle": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.1.0.tgz",
+            "integrity": "sha512-89uOo6yh/oxaU8AeOUnVrTdVMcGk9Q1hJa7Hkvalc6G3Z3CupWk4Xe9djSgJm9fMkH69s0P0cVHUoKSOemLdng==",
             "dev": true,
             "dependencies": {
-                "@babel/helper-validator-identifier": "^7.18.6",
-                "chalk": "^2.0.0",
-                "js-tokens": "^4.0.0"
+                "@sigstore/protobuf-specs": "^0.2.1"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^16.14.0 || >=18.0.0"
             }
         },
-        "node_modules/@babel/highlight/node_modules/ansi-styles": {
-            "version": "3.2.1",
-            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-            "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+        "node_modules/@sigstore/protobuf-specs": {
+            "version": "0.2.1",
+            "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.2.1.tgz",
+            "integrity": "sha512-XTWVxnWJu+c1oCshMLwnKvz8ZQJJDVOlciMfgpJBQbThVjKTCG8dwyhgLngBD2KN0ap9F/gOV8rFDEx8uh7R2A==",
+            "dev": true,
+            "engines": {
+                "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+            }
+        },
+        "node_modules/@sigstore/sign": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.1.0.tgz",
+            "integrity": "sha512-4VRpfJxs+8eLqzLVrZngVNExVA/zAhVbi4UT4zmtLi4xRd7vz5qie834OgkrGsLlLB1B2nz/3wUxT1XAUBe8gw==",
             "dev": true,
             "dependencies": {
-                "color-convert": "^1.9.0"
+                "@sigstore/bundle": "^2.1.0",
+                "@sigstore/protobuf-specs": "^0.2.1",
+                "make-fetch-happen": "^13.0.0"
             },
             "engines": {
-                "node": ">=4"
+                "node": "^16.14.0 || >=18.0.0"
             }
         },
-        "node_modules/@babel/highlight/node_modules/chalk": {
-            "version": "2.4.2",
-            "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
-            "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+        "node_modules/@sigstore/sign/node_modules/make-fetch-happen": {
+            "version": "13.0.0",
+            "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.0.tgz",
+            "integrity": "sha512-7ThobcL8brtGo9CavByQrQi+23aIfgYU++wg4B87AIS8Rb2ZBt/MEaDqzA00Xwv/jUjAjYkLHjVolYuTLKda2A==",
             "dev": true,
             "dependencies": {
-                "ansi-styles": "^3.2.1",
-                "escape-string-regexp": "^1.0.5",
-                "supports-color": "^5.3.0"
+                "@npmcli/agent": "^2.0.0",
+                "cacache": "^18.0.0",
+                "http-cache-semantics": "^4.1.1",
+                "is-lambda": "^1.0.1",
+                "minipass": "^7.0.2",
+                "minipass-fetch": "^3.0.0",
+                "minipass-flush": "^1.0.5",
+                "minipass-pipeline": "^1.2.4",
+                "negotiator": "^0.6.3",
+                "promise-retry": "^2.0.1",
+                "ssri": "^10.0.0"
             },
             "engines": {
-                "node": ">=4"
+                "node": "^16.14.0 || >=18.0.0"
             }
         },
-        "node_modules/@babel/highlight/node_modules/color-convert": {
-            "version": "1.9.3",
-            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
-            "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+        "node_modules/@sigstore/tuf": {
+            "version": "2.2.0",
+            "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.2.0.tgz",
+            "integrity": "sha512-KKATZ5orWfqd9ZG6MN8PtCIx4eevWSuGRKQvofnWXRpyMyUEpmrzg5M5BrCpjM+NfZ0RbNGOh5tCz/P2uoRqOA==",
             "dev": true,
             "dependencies": {
-                "color-name": "1.1.3"
+                "@sigstore/protobuf-specs": "^0.2.1",
+                "tuf-js": "^2.1.0"
+            },
+            "engines": {
+                "node": "^16.14.0 || >=18.0.0"
             }
         },
-        "node_modules/@babel/highlight/node_modules/color-name": {
-            "version": "1.1.3",
-            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-            "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
-            "dev": true
+        "node_modules/@tapjs/after": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/after/-/after-1.1.4.tgz",
+            "integrity": "sha512-TVjrOwpPZt/VfdYc+X4gF/TY06gDHfzP9lfSv7hcxSaUGtvlU0xLH1xsTZS1BKM+EX1qXrCA8RYaLblAniKmaQ==",
+            "dev": true,
+            "dependencies": {
+                "is-actual-promise": "^1.0.0"
+            },
+            "engines": {
+                "node": ">=16"
+            },
+            "peerDependencies": {
+                "@tapjs/core": "1.3.4"
+            }
         },
-        "node_modules/@babel/highlight/node_modules/escape-string-regexp": {
-            "version": "1.0.5",
-            "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-            "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+        "node_modules/@tapjs/after-each": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/after-each/-/after-each-1.1.4.tgz",
+            "integrity": "sha512-vcmPQi2wXi2obK2j1nXTDo6EV8uqXONGiaPAPsj+iELr7OB3vBR1FFOQ6GWAFw0Xh8EIIUs8CWyNHn40/kmyUg==",
             "dev": true,
+            "dependencies": {
+                "function-loop": "^4.0.0"
+            },
             "engines": {
-                "node": ">=0.8.0"
+                "node": ">=16"
+            },
+            "peerDependencies": {
+                "@tapjs/core": "1.3.4"
             }
         },
-        "node_modules/@babel/highlight/node_modules/has-flag": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-            "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+        "node_modules/@tapjs/asserts": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/asserts/-/asserts-1.1.4.tgz",
+            "integrity": "sha512-5jhbvqJ88agvGEW27l/ucNK7WqQAsCCt6gTBJKdVIL8jOZz5jOVaN/UI6gqUHLO7SYxIl4SOh8N11OYizRSKfA==",
             "dev": true,
+            "dependencies": {
+                "is-actual-promise": "^1.0.0",
+                "tcompare": "6.4.0",
+                "trivial-deferred": "^2.0.0"
+            },
             "engines": {
-                "node": ">=4"
+                "node": ">=16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            },
+            "peerDependencies": {
+                "@tapjs/core": "1.3.4"
             }
         },
-        "node_modules/@babel/highlight/node_modules/supports-color": {
-            "version": "5.5.0",
-            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
-            "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+        "node_modules/@tapjs/before": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/before/-/before-1.1.4.tgz",
+            "integrity": "sha512-JnCg39toYCBMZKECL6dqXkpi5p9efxvug/vqMoW7XDpYSJRnRz25EUvTPFd1IE6SwVpJF2xRFL7EKUnxLN3JiQ==",
             "dev": true,
             "dependencies": {
-                "has-flag": "^3.0.0"
+                "is-actual-promise": "^1.0.0"
             },
             "engines": {
-                "node": ">=4"
+                "node": ">=16"
+            },
+            "peerDependencies": {
+                "@tapjs/core": "1.3.4"
             }
         },
-        "node_modules/@babel/parser": {
-            "version": "7.21.3",
-            "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.3.tgz",
-            "integrity": "sha512-lobG0d7aOfQRXh8AyklEAgZGvA4FShxo6xQbUrrT/cNBPUdIDojlokwJsQyCC/eKia7ifqM0yP+2DRZ4WKw2RQ==",
+        "node_modules/@tapjs/before-each": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/before-each/-/before-each-1.1.4.tgz",
+            "integrity": "sha512-DnwLTOmeifh571kvL3Ef94Ui0OpGzM/oIbjOaL9onHnLTR+cOO8yZALJp6zVg/pq/OzScDY3DQuazunolEVCQQ==",
             "dev": true,
-            "bin": {
-                "parser": "bin/babel-parser.js"
+            "dependencies": {
+                "function-loop": "^4.0.0"
             },
             "engines": {
-                "node": ">=6.0.0"
+                "node": ">=16"
+            },
+            "peerDependencies": {
+                "@tapjs/core": "1.3.4"
             }
         },
-        "node_modules/@babel/template": {
-            "version": "7.20.7",
-            "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz",
-            "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==",
+        "node_modules/@tapjs/config": {
+            "version": "2.4.0",
+            "resolved": "https://registry.npmjs.org/@tapjs/config/-/config-2.4.0.tgz",
+            "integrity": "sha512-iz8n4GFY8FM1kKro4W6kZ3mQvzjddL4j8ta1B08q9ix8K5ysfHnbamjh2syORVRGo/dZNMnKvfXTxFzZ+WIbDg==",
             "dev": true,
             "dependencies": {
-                "@babel/code-frame": "^7.18.6",
-                "@babel/parser": "^7.20.7",
-                "@babel/types": "^7.20.7"
+                "chalk": "^5.2.0",
+                "jackspeak": "^2.3.6",
+                "polite-json": "^4.0.1",
+                "walk-up-path": "^3.0.1"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">=16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            },
+            "peerDependencies": {
+                "@tapjs/core": "1.3.4",
+                "@tapjs/test": "1.3.4"
             }
         },
-        "node_modules/@babel/traverse": {
-            "version": "7.21.3",
-            "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.3.tgz",
-            "integrity": "sha512-XLyopNeaTancVitYZe2MlUEvgKb6YVVPXzofHgqHijCImG33b/uTurMS488ht/Hbsb2XK3U2BnSTxKVNGV3nGQ==",
+        "node_modules/@tapjs/config/node_modules/chalk": {
+            "version": "5.3.0",
+            "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
+            "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
+            "dev": true,
+            "engines": {
+                "node": "^12.17.0 || ^14.13 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/chalk?sponsor=1"
+            }
+        },
+        "node_modules/@tapjs/core": {
+            "version": "1.3.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/core/-/core-1.3.4.tgz",
+            "integrity": "sha512-EcINYx86gDzLeZAsHMckv4Fjd4TdYJ7KduvdhD0Qy4EhROjQnaY9lPQTQxT2uwaEjpWB2Pio3ahtLzNUT2lY1g==",
             "dev": true,
             "dependencies": {
-                "@babel/code-frame": "^7.18.6",
-                "@babel/generator": "^7.21.3",
-                "@babel/helper-environment-visitor": "^7.18.9",
-                "@babel/helper-function-name": "^7.21.0",
-                "@babel/helper-hoist-variables": "^7.18.6",
-                "@babel/helper-split-export-declaration": "^7.18.6",
-                "@babel/parser": "^7.21.3",
-                "@babel/types": "^7.21.3",
-                "debug": "^4.1.0",
-                "globals": "^11.1.0"
+                "@tapjs/processinfo": "^3.1.2",
+                "@tapjs/stack": "1.2.3",
+                "@tapjs/test": "1.3.4",
+                "async-hook-domain": "^4.0.1",
+                "is-actual-promise": "^1.0.0",
+                "jackspeak": "^2.3.6",
+                "minipass": "^7.0.3",
+                "signal-exit": "4.1",
+                "tap-parser": "15.2.0",
+                "tcompare": "6.4.0",
+                "trivial-deferred": "^2.0.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">=16"
             }
         },
-        "node_modules/@babel/traverse/node_modules/globals": {
-            "version": "11.12.0",
-            "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
-            "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+        "node_modules/@tapjs/error-serdes": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/@tapjs/error-serdes/-/error-serdes-1.1.0.tgz",
+            "integrity": "sha512-RAdsafCQ9fyudLY4EQPhfWQvRNddvSoXKEsZQWZC6G5QfdB/BYnSqaXggK5TD0XZ79Ja0ex3uB+5kBaaeLKtQA==",
             "dev": true,
+            "dependencies": {
+                "minipass": "^7.0.3"
+            },
             "engines": {
-                "node": ">=4"
+                "node": ">=16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/@babel/types": {
-            "version": "7.21.3",
-            "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.3.tgz",
-            "integrity": "sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==",
+        "node_modules/@tapjs/filter": {
+            "version": "1.2.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/filter/-/filter-1.2.4.tgz",
+            "integrity": "sha512-YHIjat67MuuO2SzSg2Hcwwm1Y1UJ1yvD20hyy6MYGrKG8vkaU1hSu4bBheRhJ2IyqJQVgSIM+raNctlN5Bpa/A==",
             "dev": true,
             "dependencies": {
-                "@babel/helper-string-parser": "^7.19.4",
-                "@babel/helper-validator-identifier": "^7.19.1",
-                "to-fast-properties": "^2.0.0"
+                "tcompare": "6.4.0",
+                "trivial-deferred": "^2.0.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">=16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            },
+            "peerDependencies": {
+                "@tapjs/core": "1.3.4"
             }
         },
-        "node_modules/@eslint-community/eslint-utils": {
-            "version": "4.4.0",
-            "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
-            "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+        "node_modules/@tapjs/fixture": {
+            "version": "1.2.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/fixture/-/fixture-1.2.4.tgz",
+            "integrity": "sha512-7PHkg7fbKRWThU017qkw92dovreQct3LCArUJ9OdZWFoPYRwYND7CKB3/x7qtnNftBFZbRzf562miH0+TLDDTQ==",
+            "dev": true,
             "dependencies": {
-                "eslint-visitor-keys": "^3.3.0"
+                "mkdirp": "^3.0.0",
+                "rimraf": "^5.0.5"
             },
             "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+                "node": ">=16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             },
             "peerDependencies": {
-                "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+                "@tapjs/core": "1.3.4"
             }
         },
-        "node_modules/@eslint-community/regexpp": {
-            "version": "4.5.0",
-            "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz",
-            "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==",
+        "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.3.10",
+            "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+            "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
+            "dev": true,
+            "dependencies": {
+                "foreground-child": "^3.1.0",
+                "jackspeak": "^2.3.5",
+                "minimatch": "^9.0.1",
+                "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+                "path-scurry": "^1.10.1"
+            },
+            "bin": {
+                "glob": "dist/esm/bin.mjs"
+            },
             "engines": {
-                "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+                "node": ">=16 || 14 >=14.17"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/@eslint/eslintrc": {
-            "version": "2.0.2",
-            "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz",
-            "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==",
+        "node_modules/@tapjs/fixture/node_modules/minimatch": {
+            "version": "9.0.3",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+            "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+            "dev": true,
             "dependencies": {
-                "ajv": "^6.12.4",
-                "debug": "^4.3.2",
-                "espree": "^9.5.1",
-                "globals": "^13.19.0",
-                "ignore": "^5.2.0",
-                "import-fresh": "^3.2.1",
-                "js-yaml": "^4.1.0",
-                "minimatch": "^3.1.2",
-                "strip-json-comments": "^3.1.1"
+                "brace-expansion": "^2.0.1"
             },
             "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+                "node": ">=16 || 14 >=14.17"
             },
             "funding": {
-                "url": "https://opencollective.com/eslint"
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/@eslint/js": {
-            "version": "8.37.0",
-            "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.37.0.tgz",
-            "integrity": "sha512-x5vzdtOOGgFVDCUs81QRB2+liax8rFg3+7hqM+QhBG0/G3F1ZsoYl97UrqgHgQ9KKT7G6c4V+aTUCgu/n22v1A==",
+        "node_modules/@tapjs/fixture/node_modules/rimraf": {
+            "version": "5.0.5",
+            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz",
+            "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==",
+            "dev": true,
+            "dependencies": {
+                "glob": "^10.3.7"
+            },
+            "bin": {
+                "rimraf": "dist/esm/bin.mjs"
+            },
             "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+                "node": ">=14"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/@humanwhocodes/config-array": {
-            "version": "0.11.8",
-            "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
-            "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
+        "node_modules/@tapjs/intercept": {
+            "version": "1.2.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/intercept/-/intercept-1.2.4.tgz",
+            "integrity": "sha512-aEPwa40DqJPmgnZRbED+hI1x3dSUn4o5rePW6I2ludRle3o1bHSSnucYsjhwNPz0LCpOH9q/UAivJPO66xyTBA==",
+            "dev": true,
             "dependencies": {
-                "@humanwhocodes/object-schema": "^1.2.1",
-                "debug": "^4.1.1",
-                "minimatch": "^3.0.5"
+                "@tapjs/after": "1.1.4",
+                "@tapjs/stack": "1.2.3"
             },
             "engines": {
-                "node": ">=10.10.0"
+                "node": ">=16"
+            },
+            "peerDependencies": {
+                "@tapjs/core": "1.3.4"
             }
         },
-        "node_modules/@humanwhocodes/module-importer": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
-            "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+        "node_modules/@tapjs/mock": {
+            "version": "1.2.2",
+            "resolved": "https://registry.npmjs.org/@tapjs/mock/-/mock-1.2.2.tgz",
+            "integrity": "sha512-5SgMRNaHgxjuna5YfVrT/l9bCTV4qePbqxNhwLWiL/l4fHMcF8CB7jMQ2IXsB8/0q9dKSuuxysOeiYSScNQcsA==",
+            "dev": true,
+            "dependencies": {
+                "@tapjs/after": "1.1.4",
+                "@tapjs/stack": "1.2.3",
+                "resolve-import": "^1.4.2",
+                "walk-up-path": "^3.0.1"
+            },
             "engines": {
-                "node": ">=12.22"
+                "node": ">=16"
             },
             "funding": {
-                "type": "github",
-                "url": "https://github.com/sponsors/nzakas"
+                "url": "https://github.com/sponsors/isaacs"
+            },
+            "peerDependencies": {
+                "@tapjs/core": "1.3.4"
             }
         },
-        "node_modules/@humanwhocodes/object-schema": {
-            "version": "1.2.1",
-            "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
-            "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA=="
+        "node_modules/@tapjs/node-serialize": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/node-serialize/-/node-serialize-1.1.4.tgz",
+            "integrity": "sha512-t0x4jC15jae4DviixIqb0v53eXkWdE3KkmKcf/eMGCqN7EL3lRyQRTOtjC3fJRWmdXYCGK/311DpoUfpgzL3sA==",
+            "dev": true,
+            "dependencies": {
+                "@tapjs/error-serdes": "1.1.0"
+            },
+            "engines": {
+                "node": ">=16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            },
+            "peerDependencies": {
+                "@tapjs/core": "1.3.4"
+            }
         },
-        "node_modules/@istanbuljs/load-nyc-config": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
-            "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==",
+        "node_modules/@tapjs/processinfo": {
+            "version": "3.1.2",
+            "resolved": "https://registry.npmjs.org/@tapjs/processinfo/-/processinfo-3.1.2.tgz",
+            "integrity": "sha512-O3lg1X7zy4sQs+jDYHu+njFQCC5hYJWRmmbLy9UVhgqQKZifS4DYqkoAedK3ixj5NQ1stMNmJGJxbEvJLw/NWA==",
             "dev": true,
             "dependencies": {
-                "camelcase": "^5.3.1",
-                "find-up": "^4.1.0",
-                "get-package-type": "^0.1.0",
-                "js-yaml": "^3.13.1",
-                "resolve-from": "^5.0.0"
+                "pirates": "^4.0.5",
+                "process-on-spawn": "^1.0.0",
+                "signal-exit": "^4.0.2",
+                "uuid": "^8.3.2"
             },
             "engines": {
-                "node": ">=8"
+                "node": ">=16.17"
             }
         },
-        "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": {
-            "version": "1.0.10",
-            "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
-            "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+        "node_modules/@tapjs/reporter": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmjs.org/@tapjs/reporter/-/reporter-1.3.0.tgz",
+            "integrity": "sha512-zjXwsZh895zUPM00w9q0W2u/y2ncTz4q/FYu3Jl8Ph0KcSTiGBob01Rj4+Uhhx0N5YwJxb4HOujRtAqhyqs7Gg==",
             "dev": true,
             "dependencies": {
-                "sprintf-js": "~1.0.2"
+                "@tapjs/config": "2.4.0",
+                "@tapjs/test": "1.3.4",
+                "chalk": "^5.2.0",
+                "ink": "^4.4.1",
+                "minipass": "^7.0.3",
+                "ms": "^2.1.3",
+                "patch-console": "^2.0.0",
+                "prismjs": "^1.29.0",
+                "prismjs-terminal": "^1.2.3",
+                "react": "^18.2.0",
+                "string-length": "^6.0.0",
+                "tcompare": "6.4.0"
+            },
+            "engines": {
+                "node": ">=16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            },
+            "peerDependencies": {
+                "@tapjs/core": "1.3.4"
             }
         },
-        "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
-            "version": "3.14.1",
-            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
-            "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+        "node_modules/@tapjs/reporter/node_modules/chalk": {
+            "version": "5.3.0",
+            "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
+            "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
+            "dev": true,
+            "engines": {
+                "node": "^12.17.0 || ^14.13 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/chalk?sponsor=1"
+            }
+        },
+        "node_modules/@tapjs/reporter/node_modules/ms": {
+            "version": "2.1.3",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+            "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+            "dev": true
+        },
+        "node_modules/@tapjs/run": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/@tapjs/run/-/run-1.4.0.tgz",
+            "integrity": "sha512-3LNRejFAos8iND30CiQV+RIdaiHBKjsLNq1BZ/nena7lcshKoQCFtiVpKMlqGAStMQgLygjgSo2uHbuSDD0Qww==",
+            "dev": true,
+            "dependencies": {
+                "@tapjs/after": "1.1.4",
+                "@tapjs/before": "1.1.4",
+                "@tapjs/config": "2.4.0",
+                "@tapjs/processinfo": "^3.1.2",
+                "@tapjs/reporter": "1.3.0",
+                "@tapjs/spawn": "1.1.4",
+                "@tapjs/stdin": "1.1.4",
+                "@tapjs/test": "1.3.4",
+                "c8": "^8.0.1",
+                "chokidar": "^3.5.3",
+                "foreground-child": "^3.1.1",
+                "glob": "^10.3.10",
+                "minipass": "^7.0.3",
+                "mkdirp": "^3.0.1",
+                "opener": "^1.5.2",
+                "pacote": "^17.0.3",
+                "path-scurry": "^1.9.2",
+                "resolve-import": "^1.4.2",
+                "rimraf": "^5.0.5",
+                "semver": "^7.5.4",
+                "signal-exit": "^4.1.0",
+                "tap-yaml": "2.2.0",
+                "tcompare": "6.4.0",
+                "trivial-deferred": "^2.0.0",
+                "ts-node": "npm:@isaacs/ts-node-temp-fork-for-pr-2009@^10.9.1",
+                "which": "^4.0.0"
+            },
+            "bin": {
+                "tap-run": "dist/esm/index.js"
+            },
+            "engines": {
+                "node": ">=16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            },
+            "peerDependencies": {
+                "@tapjs/core": "1.3.4"
+            }
+        },
+        "node_modules/@tapjs/run/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/run/node_modules/glob": {
+            "version": "10.3.10",
+            "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+            "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
             "dev": true,
             "dependencies": {
-                "argparse": "^1.0.7",
-                "esprima": "^4.0.0"
+                "foreground-child": "^3.1.0",
+                "jackspeak": "^2.3.5",
+                "minimatch": "^9.0.1",
+                "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+                "path-scurry": "^1.10.1"
             },
             "bin": {
-                "js-yaml": "bin/js-yaml.js"
+                "glob": "dist/esm/bin.mjs"
+            },
+            "engines": {
+                "node": ">=16 || 14 >=14.17"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": {
-            "version": "5.0.0",
-            "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
-            "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+        "node_modules/@tapjs/run/node_modules/isexe": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
+            "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
             "dev": true,
             "engines": {
-                "node": ">=8"
+                "node": ">=16"
             }
         },
-        "node_modules/@istanbuljs/schema": {
-            "version": "0.1.3",
-            "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
-            "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+        "node_modules/@tapjs/run/node_modules/minimatch": {
+            "version": "9.0.3",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+            "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
             "dev": true,
+            "dependencies": {
+                "brace-expansion": "^2.0.1"
+            },
             "engines": {
-                "node": ">=8"
+                "node": ">=16 || 14 >=14.17"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/@jridgewell/gen-mapping": {
-            "version": "0.1.1",
-            "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
-            "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==",
+        "node_modules/@tapjs/run/node_modules/rimraf": {
+            "version": "5.0.5",
+            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz",
+            "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==",
             "dev": true,
             "dependencies": {
-                "@jridgewell/set-array": "^1.0.0",
-                "@jridgewell/sourcemap-codec": "^1.4.10"
+                "glob": "^10.3.7"
+            },
+            "bin": {
+                "rimraf": "dist/esm/bin.mjs"
             },
             "engines": {
-                "node": ">=6.0.0"
+                "node": ">=14"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/@jridgewell/resolve-uri": {
-            "version": "3.1.0",
-            "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
-            "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
+        "node_modules/@tapjs/run/node_modules/which": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
+            "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
             "dev": true,
+            "dependencies": {
+                "isexe": "^3.1.1"
+            },
+            "bin": {
+                "node-which": "bin/which.js"
+            },
             "engines": {
-                "node": ">=6.0.0"
+                "node": "^16.13.0 || >=18.0.0"
             }
         },
-        "node_modules/@jridgewell/set-array": {
-            "version": "1.1.2",
-            "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
-            "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+        "node_modules/@tapjs/snapshot": {
+            "version": "1.2.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/snapshot/-/snapshot-1.2.4.tgz",
+            "integrity": "sha512-8pStZczbArIC6+s8TblHTs/Mr5RGApWZA91Eey5UuU5MX3IPUw77MPQpPOoh2zrefa8VZRmHM7IgQq8SKyYjyQ==",
             "dev": true,
+            "dependencies": {
+                "is-actual-promise": "^1.0.0",
+                "tcompare": "6.4.0",
+                "trivial-deferred": "^2.0.0"
+            },
             "engines": {
-                "node": ">=6.0.0"
+                "node": ">=16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            },
+            "peerDependencies": {
+                "@tapjs/core": "1.3.4"
             }
         },
-        "node_modules/@jridgewell/sourcemap-codec": {
-            "version": "1.4.14",
-            "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
-            "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
-            "dev": true
+        "node_modules/@tapjs/spawn": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/spawn/-/spawn-1.1.4.tgz",
+            "integrity": "sha512-H3/VBi/Zfnb53PbpNmT/OYhIdqk8k6pGnM+WNLB8KBzwLa23q75P0jSYAEhzX3sZO+JIiaHACj/SxvttFapDtg==",
+            "dev": true,
+            "engines": {
+                "node": ">=16"
+            },
+            "peerDependencies": {
+                "@tapjs/core": "1.3.4"
+            }
         },
-        "node_modules/@jridgewell/trace-mapping": {
-            "version": "0.3.17",
-            "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz",
-            "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==",
+        "node_modules/@tapjs/stack": {
+            "version": "1.2.3",
+            "resolved": "https://registry.npmjs.org/@tapjs/stack/-/stack-1.2.3.tgz",
+            "integrity": "sha512-LY7Rxse2QY+DczTCoqOA4rxjqhnCgXYZeynrhzOsiut6IVnDWnqjUvZMq1XYnk5G69lhgG5lTDHmZrKP33BKgg==",
             "dev": true,
             "dependencies": {
-                "@jridgewell/resolve-uri": "3.1.0",
-                "@jridgewell/sourcemap-codec": "1.4.14"
+                "tcompare": "6.4.0",
+                "trivial-deferred": "^2.0.0"
+            },
+            "engines": {
+                "node": ">=16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/@nodelib/fs.scandir": {
-            "version": "2.1.5",
-            "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
-            "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+        "node_modules/@tapjs/stdin": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/stdin/-/stdin-1.1.4.tgz",
+            "integrity": "sha512-yQzeiWaWRFd5jXVy3F0Q4inQqVmEGynFfWz2cbQYJFm/CNCcKFM1t4uIRRqtNdfJwSrr19m8Lq0qqfT7pHV/yg==",
+            "dev": true,
+            "engines": {
+                "node": ">=16"
+            },
+            "peerDependencies": {
+                "@tapjs/core": "1.3.4"
+            }
+        },
+        "node_modules/@tapjs/test": {
+            "version": "1.3.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/test/-/test-1.3.4.tgz",
+            "integrity": "sha512-ud2T10OhxdQw4f7Wo4G+5/Vyw5JYgfb5bDmKo0B3xmMgVvIFpUS/4V2Zq+59DZGXmEgjO0KPhb8NvOpOHAy/fg==",
+            "dev": true,
+            "dependencies": {
+                "@tapjs/after": "1.1.4",
+                "@tapjs/after-each": "1.1.4",
+                "@tapjs/asserts": "1.1.4",
+                "@tapjs/before": "1.1.4",
+                "@tapjs/before-each": "1.1.4",
+                "@tapjs/filter": "1.2.4",
+                "@tapjs/fixture": "1.2.4",
+                "@tapjs/intercept": "1.2.4",
+                "@tapjs/mock": "1.2.2",
+                "@tapjs/node-serialize": "1.1.4",
+                "@tapjs/snapshot": "1.2.4",
+                "@tapjs/spawn": "1.1.4",
+                "@tapjs/stdin": "1.1.4",
+                "@tapjs/typescript": "1.2.4",
+                "@tapjs/worker": "1.1.4",
+                "glob": "^10.3.10",
+                "jackspeak": "^2.3.6",
+                "mkdirp": "^3.0.0",
+                "resolve-import": "^1.4.1",
+                "rimraf": "^5.0.5",
+                "sync-content": "^1.0.1",
+                "tap-parser": "15.2.0",
+                "ts-node": "npm:@isaacs/ts-node-temp-fork-for-pr-2009@^10.9.1",
+                "tshy": "^1.2.2",
+                "typescript": "5.2"
+            },
+            "bin": {
+                "generate-tap-test-class": "scripts/build.mjs"
+            },
+            "engines": {
+                "node": ">=16"
+            },
+            "peerDependencies": {
+                "@tapjs/core": "1.3.4"
+            }
+        },
+        "node_modules/@tapjs/test/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": {
-                "@nodelib/fs.stat": "2.0.5",
-                "run-parallel": "^1.1.9"
+                "balanced-match": "^1.0.0"
+            }
+        },
+        "node_modules/@tapjs/test/node_modules/glob": {
+            "version": "10.3.10",
+            "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+            "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
+            "dev": true,
+            "dependencies": {
+                "foreground-child": "^3.1.0",
+                "jackspeak": "^2.3.5",
+                "minimatch": "^9.0.1",
+                "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+                "path-scurry": "^1.10.1"
+            },
+            "bin": {
+                "glob": "dist/esm/bin.mjs"
             },
             "engines": {
-                "node": ">= 8"
+                "node": ">=16 || 14 >=14.17"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/@nodelib/fs.stat": {
-            "version": "2.0.5",
-            "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
-            "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+        "node_modules/@tapjs/test/node_modules/minimatch": {
+            "version": "9.0.3",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+            "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+            "dev": true,
+            "dependencies": {
+                "brace-expansion": "^2.0.1"
+            },
             "engines": {
-                "node": ">= 8"
+                "node": ">=16 || 14 >=14.17"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/@nodelib/fs.walk": {
-            "version": "1.2.8",
-            "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
-            "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+        "node_modules/@tapjs/test/node_modules/rimraf": {
+            "version": "5.0.5",
+            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz",
+            "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==",
+            "dev": true,
             "dependencies": {
-                "@nodelib/fs.scandir": "2.1.5",
-                "fastq": "^1.6.0"
+                "glob": "^10.3.7"
+            },
+            "bin": {
+                "rimraf": "dist/esm/bin.mjs"
             },
             "engines": {
-                "node": ">= 8"
+                "node": ">=14"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            }
+        },
+        "node_modules/@tapjs/typescript": {
+            "version": "1.2.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/typescript/-/typescript-1.2.4.tgz",
+            "integrity": "sha512-exhSckFlKLr0RFHKYBJb3N6CftoafH5GwNeAWN0yua+FmzwDleGvgKThW3l/xeOF7BeCq/m4zu9HWrwjkPaDhQ==",
+            "dev": true,
+            "dependencies": {
+                "ts-node": "npm:@isaacs/ts-node-temp-fork-for-pr-2009@^10.9.1"
+            },
+            "engines": {
+                "node": ">=16"
+            },
+            "peerDependencies": {
+                "@tapjs/core": "1.3.4"
             }
         },
+        "node_modules/@tapjs/worker": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/worker/-/worker-1.1.4.tgz",
+            "integrity": "sha512-HcaafOWghXpMtLaCk8BOIMQcphZU2Gi0OSUb6vzgxKQ4iQxTsBkJSnZ1+4F8Qed9EWZ9n6zaggjy7/fDLVdJRg==",
+            "dev": true,
+            "engines": {
+                "node": ">=16"
+            },
+            "peerDependencies": {
+                "@tapjs/core": "1.3.4"
+            }
+        },
+        "node_modules/@tootallnate/once": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
+            "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
+            "dev": true,
+            "engines": {
+                "node": ">= 10"
+            }
+        },
+        "node_modules/@tsconfig/node14": {
+            "version": "14.1.0",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-14.1.0.tgz",
+            "integrity": "sha512-VmsCG04YR58ciHBeJKBDNMWWfYbyP8FekWVuTlpstaUPlat1D0x/tXzkWP7yCMU0eSz9V4OZU0LBWTFJ3xZf6w==",
+            "dev": true
+        },
+        "node_modules/@tsconfig/node16": {
+            "version": "16.1.1",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-16.1.1.tgz",
+            "integrity": "sha512-+pio93ejHN4nINX4pXqfnR/fPLRtJBaT4ORaa5RH0Oc1zoYmo2B2koG+M328CQhHKn1Wj6FcOxCDFXAot9NhvA==",
+            "dev": true
+        },
+        "node_modules/@tsconfig/node18": {
+            "version": "18.2.2",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-18.2.2.tgz",
+            "integrity": "sha512-d6McJeGsuoRlwWZmVIeE8CUA27lu6jLjvv1JzqmpsytOYYbVi1tHZEnwCNVOXnj4pyLvneZlFlpXUK+X9wBWyw==",
+            "dev": true
+        },
+        "node_modules/@tsconfig/node20": {
+            "version": "20.1.2",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.2.tgz",
+            "integrity": "sha512-madaWq2k+LYMEhmcp0fs+OGaLFk0OenpHa4gmI4VEmCKX4PJntQ6fnnGADVFrVkBj0wIdAlQnK/MrlYTHsa1gQ==",
+            "dev": true
+        },
+        "node_modules/@tufjs/canonical-json": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
+            "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==",
+            "dev": true,
+            "engines": {
+                "node": "^16.14.0 || >=18.0.0"
+            }
+        },
+        "node_modules/@tufjs/models": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-2.0.0.tgz",
+            "integrity": "sha512-c8nj8BaOExmZKO2DXhDfegyhSGcG9E/mPN3U13L+/PsoWm1uaGiHHjxqSHQiasDBQwDA3aHuw9+9spYAP1qvvg==",
+            "dev": true,
+            "dependencies": {
+                "@tufjs/canonical-json": "2.0.0",
+                "minimatch": "^9.0.3"
+            },
+            "engines": {
+                "node": "^16.14.0 || >=18.0.0"
+            }
+        },
+        "node_modules/@tufjs/models/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/@tufjs/models/node_modules/minimatch": {
+            "version": "9.0.3",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+            "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+            "dev": true,
+            "dependencies": {
+                "brace-expansion": "^2.0.1"
+            },
+            "engines": {
+                "node": ">=16 || 14 >=14.17"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            }
+        },
+        "node_modules/@types/istanbul-lib-coverage": {
+            "version": "2.0.4",
+            "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
+            "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==",
+            "dev": true
+        },
+        "node_modules/@types/node": {
+            "version": "20.8.0",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.0.tgz",
+            "integrity": "sha512-LzcWltT83s1bthcvjBmiBvGJiiUe84NWRHkw+ZV6Fr41z2FbIzvc815dk2nQ3RAKMuN2fkenM/z3Xv2QzEpYxQ==",
+            "dev": true,
+            "peer": true
+        },
+        "node_modules/abbrev": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+            "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+            "dev": true
+        },
         "node_modules/acorn": {
             "version": "8.8.2",
             "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
@@ -660,6 +1386,39 @@
                 "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
             }
         },
+        "node_modules/acorn-walk": {
+            "version": "8.2.0",
+            "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
+            "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
+            "dev": true,
+            "engines": {
+                "node": ">=0.4.0"
+            }
+        },
+        "node_modules/agent-base": {
+            "version": "6.0.2",
+            "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+            "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+            "dev": true,
+            "dependencies": {
+                "debug": "4"
+            },
+            "engines": {
+                "node": ">= 6.0.0"
+            }
+        },
+        "node_modules/agentkeepalive": {
+            "version": "4.5.0",
+            "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz",
+            "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==",
+            "dev": true,
+            "dependencies": {
+                "humanize-ms": "^1.2.1"
+            },
+            "engines": {
+                "node": ">= 8.0.0"
+            }
+        },
         "node_modules/aggregate-error": {
             "version": "3.1.0",
             "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
@@ -673,6 +1432,15 @@
                 "node": ">=8"
             }
         },
+        "node_modules/aggregate-error/node_modules/indent-string": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+            "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
         "node_modules/ajv": {
             "version": "6.12.6",
             "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -688,6 +1456,33 @@
                 "url": "https://github.com/sponsors/epoberezkin"
             }
         },
+        "node_modules/ansi-escapes": {
+            "version": "6.2.0",
+            "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz",
+            "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==",
+            "dev": true,
+            "dependencies": {
+                "type-fest": "^3.0.0"
+            },
+            "engines": {
+                "node": ">=14.16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/ansi-escapes/node_modules/type-fest": {
+            "version": "3.13.1",
+            "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz",
+            "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==",
+            "dev": true,
+            "engines": {
+                "node": ">=14.16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
         "node_modules/ansi-regex": {
             "version": "5.0.1",
             "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -723,22 +1518,29 @@
                 "node": ">= 8"
             }
         },
-        "node_modules/append-transform": {
+        "node_modules/aproba": {
             "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz",
-            "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==",
+            "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
+            "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
+            "dev": true
+        },
+        "node_modules/are-we-there-yet": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz",
+            "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==",
             "dev": true,
             "dependencies": {
-                "default-require-extensions": "^3.0.0"
+                "delegates": "^1.0.0",
+                "readable-stream": "^3.6.0"
             },
             "engines": {
-                "node": ">=8"
+                "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
             }
         },
-        "node_modules/archy": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz",
-            "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==",
+        "node_modules/arg": {
+            "version": "4.1.3",
+            "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+            "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
             "dev": true
         },
         "node_modules/argparse": {
@@ -747,12 +1549,24 @@
             "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
         },
         "node_modules/async-hook-domain": {
-            "version": "2.0.4",
-            "resolved": "https://registry.npmjs.org/async-hook-domain/-/async-hook-domain-2.0.4.tgz",
-            "integrity": "sha512-14LjCmlK1PK8eDtTezR6WX8TMaYNIzBIsd2D1sGoGjgx0BuNMMoSdk7i/drlbtamy0AWv9yv2tkB+ASdmeqFIw==",
+            "version": "4.0.1",
+            "resolved": "https://registry.npmjs.org/async-hook-domain/-/async-hook-domain-4.0.1.tgz",
+            "integrity": "sha512-bSktexGodAjfHWIrSrrqxqWzf1hWBZBpmPNZv+TYUMyWa2eoefFc6q6H1+KtdHYSz35lrhWdmXt/XK9wNEZvww==",
             "dev": true,
             "engines": {
-                "node": ">=10"
+                "node": ">=16"
+            }
+        },
+        "node_modules/auto-bind": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz",
+            "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==",
+            "dev": true,
+            "engines": {
+                "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
             }
         },
         "node_modules/balanced-match": {
@@ -769,15 +1583,6 @@
                 "node": ">=8"
             }
         },
-        "node_modules/bind-obj-methods": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/bind-obj-methods/-/bind-obj-methods-3.0.0.tgz",
-            "integrity": "sha512-nLEaaz3/sEzNSyPWRsN9HNsqwk1AUyECtGj+XwGdIi3xABnEqecvXtIJ0wehQXuuER5uZ/5fTs2usONgYjG+iw==",
-            "dev": true,
-            "engines": {
-                "node": ">=10"
-            }
-        },
         "node_modules/brace-expansion": {
             "version": "1.1.11",
             "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -799,87 +1604,136 @@
                 "node": ">=8"
             }
         },
-        "node_modules/browserslist": {
-            "version": "4.21.5",
-            "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz",
-            "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==",
+        "node_modules/builtins": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz",
+            "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==",
             "dev": true,
-            "funding": [
-                {
-                    "type": "opencollective",
-                    "url": "https://opencollective.com/browserslist"
-                },
-                {
-                    "type": "tidelift",
-                    "url": "https://tidelift.com/funding/github/npm/browserslist"
-                }
-            ],
             "dependencies": {
-                "caniuse-lite": "^1.0.30001449",
-                "electron-to-chromium": "^1.4.284",
-                "node-releases": "^2.0.8",
-                "update-browserslist-db": "^1.0.10"
+                "semver": "^7.0.0"
+            }
+        },
+        "node_modules/c8": {
+            "version": "8.0.1",
+            "resolved": "https://registry.npmjs.org/c8/-/c8-8.0.1.tgz",
+            "integrity": "sha512-EINpopxZNH1mETuI0DzRA4MZpAUH+IFiRhnmFD3vFr3vdrgxqi3VfE3KL0AIL+zDq8rC9bZqwM/VDmmoe04y7w==",
+            "dev": true,
+            "dependencies": {
+                "@bcoe/v8-coverage": "^0.2.3",
+                "@istanbuljs/schema": "^0.1.3",
+                "find-up": "^5.0.0",
+                "foreground-child": "^2.0.0",
+                "istanbul-lib-coverage": "^3.2.0",
+                "istanbul-lib-report": "^3.0.1",
+                "istanbul-reports": "^3.1.6",
+                "rimraf": "^3.0.2",
+                "test-exclude": "^6.0.0",
+                "v8-to-istanbul": "^9.0.0",
+                "yargs": "^17.7.2",
+                "yargs-parser": "^21.1.1"
             },
             "bin": {
-                "browserslist": "cli.js"
+                "c8": "bin/c8.js"
             },
             "engines": {
-                "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+                "node": ">=12"
             }
         },
-        "node_modules/buffer-from": {
-            "version": "1.1.2",
-            "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
-            "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+        "node_modules/c8/node_modules/foreground-child": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz",
+            "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==",
+            "dev": true,
+            "dependencies": {
+                "cross-spawn": "^7.0.0",
+                "signal-exit": "^3.0.2"
+            },
+            "engines": {
+                "node": ">=8.0.0"
+            }
+        },
+        "node_modules/c8/node_modules/signal-exit": {
+            "version": "3.0.7",
+            "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+            "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
             "dev": true
         },
-        "node_modules/caching-transform": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz",
-            "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==",
+        "node_modules/cacache": {
+            "version": "18.0.0",
+            "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.0.tgz",
+            "integrity": "sha512-I7mVOPl3PUCeRub1U8YoGz2Lqv9WOBpobZ8RyWFXmReuILz+3OAyTa5oH3QPdtKZD7N0Yk00aLfzn0qvp8dZ1w==",
             "dev": true,
             "dependencies": {
-                "hasha": "^5.0.0",
-                "make-dir": "^3.0.0",
-                "package-hash": "^4.0.0",
-                "write-file-atomic": "^3.0.0"
+                "@npmcli/fs": "^3.1.0",
+                "fs-minipass": "^3.0.0",
+                "glob": "^10.2.2",
+                "lru-cache": "^10.0.1",
+                "minipass": "^7.0.3",
+                "minipass-collect": "^1.0.2",
+                "minipass-flush": "^1.0.5",
+                "minipass-pipeline": "^1.2.4",
+                "p-map": "^4.0.0",
+                "ssri": "^10.0.0",
+                "tar": "^6.1.11",
+                "unique-filename": "^3.0.0"
             },
             "engines": {
-                "node": ">=8"
+                "node": "^16.14.0 || >=18.0.0"
             }
         },
-        "node_modules/callsites": {
-            "version": "3.1.0",
-            "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
-            "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
-            "engines": {
-                "node": ">=6"
+        "node_modules/cacache/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/camelcase": {
-            "version": "5.3.1",
-            "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
-            "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+        "node_modules/cacache/node_modules/glob": {
+            "version": "10.3.10",
+            "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+            "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
             "dev": true,
+            "dependencies": {
+                "foreground-child": "^3.1.0",
+                "jackspeak": "^2.3.5",
+                "minimatch": "^9.0.1",
+                "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+                "path-scurry": "^1.10.1"
+            },
+            "bin": {
+                "glob": "dist/esm/bin.mjs"
+            },
             "engines": {
-                "node": ">=6"
+                "node": ">=16 || 14 >=14.17"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/caniuse-lite": {
-            "version": "1.0.30001469",
-            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001469.tgz",
-            "integrity": "sha512-Rcp7221ScNqQPP3W+lVOYDyjdR6dC+neEQCttoNr5bAyz54AboB4iwpnWgyi8P4YUsPybVzT4LgWiBbI3drL4g==",
+        "node_modules/cacache/node_modules/minimatch": {
+            "version": "9.0.3",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+            "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
             "dev": true,
-            "funding": [
-                {
-                    "type": "opencollective",
-                    "url": "https://opencollective.com/browserslist"
-                },
-                {
-                    "type": "tidelift",
-                    "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
-                }
-            ]
+            "dependencies": {
+                "brace-expansion": "^2.0.1"
+            },
+            "engines": {
+                "node": ">=16 || 14 >=14.17"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            }
+        },
+        "node_modules/callsites": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+            "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+            "engines": {
+                "node": ">=6"
+            }
         },
         "node_modules/chalk": {
             "version": "4.1.2",
@@ -935,11 +1789,35 @@
                 "node": ">= 6"
             }
         },
+        "node_modules/chownr": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+            "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+            "dev": true,
+            "engines": {
+                "node": ">=10"
+            }
+        },
         "node_modules/chroma-js": {
             "version": "2.4.2",
             "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz",
             "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A=="
         },
+        "node_modules/ci-info": {
+            "version": "3.8.0",
+            "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
+            "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/sibiraj-s"
+                }
+            ],
+            "engines": {
+                "node": ">=8"
+            }
+        },
         "node_modules/clean-stack": {
             "version": "2.2.0",
             "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
@@ -949,15 +1827,147 @@
                 "node": ">=6"
             }
         },
+        "node_modules/cli-boxes": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz",
+            "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==",
+            "dev": true,
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/cli-cursor": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
+            "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==",
+            "dev": true,
+            "dependencies": {
+                "restore-cursor": "^4.0.0"
+            },
+            "engines": {
+                "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/cli-truncate": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz",
+            "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==",
+            "dev": true,
+            "dependencies": {
+                "slice-ansi": "^5.0.0",
+                "string-width": "^5.0.0"
+            },
+            "engines": {
+                "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/cli-truncate/node_modules/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,
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+            }
+        },
+        "node_modules/cli-truncate/node_modules/slice-ansi": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
+            "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
+            "dev": true,
+            "dependencies": {
+                "ansi-styles": "^6.0.0",
+                "is-fullwidth-code-point": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+            }
+        },
         "node_modules/cliui": {
-            "version": "7.0.4",
-            "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
-            "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+            "version": "8.0.1",
+            "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+            "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
             "dev": true,
             "dependencies": {
                 "string-width": "^4.2.0",
-                "strip-ansi": "^6.0.0",
+                "strip-ansi": "^6.0.1",
                 "wrap-ansi": "^7.0.0"
+            },
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/cliui/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
+        },
+        "node_modules/cliui/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"
+            }
+        },
+        "node_modules/cliui/node_modules/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,
+            "dependencies": {
+                "emoji-regex": "^8.0.0",
+                "is-fullwidth-code-point": "^3.0.0",
+                "strip-ansi": "^6.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/cliui/node_modules/wrap-ansi": {
+            "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",
+                "strip-ansi": "^6.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+            }
+        },
+        "node_modules/code-excerpt": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz",
+            "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==",
+            "dev": true,
+            "dependencies": {
+                "convert-to-spaces": "^2.0.1"
+            },
+            "engines": {
+                "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
             }
         },
         "node_modules/color-convert": {
@@ -990,23 +2000,32 @@
             "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz",
             "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w=="
         },
-        "node_modules/commondir": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
-            "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
-            "dev": true
-        },
         "node_modules/concat-map": {
             "version": "0.0.1",
             "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
             "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
         },
+        "node_modules/console-control-strings": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+            "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
+            "dev": true
+        },
         "node_modules/convert-source-map": {
             "version": "1.9.0",
             "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
             "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
             "dev": true
         },
+        "node_modules/convert-to-spaces": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz",
+            "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==",
+            "dev": true,
+            "engines": {
+                "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+            }
+        },
         "node_modules/cross-spawn": {
             "version": "7.0.3",
             "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -1036,34 +2055,16 @@
                 }
             }
         },
-        "node_modules/decamelize": {
-            "version": "1.2.0",
-            "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
-            "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/deep-is": {
             "version": "0.1.4",
             "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
             "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
         },
-        "node_modules/default-require-extensions": {
-            "version": "3.0.1",
-            "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz",
-            "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==",
-            "dev": true,
-            "dependencies": {
-                "strip-bom": "^4.0.0"
-            },
-            "engines": {
-                "node": ">=8"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
+        "node_modules/delegates": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+            "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
+            "dev": true
         },
         "node_modules/diff": {
             "version": "4.0.2",
@@ -1085,22 +2086,41 @@
                 "node": ">=6.0.0"
             }
         },
-        "node_modules/electron-to-chromium": {
-            "version": "1.4.340",
-            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.340.tgz",
-            "integrity": "sha512-zx8hqumOqltKsv/MF50yvdAlPF9S/4PXbyfzJS6ZGhbddGkRegdwImmfSVqCkEziYzrIGZ/TlrzBND4FysfkDg==",
+        "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
         },
         "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==",
+            "version": "9.2.2",
+            "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+            "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
             "dev": true
         },
-        "node_modules/es6-error": {
-            "version": "4.1.1",
-            "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
-            "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
+        "node_modules/encoding": {
+            "version": "0.1.13",
+            "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
+            "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
+            "dev": true,
+            "optional": true,
+            "dependencies": {
+                "iconv-lite": "^0.6.2"
+            }
+        },
+        "node_modules/env-paths": {
+            "version": "2.2.1",
+            "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+            "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+            "dev": true,
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/err-code": {
+            "version": "2.0.3",
+            "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
+            "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
             "dev": true
         },
         "node_modules/escalade": {
@@ -1202,63 +2222,6 @@
                 "url": "https://opencollective.com/eslint"
             }
         },
-        "node_modules/eslint/node_modules/find-up": {
-            "version": "5.0.0",
-            "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
-            "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
-            "dependencies": {
-                "locate-path": "^6.0.0",
-                "path-exists": "^4.0.0"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/eslint/node_modules/locate-path": {
-            "version": "6.0.0",
-            "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
-            "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
-            "dependencies": {
-                "p-locate": "^5.0.0"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/eslint/node_modules/p-limit": {
-            "version": "3.1.0",
-            "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
-            "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
-            "dependencies": {
-                "yocto-queue": "^0.1.0"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/eslint/node_modules/p-locate": {
-            "version": "5.0.0",
-            "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
-            "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
-            "dependencies": {
-                "p-limit": "^3.0.2"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
         "node_modules/espree": {
             "version": "9.5.1",
             "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz",
@@ -1275,19 +2238,6 @@
                 "url": "https://opencollective.com/eslint"
             }
         },
-        "node_modules/esprima": {
-            "version": "4.0.1",
-            "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
-            "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
-            "dev": true,
-            "bin": {
-                "esparse": "bin/esparse.js",
-                "esvalidate": "bin/esvalidate.js"
-            },
-            "engines": {
-                "node": ">=4"
-            }
-        },
         "node_modules/esquery": {
             "version": "1.5.0",
             "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
@@ -1327,9 +2277,18 @@
             }
         },
         "node_modules/events-to-array": {
-            "version": "1.1.2",
-            "resolved": "https://registry.npmjs.org/events-to-array/-/events-to-array-1.1.2.tgz",
-            "integrity": "sha512-inRWzRY7nG+aXZxBzEqYKB3HPgwflZRopAjDCHv0whhRx+MTUr1ei0ICZUypdyE0HRm4L2d5VEcIqLD6yl+BFA==",
+            "version": "2.0.3",
+            "resolved": "https://registry.npmjs.org/events-to-array/-/events-to-array-2.0.3.tgz",
+            "integrity": "sha512-f/qE2gImHRa4Cp2y1stEOSgw8wTFyUdVJX7G//bMwbaV9JqISFxg99NbmVQeP7YLnDUZ2un851jlaDrlpmGehQ==",
+            "dev": true,
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/exponential-backoff": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz",
+            "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==",
             "dev": true
         },
         "node_modules/fast-deep-equal": {
@@ -1378,42 +2337,21 @@
                 "node": ">=8"
             }
         },
-        "node_modules/find-cache-dir": {
-            "version": "3.3.2",
-            "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
-            "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
-            "dev": true,
-            "dependencies": {
-                "commondir": "^1.0.1",
-                "make-dir": "^3.0.2",
-                "pkg-dir": "^4.1.0"
-            },
-            "engines": {
-                "node": ">=8"
-            },
-            "funding": {
-                "url": "https://github.com/avajs/find-cache-dir?sponsor=1"
-            }
-        },
         "node_modules/find-up": {
-            "version": "4.1.0",
-            "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
-            "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
-            "dev": true,
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+            "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
             "dependencies": {
-                "locate-path": "^5.0.0",
+                "locate-path": "^6.0.0",
                 "path-exists": "^4.0.0"
             },
             "engines": {
-                "node": ">=8"
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/findit": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/findit/-/findit-2.0.0.tgz",
-            "integrity": "sha512-ENZS237/Hr8bjczn5eKuBohLgaD0JyUd0arxretR1f9RO46vZHA1b2y0VorgGV3WaOT3c+78P8h7v4JGJ1i/rg==",
-            "dev": true
-        },
         "node_modules/flat-cache": {
             "version": "3.0.4",
             "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
@@ -1432,16 +2370,19 @@
             "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg=="
         },
         "node_modules/foreground-child": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz",
-            "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==",
+            "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": "^3.0.2"
+                "signal-exit": "^4.0.1"
             },
             "engines": {
-                "node": ">=8.0.0"
+                "node": ">=14"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
         "node_modules/fromentries": {
@@ -1464,11 +2405,17 @@
                 }
             ]
         },
-        "node_modules/fs-exists-cached": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/fs-exists-cached/-/fs-exists-cached-1.0.0.tgz",
-            "integrity": "sha512-kSxoARUDn4F2RPXX48UXnaFKwVU7Ivd/6qpzZL29MCDmr9sTvybv4gFCp+qaI4fM9m0z9fgz/yJvi56GAz+BZg==",
-            "dev": true
+        "node_modules/fs-minipass": {
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz",
+            "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==",
+            "dev": true,
+            "dependencies": {
+                "minipass": "^7.0.3"
+            },
+            "engines": {
+                "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+            }
         },
         "node_modules/fs.realpath": {
             "version": "1.0.0",
@@ -1489,19 +2436,70 @@
                 "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
             }
         },
+        "node_modules/function-bind": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+            "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+            "dev": true
+        },
         "node_modules/function-loop": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmjs.org/function-loop/-/function-loop-2.0.1.tgz",
-            "integrity": "sha512-ktIR+O6i/4h+j/ZhZJNdzeI4i9lEPeEK6UPR2EVyTVBqOwcU3Za9xYKLH64ZR9HmcROyRrOkizNyjjtWJzDDkQ==",
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/function-loop/-/function-loop-4.0.0.tgz",
+            "integrity": "sha512-f34iQBedYF3XcI93uewZZOnyscDragxgTK/eTvVB74k3fCD0ZorOi5BV9GS4M8rz/JoNi0Kl3qX5Y9MH3S/CLQ==",
+            "dev": true
+        },
+        "node_modules/gauge": {
+            "version": "4.0.4",
+            "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz",
+            "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==",
+            "dev": true,
+            "dependencies": {
+                "aproba": "^1.0.3 || ^2.0.0",
+                "color-support": "^1.1.3",
+                "console-control-strings": "^1.1.0",
+                "has-unicode": "^2.0.1",
+                "signal-exit": "^3.0.7",
+                "string-width": "^4.2.3",
+                "strip-ansi": "^6.0.1",
+                "wide-align": "^1.1.5"
+            },
+            "engines": {
+                "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+            }
+        },
+        "node_modules/gauge/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
         },
-        "node_modules/gensync": {
-            "version": "1.0.0-beta.2",
-            "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
-            "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+        "node_modules/gauge/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": ">=6.9.0"
+                "node": ">=8"
+            }
+        },
+        "node_modules/gauge/node_modules/signal-exit": {
+            "version": "3.0.7",
+            "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+            "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+            "dev": true
+        },
+        "node_modules/gauge/node_modules/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,
+            "dependencies": {
+                "emoji-regex": "^8.0.0",
+                "is-fullwidth-code-point": "^3.0.0",
+                "strip-ansi": "^6.0.1"
+            },
+            "engines": {
+                "node": ">=8"
             }
         },
         "node_modules/get-caller-file": {
@@ -1513,15 +2511,6 @@
                 "node": "6.* || 8.* || >= 10.*"
             }
         },
-        "node_modules/get-package-type": {
-            "version": "0.1.0",
-            "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
-            "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
-            "dev": true,
-            "engines": {
-                "node": ">=8.0.0"
-            }
-        },
         "node_modules/glob": {
             "version": "7.2.3",
             "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -1577,39 +2566,32 @@
             "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
             "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ=="
         },
-        "node_modules/has-flag": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-            "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/hasha": {
-            "version": "5.2.2",
-            "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz",
-            "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==",
+        "node_modules/has": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+            "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
             "dev": true,
             "dependencies": {
-                "is-stream": "^2.0.0",
-                "type-fest": "^0.8.0"
+                "function-bind": "^1.1.1"
             },
             "engines": {
-                "node": ">=8"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
+                "node": ">= 0.4.0"
             }
         },
-        "node_modules/hasha/node_modules/type-fest": {
-            "version": "0.8.1",
-            "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
-            "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
-            "dev": true,
+        "node_modules/has-flag": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+            "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
             "engines": {
                 "node": ">=8"
             }
         },
+        "node_modules/has-unicode": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+            "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
+            "dev": true
+        },
         "node_modules/he": {
             "version": "1.2.0",
             "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
@@ -1618,12 +2600,79 @@
                 "he": "bin/he"
             }
         },
+        "node_modules/hosted-git-info": {
+            "version": "7.0.1",
+            "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz",
+            "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==",
+            "dev": true,
+            "dependencies": {
+                "lru-cache": "^10.0.1"
+            },
+            "engines": {
+                "node": "^16.14.0 || >=18.0.0"
+            }
+        },
         "node_modules/html-escaper": {
             "version": "2.0.2",
             "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
             "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
             "dev": true
         },
+        "node_modules/http-cache-semantics": {
+            "version": "4.1.1",
+            "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
+            "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
+            "dev": true
+        },
+        "node_modules/http-proxy-agent": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+            "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+            "dev": true,
+            "dependencies": {
+                "@tootallnate/once": "2",
+                "agent-base": "6",
+                "debug": "4"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/https-proxy-agent": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+            "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+            "dev": true,
+            "dependencies": {
+                "agent-base": "6",
+                "debug": "4"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/humanize-ms": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
+            "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
+            "dev": true,
+            "dependencies": {
+                "ms": "^2.0.0"
+            }
+        },
+        "node_modules/iconv-lite": {
+            "version": "0.6.3",
+            "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+            "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+            "dev": true,
+            "optional": true,
+            "dependencies": {
+                "safer-buffer": ">= 2.1.2 < 3.0.0"
+            },
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
         "node_modules/ignore": {
             "version": "5.2.4",
             "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
@@ -1632,6 +2681,56 @@
                 "node": ">= 4"
             }
         },
+        "node_modules/ignore-walk": {
+            "version": "6.0.3",
+            "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.3.tgz",
+            "integrity": "sha512-C7FfFoTA+bI10qfeydT8aZbvr91vAEU+2W5BZUlzPec47oNb07SsOfwYrtxuvOYdUApPP/Qlh4DtAO51Ekk2QA==",
+            "dev": true,
+            "dependencies": {
+                "minimatch": "^9.0.0"
+            },
+            "engines": {
+                "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+            }
+        },
+        "node_modules/ignore-walk/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/ignore-walk/node_modules/minimatch": {
+            "version": "9.0.3",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+            "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+            "dev": true,
+            "dependencies": {
+                "brace-expansion": "^2.0.1"
+            },
+            "engines": {
+                "node": ">=16 || 14 >=14.17"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            }
+        },
+        "node_modules/image-size": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.2.tgz",
+            "integrity": "sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg==",
+            "dependencies": {
+                "queue": "6.0.2"
+            },
+            "bin": {
+                "image-size": "bin/image-size.js"
+            },
+            "engines": {
+                "node": ">=14.0.0"
+            }
+        },
         "node_modules/import-fresh": {
             "version": "3.3.0",
             "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -1656,12 +2755,15 @@
             }
         },
         "node_modules/indent-string": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
-            "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz",
+            "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==",
             "dev": true,
             "engines": {
-                "node": ">=8"
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
             }
         },
         "node_modules/inflight": {
@@ -1678,6 +2780,97 @@
             "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
             "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
         },
+        "node_modules/ink": {
+            "version": "4.4.1",
+            "resolved": "https://registry.npmjs.org/ink/-/ink-4.4.1.tgz",
+            "integrity": "sha512-rXckvqPBB0Krifk5rn/5LvQGmyXwCUpBfmTwbkQNBY9JY8RSl3b8OftBNEYxg4+SWUhEKcPifgope28uL9inlA==",
+            "dev": true,
+            "dependencies": {
+                "@alcalzone/ansi-tokenize": "^0.1.3",
+                "ansi-escapes": "^6.0.0",
+                "auto-bind": "^5.0.1",
+                "chalk": "^5.2.0",
+                "cli-boxes": "^3.0.0",
+                "cli-cursor": "^4.0.0",
+                "cli-truncate": "^3.1.0",
+                "code-excerpt": "^4.0.0",
+                "indent-string": "^5.0.0",
+                "is-ci": "^3.0.1",
+                "is-lower-case": "^2.0.2",
+                "is-upper-case": "^2.0.2",
+                "lodash": "^4.17.21",
+                "patch-console": "^2.0.0",
+                "react-reconciler": "^0.29.0",
+                "scheduler": "^0.23.0",
+                "signal-exit": "^3.0.7",
+                "slice-ansi": "^6.0.0",
+                "stack-utils": "^2.0.6",
+                "string-width": "^5.1.2",
+                "type-fest": "^0.12.0",
+                "widest-line": "^4.0.1",
+                "wrap-ansi": "^8.1.0",
+                "ws": "^8.12.0",
+                "yoga-wasm-web": "~0.3.3"
+            },
+            "engines": {
+                "node": ">=14.16"
+            },
+            "peerDependencies": {
+                "@types/react": ">=18.0.0",
+                "react": ">=18.0.0",
+                "react-devtools-core": "^4.19.1"
+            },
+            "peerDependenciesMeta": {
+                "@types/react": {
+                    "optional": true
+                },
+                "react-devtools-core": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/ink/node_modules/chalk": {
+            "version": "5.3.0",
+            "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
+            "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
+            "dev": true,
+            "engines": {
+                "node": "^12.17.0 || ^14.13 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/chalk?sponsor=1"
+            }
+        },
+        "node_modules/ink/node_modules/signal-exit": {
+            "version": "3.0.7",
+            "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+            "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+            "dev": true
+        },
+        "node_modules/ink/node_modules/type-fest": {
+            "version": "0.12.0",
+            "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.12.0.tgz",
+            "integrity": "sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg==",
+            "dev": true,
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/ip": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
+            "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==",
+            "dev": true
+        },
+        "node_modules/is-actual-promise": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/is-actual-promise/-/is-actual-promise-1.0.0.tgz",
+            "integrity": "sha512-DWSmKTiEoY3Y9LGHG9TVnFgydCCu+3fLJi4rv3fpi0gL/lKoILekh/oF/nO3/Lq1l5Rqo+tQt5TWzxMmYIhWyg==",
+            "dev": true
+        },
         "node_modules/is-binary-path": {
             "version": "2.1.0",
             "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -1690,6 +2883,30 @@
                 "node": ">=8"
             }
         },
+        "node_modules/is-ci": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
+            "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==",
+            "dev": true,
+            "dependencies": {
+                "ci-info": "^3.2.0"
+            },
+            "bin": {
+                "is-ci": "bin.js"
+            }
+        },
+        "node_modules/is-core-module": {
+            "version": "2.13.0",
+            "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz",
+            "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==",
+            "dev": true,
+            "dependencies": {
+                "has": "^1.0.3"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
         "node_modules/is-extglob": {
             "version": "2.1.1",
             "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -1699,12 +2916,15 @@
             }
         },
         "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==",
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
+            "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
             "dev": true,
             "engines": {
-                "node": ">=8"
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
             }
         },
         "node_modules/is-glob": {
@@ -1718,6 +2938,21 @@
                 "node": ">=0.10.0"
             }
         },
+        "node_modules/is-lambda": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz",
+            "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==",
+            "dev": true
+        },
+        "node_modules/is-lower-case": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/is-lower-case/-/is-lower-case-2.0.2.tgz",
+            "integrity": "sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ==",
+            "dev": true,
+            "dependencies": {
+                "tslib": "^2.0.3"
+            }
+        },
         "node_modules/is-number": {
             "version": "7.0.0",
             "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -1735,31 +2970,22 @@
                 "node": ">=8"
             }
         },
-        "node_modules/is-stream": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
-            "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+        "node_modules/is-plain-object": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+            "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
             "dev": true,
             "engines": {
-                "node": ">=8"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
+                "node": ">=0.10.0"
             }
         },
-        "node_modules/is-typedarray": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
-            "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
-            "dev": true
-        },
-        "node_modules/is-windows": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
-            "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
+        "node_modules/is-upper-case": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/is-upper-case/-/is-upper-case-2.0.2.tgz",
+            "integrity": "sha512-44pxmxAvnnAOwBg4tHPnkfvgjPwbc5QIsSstNU+YcJ1ovxVzCWpSGosPJOZh/a1tdl81fbgnLc9LLv+x2ywbPQ==",
             "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
+            "dependencies": {
+                "tslib": "^2.0.3"
             }
         },
         "node_modules/isexe": {
@@ -1776,82 +3002,24 @@
                 "node": ">=8"
             }
         },
-        "node_modules/istanbul-lib-hook": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz",
-            "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==",
-            "dev": true,
-            "dependencies": {
-                "append-transform": "^2.0.0"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/istanbul-lib-instrument": {
-            "version": "4.0.3",
-            "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz",
-            "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==",
-            "dev": true,
-            "dependencies": {
-                "@babel/core": "^7.7.5",
-                "@istanbuljs/schema": "^0.1.2",
-                "istanbul-lib-coverage": "^3.0.0",
-                "semver": "^6.3.0"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/istanbul-lib-processinfo": {
-            "version": "2.0.3",
-            "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz",
-            "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==",
-            "dev": true,
-            "dependencies": {
-                "archy": "^1.0.0",
-                "cross-spawn": "^7.0.3",
-                "istanbul-lib-coverage": "^3.2.0",
-                "p-map": "^3.0.0",
-                "rimraf": "^3.0.0",
-                "uuid": "^8.3.2"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/istanbul-lib-report": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz",
-            "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==",
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+            "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
             "dev": true,
             "dependencies": {
                 "istanbul-lib-coverage": "^3.0.0",
-                "make-dir": "^3.0.0",
+                "make-dir": "^4.0.0",
                 "supports-color": "^7.1.0"
             },
             "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/istanbul-lib-source-maps": {
-            "version": "4.0.1",
-            "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz",
-            "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==",
-            "dev": true,
-            "dependencies": {
-                "debug": "^4.1.1",
-                "istanbul-lib-coverage": "^3.0.0",
-                "source-map": "^0.6.1"
-            },
-            "engines": {
                 "node": ">=10"
             }
         },
         "node_modules/istanbul-reports": {
-            "version": "3.1.5",
-            "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz",
-            "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==",
+            "version": "3.1.6",
+            "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz",
+            "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==",
             "dev": true,
             "dependencies": {
                 "html-escaper": "^2.0.0",
@@ -1862,15 +3030,21 @@
             }
         },
         "node_modules/jackspeak": {
-            "version": "1.4.2",
-            "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-1.4.2.tgz",
-            "integrity": "sha512-GHeGTmnuaHnvS+ZctRB01bfxARuu9wW83ENbuiweu07SFcVlZrJpcshSre/keGT7YGBhLHg/+rXCNSrsEHKU4Q==",
+            "version": "2.3.6",
+            "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
+            "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
             "dev": true,
             "dependencies": {
-                "cliui": "^7.0.4"
+                "@isaacs/cliui": "^8.0.2"
             },
             "engines": {
-                "node": ">=8"
+                "node": ">=14"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            },
+            "optionalDependencies": {
+                "@pkgjs/parseargs": "^0.11.0"
             }
         },
         "node_modules/js-sdsl": {
@@ -1899,16 +3073,13 @@
                 "js-yaml": "bin/js-yaml.js"
             }
         },
-        "node_modules/jsesc": {
-            "version": "2.5.2",
-            "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
-            "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
+        "node_modules/json-parse-even-better-errors": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz",
+            "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==",
             "dev": true,
-            "bin": {
-                "jsesc": "bin/jsesc"
-            },
             "engines": {
-                "node": ">=4"
+                "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
             }
         },
         "node_modules/json-schema-traverse": {
@@ -1921,17 +3092,14 @@
             "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
             "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="
         },
-        "node_modules/json5": {
-            "version": "2.2.3",
-            "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
-            "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+        "node_modules/jsonparse": {
+            "version": "1.3.1",
+            "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
+            "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==",
             "dev": true,
-            "bin": {
-                "json5": "lib/cli.js"
-            },
-            "engines": {
-                "node": ">=6"
-            }
+            "engines": [
+                "node >= 0.2.0"
+            ]
         },
         "node_modules/levn": {
             "version": "0.4.1",
@@ -1945,61 +3113,24 @@
                 "node": ">= 0.8.0"
             }
         },
-        "node_modules/libtap": {
-            "version": "1.4.0",
-            "resolved": "https://registry.npmjs.org/libtap/-/libtap-1.4.0.tgz",
-            "integrity": "sha512-STLFynswQ2A6W14JkabgGetBNk6INL1REgJ9UeNKw5llXroC2cGLgKTqavv0sl8OLVztLLipVKMcQ7yeUcqpmg==",
-            "dev": true,
+        "node_modules/locate-path": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+            "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
             "dependencies": {
-                "async-hook-domain": "^2.0.4",
-                "bind-obj-methods": "^3.0.0",
-                "diff": "^4.0.2",
-                "function-loop": "^2.0.1",
-                "minipass": "^3.1.5",
-                "own-or": "^1.0.0",
-                "own-or-env": "^1.0.2",
-                "signal-exit": "^3.0.4",
-                "stack-utils": "^2.0.4",
-                "tap-parser": "^11.0.0",
-                "tap-yaml": "^1.0.0",
-                "tcompare": "^5.0.6",
-                "trivial-deferred": "^1.0.1"
+                "p-locate": "^5.0.0"
             },
             "engines": {
                 "node": ">=10"
             },
             "funding": {
-                "url": "https://github.com/sponsors/isaacs"
-            }
-        },
-        "node_modules/libtap/node_modules/tcompare": {
-            "version": "5.0.7",
-            "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-5.0.7.tgz",
-            "integrity": "sha512-d9iddt6YYGgyxJw5bjsN7UJUO1kGOtjSlNy/4PoGYAjQS5pAT/hzIoLf1bZCw+uUxRmZJh7Yy1aA7xKVRT9B4w==",
-            "dev": true,
-            "dependencies": {
-                "diff": "^4.0.2"
-            },
-            "engines": {
-                "node": ">=10"
-            }
-        },
-        "node_modules/locate-path": {
-            "version": "5.0.0",
-            "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
-            "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
-            "dev": true,
-            "dependencies": {
-                "p-locate": "^4.1.0"
-            },
-            "engines": {
-                "node": ">=8"
+                "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/lodash.flattendeep": {
-            "version": "4.4.0",
-            "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz",
-            "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==",
+        "node_modules/lodash": {
+            "version": "4.17.21",
+            "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+            "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
             "dev": true
         },
         "node_modules/lodash.merge": {
@@ -2007,1856 +3138,1668 @@
             "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
             "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
         },
-        "node_modules/lru-cache": {
-            "version": "5.1.1",
-            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
-            "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+        "node_modules/loose-envify": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+            "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
             "dev": true,
             "dependencies": {
-                "yallist": "^3.0.2"
+                "js-tokens": "^3.0.0 || ^4.0.0"
+            },
+            "bin": {
+                "loose-envify": "cli.js"
             }
         },
-        "node_modules/lru-cache/node_modules/yallist": {
-            "version": "3.1.1",
-            "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
-            "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
-            "dev": true
+        "node_modules/lru-cache": {
+            "version": "10.0.1",
+            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz",
+            "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==",
+            "dev": true,
+            "engines": {
+                "node": "14 || >=16.14"
+            }
         },
         "node_modules/make-dir": {
-            "version": "3.1.0",
-            "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
-            "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+            "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
             "dev": true,
             "dependencies": {
-                "semver": "^6.0.0"
+                "semver": "^7.5.3"
             },
             "engines": {
-                "node": ">=8"
+                "node": ">=10"
             },
             "funding": {
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/marked": {
-            "version": "5.0.2",
-            "resolved": "https://registry.npmjs.org/marked/-/marked-5.0.2.tgz",
-            "integrity": "sha512-TXksm9GwqXCRNbFUZmMtqNLvy3K2cQHuWmyBDLOrY1e6i9UvZpOTJXoz7fBjYkJkaUFzV9hBFxMuZSyQt8R6KQ==",
-            "bin": {
-                "marked": "bin/marked.js"
+        "node_modules/make-error": {
+            "version": "1.3.6",
+            "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+            "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+            "dev": true
+        },
+        "node_modules/make-fetch-happen": {
+            "version": "11.1.1",
+            "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz",
+            "integrity": "sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==",
+            "dev": true,
+            "dependencies": {
+                "agentkeepalive": "^4.2.1",
+                "cacache": "^17.0.0",
+                "http-cache-semantics": "^4.1.1",
+                "http-proxy-agent": "^5.0.0",
+                "https-proxy-agent": "^5.0.0",
+                "is-lambda": "^1.0.1",
+                "lru-cache": "^7.7.1",
+                "minipass": "^5.0.0",
+                "minipass-fetch": "^3.0.0",
+                "minipass-flush": "^1.0.5",
+                "minipass-pipeline": "^1.2.4",
+                "negotiator": "^0.6.3",
+                "promise-retry": "^2.0.1",
+                "socks-proxy-agent": "^7.0.0",
+                "ssri": "^10.0.0"
             },
             "engines": {
-                "node": ">= 18"
+                "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
             }
         },
-        "node_modules/minimatch": {
-            "version": "3.1.2",
-            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
-            "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+        "node_modules/make-fetch-happen/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": {
-                "brace-expansion": "^1.1.7"
-            },
-            "engines": {
-                "node": "*"
+                "balanced-match": "^1.0.0"
             }
         },
-        "node_modules/minipass": {
-            "version": "3.3.6",
-            "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
-            "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+        "node_modules/make-fetch-happen/node_modules/cacache": {
+            "version": "17.1.4",
+            "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.4.tgz",
+            "integrity": "sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A==",
             "dev": true,
             "dependencies": {
-                "yallist": "^4.0.0"
+                "@npmcli/fs": "^3.1.0",
+                "fs-minipass": "^3.0.0",
+                "glob": "^10.2.2",
+                "lru-cache": "^7.7.1",
+                "minipass": "^7.0.3",
+                "minipass-collect": "^1.0.2",
+                "minipass-flush": "^1.0.5",
+                "minipass-pipeline": "^1.2.4",
+                "p-map": "^4.0.0",
+                "ssri": "^10.0.0",
+                "tar": "^6.1.11",
+                "unique-filename": "^3.0.0"
             },
             "engines": {
-                "node": ">=8"
+                "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
             }
         },
-        "node_modules/mkdirp": {
-            "version": "1.0.4",
-            "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
-            "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+        "node_modules/make-fetch-happen/node_modules/cacache/node_modules/minipass": {
+            "version": "7.0.4",
+            "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
+            "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
             "dev": true,
-            "bin": {
-                "mkdirp": "bin/cmd.js"
-            },
             "engines": {
-                "node": ">=10"
+                "node": ">=16 || 14 >=14.17"
             }
         },
-        "node_modules/ms": {
-            "version": "2.1.2",
-            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-            "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
-        },
-        "node_modules/natural-compare": {
-            "version": "1.4.0",
-            "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
-            "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
-        },
-        "node_modules/node-preload": {
-            "version": "0.2.1",
-            "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz",
-            "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==",
+        "node_modules/make-fetch-happen/node_modules/glob": {
+            "version": "10.3.10",
+            "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+            "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
             "dev": true,
             "dependencies": {
-                "process-on-spawn": "^1.0.0"
+                "foreground-child": "^3.1.0",
+                "jackspeak": "^2.3.5",
+                "minimatch": "^9.0.1",
+                "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+                "path-scurry": "^1.10.1"
+            },
+            "bin": {
+                "glob": "dist/esm/bin.mjs"
             },
             "engines": {
-                "node": ">=8"
+                "node": ">=16 || 14 >=14.17"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/node-releases": {
-            "version": "2.0.10",
-            "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz",
-            "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==",
-            "dev": true
-        },
-        "node_modules/normalize-path": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
-            "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+        "node_modules/make-fetch-happen/node_modules/lru-cache": {
+            "version": "7.18.3",
+            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+            "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
             "dev": true,
             "engines": {
-                "node": ">=0.10.0"
+                "node": ">=12"
             }
         },
-        "node_modules/nyc": {
-            "version": "15.1.0",
-            "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz",
-            "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==",
+        "node_modules/make-fetch-happen/node_modules/minimatch": {
+            "version": "9.0.3",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+            "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
             "dev": true,
             "dependencies": {
-                "@istanbuljs/load-nyc-config": "^1.0.0",
-                "@istanbuljs/schema": "^0.1.2",
-                "caching-transform": "^4.0.0",
-                "convert-source-map": "^1.7.0",
-                "decamelize": "^1.2.0",
-                "find-cache-dir": "^3.2.0",
-                "find-up": "^4.1.0",
-                "foreground-child": "^2.0.0",
-                "get-package-type": "^0.1.0",
-                "glob": "^7.1.6",
-                "istanbul-lib-coverage": "^3.0.0",
-                "istanbul-lib-hook": "^3.0.0",
-                "istanbul-lib-instrument": "^4.0.0",
-                "istanbul-lib-processinfo": "^2.0.2",
-                "istanbul-lib-report": "^3.0.0",
-                "istanbul-lib-source-maps": "^4.0.0",
-                "istanbul-reports": "^3.0.2",
-                "make-dir": "^3.0.0",
-                "node-preload": "^0.2.1",
-                "p-map": "^3.0.0",
-                "process-on-spawn": "^1.0.0",
-                "resolve-from": "^5.0.0",
-                "rimraf": "^3.0.0",
-                "signal-exit": "^3.0.2",
-                "spawn-wrap": "^2.0.0",
-                "test-exclude": "^6.0.0",
-                "yargs": "^15.0.2"
-            },
-            "bin": {
-                "nyc": "bin/nyc.js"
+                "brace-expansion": "^2.0.1"
             },
             "engines": {
-                "node": ">=8.9"
+                "node": ">=16 || 14 >=14.17"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/nyc/node_modules/resolve-from": {
+        "node_modules/make-fetch-happen/node_modules/minipass": {
             "version": "5.0.0",
-            "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
-            "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+            "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
+            "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
             "dev": true,
             "engines": {
                 "node": ">=8"
             }
         },
-        "node_modules/once": {
-            "version": "1.4.0",
-            "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
-            "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
-            "dependencies": {
-                "wrappy": "1"
+        "node_modules/marked": {
+            "version": "5.0.2",
+            "resolved": "https://registry.npmjs.org/marked/-/marked-5.0.2.tgz",
+            "integrity": "sha512-TXksm9GwqXCRNbFUZmMtqNLvy3K2cQHuWmyBDLOrY1e6i9UvZpOTJXoz7fBjYkJkaUFzV9hBFxMuZSyQt8R6KQ==",
+            "bin": {
+                "marked": "bin/marked.js"
+            },
+            "engines": {
+                "node": ">= 18"
             }
         },
-        "node_modules/opener": {
-            "version": "1.5.2",
-            "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
-            "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
+        "node_modules/mimic-fn": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+            "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
             "dev": true,
-            "bin": {
-                "opener": "bin/opener-bin.js"
+            "engines": {
+                "node": ">=6"
             }
         },
-        "node_modules/optionator": {
-            "version": "0.9.1",
-            "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
-            "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+        "node_modules/minimatch": {
+            "version": "3.1.2",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+            "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
             "dependencies": {
-                "deep-is": "^0.1.3",
-                "fast-levenshtein": "^2.0.6",
-                "levn": "^0.4.1",
-                "prelude-ls": "^1.2.1",
-                "type-check": "^0.4.0",
-                "word-wrap": "^1.2.3"
+                "brace-expansion": "^1.1.7"
             },
             "engines": {
-                "node": ">= 0.8.0"
+                "node": "*"
             }
         },
-        "node_modules/own-or": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/own-or/-/own-or-1.0.0.tgz",
-            "integrity": "sha512-NfZr5+Tdf6MB8UI9GLvKRs4cXY8/yB0w3xtt84xFdWy8hkGjn+JFc60VhzS/hFRfbyxFcGYMTjnF4Me+RbbqrA==",
-            "dev": true
-        },
-        "node_modules/own-or-env": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/own-or-env/-/own-or-env-1.0.2.tgz",
-            "integrity": "sha512-NQ7v0fliWtK7Lkb+WdFqe6ky9XAzYmlkXthQrBbzlYbmFKoAYbDDcwmOm6q8kOuwSRXW8bdL5ORksploUJmWgw==",
+        "node_modules/minipass": {
+            "version": "7.0.4",
+            "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
+            "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
             "dev": true,
-            "dependencies": {
-                "own-or": "^1.0.0"
+            "engines": {
+                "node": ">=16 || 14 >=14.17"
             }
         },
-        "node_modules/p-limit": {
-            "version": "2.3.0",
-            "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
-            "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+        "node_modules/minipass-collect": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz",
+            "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==",
             "dev": true,
             "dependencies": {
-                "p-try": "^2.0.0"
+                "minipass": "^3.0.0"
             },
             "engines": {
-                "node": ">=6"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
+                "node": ">= 8"
             }
         },
-        "node_modules/p-locate": {
-            "version": "4.1.0",
-            "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
-            "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+        "node_modules/minipass-collect/node_modules/minipass": {
+            "version": "3.3.6",
+            "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+            "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
             "dev": true,
             "dependencies": {
-                "p-limit": "^2.2.0"
+                "yallist": "^4.0.0"
             },
             "engines": {
                 "node": ">=8"
             }
         },
-        "node_modules/p-map": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz",
-            "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==",
+        "node_modules/minipass-fetch": {
+            "version": "3.0.4",
+            "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.4.tgz",
+            "integrity": "sha512-jHAqnA728uUpIaFm7NWsCnqKT6UqZz7GcI/bDpPATuwYyKwJwW0remxSCxUlKiEty+eopHGa3oc8WxgQ1FFJqg==",
             "dev": true,
             "dependencies": {
-                "aggregate-error": "^3.0.0"
+                "minipass": "^7.0.3",
+                "minipass-sized": "^1.0.3",
+                "minizlib": "^2.1.2"
             },
             "engines": {
-                "node": ">=8"
+                "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+            },
+            "optionalDependencies": {
+                "encoding": "^0.1.13"
             }
         },
-        "node_modules/p-try": {
-            "version": "2.2.0",
-            "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
-            "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+        "node_modules/minipass-flush": {
+            "version": "1.0.5",
+            "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz",
+            "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
             "dev": true,
+            "dependencies": {
+                "minipass": "^3.0.0"
+            },
             "engines": {
-                "node": ">=6"
+                "node": ">= 8"
             }
         },
-        "node_modules/package-hash": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz",
-            "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==",
+        "node_modules/minipass-flush/node_modules/minipass": {
+            "version": "3.3.6",
+            "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+            "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
             "dev": true,
             "dependencies": {
-                "graceful-fs": "^4.1.15",
-                "hasha": "^5.0.0",
-                "lodash.flattendeep": "^4.4.0",
-                "release-zalgo": "^1.0.0"
+                "yallist": "^4.0.0"
             },
             "engines": {
                 "node": ">=8"
             }
         },
-        "node_modules/parent-module": {
+        "node_modules/minipass-json-stream": {
             "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
-            "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+            "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz",
+            "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==",
+            "dev": true,
             "dependencies": {
-                "callsites": "^3.0.0"
-            },
-            "engines": {
-                "node": ">=6"
+                "jsonparse": "^1.3.1",
+                "minipass": "^3.0.0"
             }
         },
-        "node_modules/path-exists": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
-            "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+        "node_modules/minipass-json-stream/node_modules/minipass": {
+            "version": "3.3.6",
+            "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+            "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+            "dev": true,
+            "dependencies": {
+                "yallist": "^4.0.0"
+            },
             "engines": {
                 "node": ">=8"
             }
         },
-        "node_modules/path-is-absolute": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
-            "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/path-key": {
-            "version": "3.1.1",
-            "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
-            "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+        "node_modules/minipass-pipeline": {
+            "version": "1.2.4",
+            "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
+            "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
+            "dev": true,
+            "dependencies": {
+                "minipass": "^3.0.0"
+            },
             "engines": {
                 "node": ">=8"
             }
         },
-        "node_modules/picocolors": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
-            "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
-            "dev": true
-        },
-        "node_modules/picomatch": {
-            "version": "2.3.1",
-            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
-            "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+        "node_modules/minipass-pipeline/node_modules/minipass": {
+            "version": "3.3.6",
+            "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+            "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
             "dev": true,
-            "engines": {
-                "node": ">=8.6"
+            "dependencies": {
+                "yallist": "^4.0.0"
             },
-            "funding": {
-                "url": "https://github.com/sponsors/jonschlinkert"
+            "engines": {
+                "node": ">=8"
             }
         },
-        "node_modules/pkg-dir": {
-            "version": "4.2.0",
-            "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
-            "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+        "node_modules/minipass-sized": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz",
+            "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==",
             "dev": true,
             "dependencies": {
-                "find-up": "^4.0.0"
+                "minipass": "^3.0.0"
             },
             "engines": {
                 "node": ">=8"
             }
         },
-        "node_modules/prelude-ls": {
-            "version": "1.2.1",
-            "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
-            "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
-            "engines": {
-                "node": ">= 0.8.0"
-            }
-        },
-        "node_modules/process-on-spawn": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz",
-            "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==",
+        "node_modules/minipass-sized/node_modules/minipass": {
+            "version": "3.3.6",
+            "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+            "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
             "dev": true,
             "dependencies": {
-                "fromentries": "^1.2.0"
+                "yallist": "^4.0.0"
             },
             "engines": {
                 "node": ">=8"
             }
         },
-        "node_modules/punycode": {
-            "version": "2.1.1",
-            "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
-            "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
-            "engines": {
-                "node": ">=6"
-            }
-        },
-        "node_modules/queue-microtask": {
-            "version": "1.2.3",
-            "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
-            "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
-            "funding": [
-                {
-                    "type": "github",
-                    "url": "https://github.com/sponsors/feross"
-                },
-                {
-                    "type": "patreon",
-                    "url": "https://www.patreon.com/feross"
-                },
-                {
-                    "type": "consulting",
-                    "url": "https://feross.org/support"
-                }
-            ]
-        },
-        "node_modules/readdirp": {
-            "version": "3.6.0",
-            "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
-            "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+        "node_modules/minizlib": {
+            "version": "2.1.2",
+            "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+            "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
             "dev": true,
             "dependencies": {
-                "picomatch": "^2.2.1"
+                "minipass": "^3.0.0",
+                "yallist": "^4.0.0"
             },
             "engines": {
-                "node": ">=8.10.0"
+                "node": ">= 8"
             }
         },
-        "node_modules/release-zalgo": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz",
-            "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==",
+        "node_modules/minizlib/node_modules/minipass": {
+            "version": "3.3.6",
+            "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+            "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
             "dev": true,
             "dependencies": {
-                "es6-error": "^4.0.1"
+                "yallist": "^4.0.0"
             },
             "engines": {
-                "node": ">=4"
+                "node": ">=8"
             }
         },
-        "node_modules/require-directory": {
-            "version": "2.1.1",
-            "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
-            "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+        "node_modules/mkdirp": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
+            "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
             "dev": true,
+            "bin": {
+                "mkdirp": "dist/cjs/src/bin.js"
+            },
             "engines": {
-                "node": ">=0.10.0"
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/require-main-filename": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
-            "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
-            "dev": true
+        "node_modules/ms": {
+            "version": "2.1.2",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+            "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
         },
-        "node_modules/resolve-from": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
-            "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
-            "engines": {
-                "node": ">=4"
-            }
+        "node_modules/natural-compare": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+            "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
         },
-        "node_modules/reusify": {
-            "version": "1.0.4",
-            "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
-            "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+        "node_modules/negotiator": {
+            "version": "0.6.3",
+            "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+            "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+            "dev": true,
             "engines": {
-                "iojs": ">=1.0.0",
-                "node": ">=0.10.0"
+                "node": ">= 0.6"
             }
         },
-        "node_modules/rimraf": {
-            "version": "3.0.2",
-            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
-            "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+        "node_modules/node-gyp": {
+            "version": "9.4.0",
+            "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.0.tgz",
+            "integrity": "sha512-dMXsYP6gc9rRbejLXmTbVRYjAHw7ppswsKyMxuxJxxOHzluIO1rGp9TOQgjFJ+2MCqcOcQTOPB/8Xwhr+7s4Eg==",
+            "dev": true,
             "dependencies": {
-                "glob": "^7.1.3"
+                "env-paths": "^2.2.0",
+                "exponential-backoff": "^3.1.1",
+                "glob": "^7.1.4",
+                "graceful-fs": "^4.2.6",
+                "make-fetch-happen": "^11.0.3",
+                "nopt": "^6.0.0",
+                "npmlog": "^6.0.0",
+                "rimraf": "^3.0.2",
+                "semver": "^7.3.5",
+                "tar": "^6.1.2",
+                "which": "^2.0.2"
             },
             "bin": {
-                "rimraf": "bin.js"
+                "node-gyp": "bin/node-gyp.js"
             },
-            "funding": {
-                "url": "https://github.com/sponsors/isaacs"
-            }
-        },
-        "node_modules/run-parallel": {
-            "version": "1.2.0",
-            "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
-            "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
-            "funding": [
-                {
-                    "type": "github",
-                    "url": "https://github.com/sponsors/feross"
-                },
-                {
-                    "type": "patreon",
-                    "url": "https://www.patreon.com/feross"
-                },
-                {
-                    "type": "consulting",
-                    "url": "https://feross.org/support"
-                }
-            ],
-            "dependencies": {
-                "queue-microtask": "^1.2.2"
+            "engines": {
+                "node": "^12.13 || ^14.13 || >=16"
             }
         },
-        "node_modules/semver": {
-            "version": "6.3.0",
-            "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-            "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+        "node_modules/nopt": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz",
+            "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==",
             "dev": true,
+            "dependencies": {
+                "abbrev": "^1.0.0"
+            },
             "bin": {
-                "semver": "bin/semver.js"
+                "nopt": "bin/nopt.js"
+            },
+            "engines": {
+                "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
             }
         },
-        "node_modules/set-blocking": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
-            "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
-            "dev": true
-        },
-        "node_modules/shebang-command": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
-            "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+        "node_modules/normalize-package-data": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.0.tgz",
+            "integrity": "sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==",
+            "dev": true,
             "dependencies": {
-                "shebang-regex": "^3.0.0"
+                "hosted-git-info": "^7.0.0",
+                "is-core-module": "^2.8.1",
+                "semver": "^7.3.5",
+                "validate-npm-package-license": "^3.0.4"
             },
             "engines": {
-                "node": ">=8"
+                "node": "^16.14.0 || >=18.0.0"
             }
         },
-        "node_modules/shebang-regex": {
+        "node_modules/normalize-path": {
             "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
-            "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/signal-exit": {
-            "version": "3.0.7",
-            "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
-            "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
-            "dev": true
-        },
-        "node_modules/source-map": {
-            "version": "0.6.1",
-            "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-            "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+            "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+            "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
             "dev": true,
             "engines": {
                 "node": ">=0.10.0"
             }
         },
-        "node_modules/source-map-support": {
-            "version": "0.5.21",
-            "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
-            "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
-            "dev": true,
-            "dependencies": {
-                "buffer-from": "^1.0.0",
-                "source-map": "^0.6.0"
-            }
-        },
-        "node_modules/spawn-wrap": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz",
-            "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==",
+        "node_modules/npm-bundled": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.0.tgz",
+            "integrity": "sha512-Vq0eyEQy+elFpzsKjMss9kxqb9tG3YHg4dsyWuUENuzvSUWe1TCnW/vV9FkhvBk/brEDoDiVd+M1Btosa6ImdQ==",
             "dev": true,
             "dependencies": {
-                "foreground-child": "^2.0.0",
-                "is-windows": "^1.0.2",
-                "make-dir": "^3.0.0",
-                "rimraf": "^3.0.0",
-                "signal-exit": "^3.0.2",
-                "which": "^2.0.1"
+                "npm-normalize-package-bin": "^3.0.0"
             },
             "engines": {
-                "node": ">=8"
+                "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
             }
         },
-        "node_modules/sprintf-js": {
-            "version": "1.0.3",
-            "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
-            "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
-            "dev": true
-        },
-        "node_modules/stack-utils": {
-            "version": "2.0.6",
-            "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
-            "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
+        "node_modules/npm-install-checks": {
+            "version": "6.2.0",
+            "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.2.0.tgz",
+            "integrity": "sha512-744wat5wAAHsxa4590mWO0tJ8PKxR8ORZsH9wGpQc3nWTzozMAgBN/XyqYw7mg3yqLM8dLwEnwSfKMmXAjF69g==",
             "dev": true,
             "dependencies": {
-                "escape-string-regexp": "^2.0.0"
+                "semver": "^7.1.1"
             },
             "engines": {
-                "node": ">=10"
+                "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
             }
         },
-        "node_modules/stack-utils/node_modules/escape-string-regexp": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
-            "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
+        "node_modules/npm-normalize-package-bin": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz",
+            "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==",
             "dev": true,
             "engines": {
-                "node": ">=8"
+                "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
             }
         },
-        "node_modules/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==",
+        "node_modules/npm-package-arg": {
+            "version": "11.0.1",
+            "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.1.tgz",
+            "integrity": "sha512-M7s1BD4NxdAvBKUPqqRW957Xwcl/4Zvo8Aj+ANrzvIPzGJZElrH7Z//rSaec2ORcND6FHHLnZeY8qgTpXDMFQQ==",
             "dev": true,
             "dependencies": {
-                "emoji-regex": "^8.0.0",
-                "is-fullwidth-code-point": "^3.0.0",
-                "strip-ansi": "^6.0.1"
+                "hosted-git-info": "^7.0.0",
+                "proc-log": "^3.0.0",
+                "semver": "^7.3.5",
+                "validate-npm-package-name": "^5.0.0"
             },
             "engines": {
-                "node": ">=8"
+                "node": "^16.14.0 || >=18.0.0"
             }
         },
-        "node_modules/strip-ansi": {
-            "version": "6.0.1",
-            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
-            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+        "node_modules/npm-packlist": {
+            "version": "8.0.0",
+            "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.0.tgz",
+            "integrity": "sha512-ErAGFB5kJUciPy1mmx/C2YFbvxoJ0QJ9uwkCZOeR6CqLLISPZBOiFModAbSXnjjlwW5lOhuhXva+fURsSGJqyw==",
+            "dev": true,
             "dependencies": {
-                "ansi-regex": "^5.0.1"
+                "ignore-walk": "^6.0.0"
             },
             "engines": {
-                "node": ">=8"
+                "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
             }
         },
-        "node_modules/strip-bom": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
-            "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
+        "node_modules/npm-pick-manifest": {
+            "version": "9.0.0",
+            "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.0.0.tgz",
+            "integrity": "sha512-VfvRSs/b6n9ol4Qb+bDwNGUXutpy76x6MARw/XssevE0TnctIKcmklJZM5Z7nqs5z5aW+0S63pgCNbpkUNNXBg==",
             "dev": true,
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/strip-json-comments": {
-            "version": "3.1.1",
-            "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
-            "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
-            "engines": {
-                "node": ">=8"
+            "dependencies": {
+                "npm-install-checks": "^6.0.0",
+                "npm-normalize-package-bin": "^3.0.0",
+                "npm-package-arg": "^11.0.0",
+                "semver": "^7.3.5"
             },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
+            "engines": {
+                "node": "^16.14.0 || >=18.0.0"
             }
         },
-        "node_modules/striptags": {
-            "version": "4.0.0-alpha.4",
-            "resolved": "https://registry.npmjs.org/striptags/-/striptags-4.0.0-alpha.4.tgz",
-            "integrity": "sha512-/0jWyVWhpg9ciRHfjKYBpMHXct/HrFRfsR2HU77nGPbc8SPcVSIHZlZR/0TG3MyPq2C+HiHuwx8BlbcdI/cNbw=="
-        },
-        "node_modules/supports-color": {
-            "version": "7.2.0",
-            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-            "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+        "node_modules/npm-registry-fetch": {
+            "version": "16.0.0",
+            "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-16.0.0.tgz",
+            "integrity": "sha512-JFCpAPUpvpwfSydv99u85yhP68rNIxSFmDpNbNnRWKSe3gpjHnWL8v320gATwRzjtgmZ9Jfe37+ZPOLZPwz6BQ==",
+            "dev": true,
             "dependencies": {
-                "has-flag": "^4.0.0"
+                "make-fetch-happen": "^13.0.0",
+                "minipass": "^7.0.2",
+                "minipass-fetch": "^3.0.0",
+                "minipass-json-stream": "^1.0.1",
+                "minizlib": "^2.1.2",
+                "npm-package-arg": "^11.0.0",
+                "proc-log": "^3.0.0"
             },
             "engines": {
-                "node": ">=8"
+                "node": "^16.14.0 || >=18.0.0"
             }
         },
-        "node_modules/tap": {
-            "version": "16.3.4",
-            "resolved": "https://registry.npmjs.org/tap/-/tap-16.3.4.tgz",
-            "integrity": "sha512-SAexdt2ZF4XBgye6TPucFI2y7VE0qeFXlXucJIV1XDPCs+iJodk0MYacr1zR6Ycltzz7PYg8zrblDXKbAZM2LQ==",
-            "bundleDependencies": [
-                "ink",
-                "treport",
-                "@types/react",
-                "@isaacs/import-jsx",
-                "react"
-            ],
+        "node_modules/npm-registry-fetch/node_modules/make-fetch-happen": {
+            "version": "13.0.0",
+            "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.0.tgz",
+            "integrity": "sha512-7ThobcL8brtGo9CavByQrQi+23aIfgYU++wg4B87AIS8Rb2ZBt/MEaDqzA00Xwv/jUjAjYkLHjVolYuTLKda2A==",
             "dev": true,
             "dependencies": {
-                "@isaacs/import-jsx": "^4.0.1",
-                "@types/react": "^17.0.52",
-                "chokidar": "^3.3.0",
-                "findit": "^2.0.0",
-                "foreground-child": "^2.0.0",
-                "fs-exists-cached": "^1.0.0",
-                "glob": "^7.2.3",
-                "ink": "^3.2.0",
-                "isexe": "^2.0.0",
-                "istanbul-lib-processinfo": "^2.0.3",
-                "jackspeak": "^1.4.2",
-                "libtap": "^1.4.0",
-                "minipass": "^3.3.4",
-                "mkdirp": "^1.0.4",
-                "nyc": "^15.1.0",
-                "opener": "^1.5.1",
-                "react": "^17.0.2",
-                "rimraf": "^3.0.0",
-                "signal-exit": "^3.0.6",
-                "source-map-support": "^0.5.16",
-                "tap-mocha-reporter": "^5.0.3",
-                "tap-parser": "^11.0.2",
-                "tap-yaml": "^1.0.2",
-                "tcompare": "^5.0.7",
-                "treport": "^3.0.4",
-                "which": "^2.0.2"
-            },
-            "bin": {
-                "tap": "bin/run.js"
+                "@npmcli/agent": "^2.0.0",
+                "cacache": "^18.0.0",
+                "http-cache-semantics": "^4.1.1",
+                "is-lambda": "^1.0.1",
+                "minipass": "^7.0.2",
+                "minipass-fetch": "^3.0.0",
+                "minipass-flush": "^1.0.5",
+                "minipass-pipeline": "^1.2.4",
+                "negotiator": "^0.6.3",
+                "promise-retry": "^2.0.1",
+                "ssri": "^10.0.0"
             },
             "engines": {
-                "node": ">=12"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/isaacs"
-            },
-            "peerDependencies": {
-                "coveralls": "^3.1.1",
-                "flow-remove-types": ">=2.112.0",
-                "ts-node": ">=8.5.2",
-                "typescript": ">=3.7.2"
-            },
-            "peerDependenciesMeta": {
-                "coveralls": {
-                    "optional": true
-                },
-                "flow-remove-types": {
-                    "optional": true
-                },
-                "ts-node": {
-                    "optional": true
-                },
-                "typescript": {
-                    "optional": true
-                }
+                "node": "^16.14.0 || >=18.0.0"
             }
         },
-        "node_modules/tap-mocha-reporter": {
-            "version": "5.0.3",
-            "resolved": "https://registry.npmjs.org/tap-mocha-reporter/-/tap-mocha-reporter-5.0.3.tgz",
-            "integrity": "sha512-6zlGkaV4J+XMRFkN0X+yuw6xHbE9jyCZ3WUKfw4KxMyRGOpYSRuuQTRJyWX88WWuLdVTuFbxzwXhXuS2XE6o0g==",
+        "node_modules/npmlog": {
+            "version": "6.0.2",
+            "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz",
+            "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==",
             "dev": true,
             "dependencies": {
-                "color-support": "^1.1.0",
-                "debug": "^4.1.1",
-                "diff": "^4.0.1",
-                "escape-string-regexp": "^2.0.0",
-                "glob": "^7.0.5",
-                "tap-parser": "^11.0.0",
-                "tap-yaml": "^1.0.0",
-                "unicode-length": "^2.0.2"
-            },
-            "bin": {
-                "tap-mocha-reporter": "index.js"
+                "are-we-there-yet": "^3.0.0",
+                "console-control-strings": "^1.1.0",
+                "gauge": "^4.0.3",
+                "set-blocking": "^2.0.0"
             },
             "engines": {
-                "node": ">= 8"
+                "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
             }
         },
-        "node_modules/tap-mocha-reporter/node_modules/escape-string-regexp": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
-            "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
-            "dev": true,
-            "engines": {
-                "node": ">=8"
+        "node_modules/once": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+            "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+            "dependencies": {
+                "wrappy": "1"
             }
         },
-        "node_modules/tap-parser": {
-            "version": "11.0.2",
-            "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-11.0.2.tgz",
-            "integrity": "sha512-6qGlC956rcORw+fg7Fv1iCRAY8/bU9UabUAhs3mXRH6eRmVZcNPLheSXCYaVaYeSwx5xa/1HXZb1537YSvwDZg==",
+        "node_modules/onetime": {
+            "version": "5.1.2",
+            "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+            "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
             "dev": true,
             "dependencies": {
-                "events-to-array": "^1.0.1",
-                "minipass": "^3.1.6",
-                "tap-yaml": "^1.0.0"
-            },
-            "bin": {
-                "tap-parser": "bin/cmd.js"
+                "mimic-fn": "^2.1.0"
             },
             "engines": {
-                "node": ">= 8"
+                "node": ">=6"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/tap-yaml": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/tap-yaml/-/tap-yaml-1.0.2.tgz",
-            "integrity": "sha512-GegASpuqBnRNdT1U+yuUPZ8rEU64pL35WPBpCISWwff4dErS2/438barz7WFJl4Nzh3Y05tfPidZnH+GaV1wMg==",
+        "node_modules/opener": {
+            "version": "1.5.2",
+            "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
+            "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
             "dev": true,
-            "dependencies": {
-                "yaml": "^1.10.2"
+            "bin": {
+                "opener": "bin/opener-bin.js"
             }
         },
-        "node_modules/tap/node_modules/@ampproject/remapping": {
-            "version": "2.1.2",
-            "dev": true,
-            "inBundle": true,
-            "license": "Apache-2.0",
+        "node_modules/optionator": {
+            "version": "0.9.1",
+            "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
+            "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
             "dependencies": {
-                "@jridgewell/trace-mapping": "^0.3.0"
+                "deep-is": "^0.1.3",
+                "fast-levenshtein": "^2.0.6",
+                "levn": "^0.4.1",
+                "prelude-ls": "^1.2.1",
+                "type-check": "^0.4.0",
+                "word-wrap": "^1.2.3"
             },
             "engines": {
-                "node": ">=6.0.0"
+                "node": ">= 0.8.0"
             }
         },
-        "node_modules/tap/node_modules/@babel/code-frame": {
-            "version": "7.16.7",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
+        "node_modules/p-limit": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+            "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
             "dependencies": {
-                "@babel/highlight": "^7.16.7"
+                "yocto-queue": "^0.1.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/tap/node_modules/@babel/compat-data": {
-            "version": "7.17.7",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
+        "node_modules/p-locate": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+            "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+            "dependencies": {
+                "p-limit": "^3.0.2"
+            },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/tap/node_modules/@babel/core": {
-            "version": "7.17.8",
+        "node_modules/p-map": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
+            "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "@ampproject/remapping": "^2.1.0",
-                "@babel/code-frame": "^7.16.7",
-                "@babel/generator": "^7.17.7",
-                "@babel/helper-compilation-targets": "^7.17.7",
-                "@babel/helper-module-transforms": "^7.17.7",
-                "@babel/helpers": "^7.17.8",
-                "@babel/parser": "^7.17.8",
-                "@babel/template": "^7.16.7",
-                "@babel/traverse": "^7.17.3",
-                "@babel/types": "^7.17.0",
-                "convert-source-map": "^1.7.0",
-                "debug": "^4.1.0",
-                "gensync": "^1.0.0-beta.2",
-                "json5": "^2.1.2",
-                "semver": "^6.3.0"
+                "aggregate-error": "^3.0.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">=10"
             },
             "funding": {
-                "type": "opencollective",
-                "url": "https://opencollective.com/babel"
+                "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/tap/node_modules/@babel/generator": {
-            "version": "7.17.7",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/types": "^7.17.0",
-                "jsesc": "^2.5.1",
-                "source-map": "^0.5.0"
+        "node_modules/pacote": {
+            "version": "17.0.4",
+            "resolved": "https://registry.npmjs.org/pacote/-/pacote-17.0.4.tgz",
+            "integrity": "sha512-eGdLHrV/g5b5MtD5cTPyss+JxOlaOloSMG3UwPMAvL8ywaLJ6beONPF40K4KKl/UI6q5hTKCJq5rCu8tkF+7Dg==",
+            "dev": true,
+            "dependencies": {
+                "@npmcli/git": "^5.0.0",
+                "@npmcli/installed-package-contents": "^2.0.1",
+                "@npmcli/promise-spawn": "^7.0.0",
+                "@npmcli/run-script": "^7.0.0",
+                "cacache": "^18.0.0",
+                "fs-minipass": "^3.0.0",
+                "minipass": "^7.0.2",
+                "npm-package-arg": "^11.0.0",
+                "npm-packlist": "^8.0.0",
+                "npm-pick-manifest": "^9.0.0",
+                "npm-registry-fetch": "^16.0.0",
+                "proc-log": "^3.0.0",
+                "promise-retry": "^2.0.1",
+                "read-package-json": "^7.0.0",
+                "read-package-json-fast": "^3.0.0",
+                "sigstore": "^2.0.0",
+                "ssri": "^10.0.0",
+                "tar": "^6.1.11"
+            },
+            "bin": {
+                "pacote": "lib/bin.js"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^16.14.0 || >=18.0.0"
             }
         },
-        "node_modules/tap/node_modules/@babel/helper-annotate-as-pure": {
-            "version": "7.16.7",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
+        "node_modules/parent-module": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+            "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
             "dependencies": {
-                "@babel/types": "^7.16.7"
+                "callsites": "^3.0.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">=6"
             }
         },
-        "node_modules/tap/node_modules/@babel/helper-compilation-targets": {
-            "version": "7.17.7",
+        "node_modules/patch-console": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz",
+            "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/compat-data": "^7.17.7",
-                "@babel/helper-validator-option": "^7.16.7",
-                "browserslist": "^4.17.5",
-                "semver": "^6.3.0"
-            },
             "engines": {
-                "node": ">=6.9.0"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0"
+                "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
             }
         },
-        "node_modules/tap/node_modules/@babel/helper-environment-visitor": {
-            "version": "7.16.7",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/types": "^7.16.7"
-            },
+        "node_modules/path-exists": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+            "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">=8"
             }
         },
-        "node_modules/tap/node_modules/@babel/helper-function-name": {
-            "version": "7.16.7",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/helper-get-function-arity": "^7.16.7",
-                "@babel/template": "^7.16.7",
-                "@babel/types": "^7.16.7"
-            },
+        "node_modules/path-is-absolute": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+            "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">=0.10.0"
             }
         },
-        "node_modules/tap/node_modules/@babel/helper-get-function-arity": {
-            "version": "7.16.7",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/types": "^7.16.7"
-            },
+        "node_modules/path-key": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+            "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">=8"
             }
         },
-        "node_modules/tap/node_modules/@babel/helper-hoist-variables": {
-            "version": "7.16.7",
+        "node_modules/path-scurry": {
+            "version": "1.10.1",
+            "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
+            "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "@babel/types": "^7.16.7"
+                "lru-cache": "^9.1.1 || ^10.0.0",
+                "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">=16 || 14 >=14.17"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/tap/node_modules/@babel/helper-module-imports": {
-            "version": "7.16.7",
+        "node_modules/picomatch": {
+            "version": "2.3.1",
+            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+            "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/types": "^7.16.7"
-            },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">=8.6"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/jonschlinkert"
             }
         },
-        "node_modules/tap/node_modules/@babel/helper-module-transforms": {
-            "version": "7.17.7",
+        "node_modules/pirates": {
+            "version": "4.0.6",
+            "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
+            "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/helper-environment-visitor": "^7.16.7",
-                "@babel/helper-module-imports": "^7.16.7",
-                "@babel/helper-simple-access": "^7.17.7",
-                "@babel/helper-split-export-declaration": "^7.16.7",
-                "@babel/helper-validator-identifier": "^7.16.7",
-                "@babel/template": "^7.16.7",
-                "@babel/traverse": "^7.17.3",
-                "@babel/types": "^7.17.0"
-            },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">= 6"
             }
         },
-        "node_modules/tap/node_modules/@babel/helper-plugin-utils": {
-            "version": "7.16.7",
+        "node_modules/polite-json": {
+            "version": "4.0.1",
+            "resolved": "https://registry.npmjs.org/polite-json/-/polite-json-4.0.1.tgz",
+            "integrity": "sha512-8LI5ZeCPBEb4uBbcYKNVwk4jgqNx1yHReWoW4H4uUihWlSqZsUDfSITrRhjliuPgxsNPFhNSudGO2Zu4cbWinQ==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            }
+        },
+        "node_modules/prelude-ls": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+            "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+            "engines": {
+                "node": ">= 0.8.0"
             }
         },
-        "node_modules/tap/node_modules/@babel/helper-simple-access": {
-            "version": "7.17.7",
+        "node_modules/prismjs": {
+            "version": "1.29.0",
+            "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
+            "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/types": "^7.17.0"
-            },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">=6"
             }
         },
-        "node_modules/tap/node_modules/@babel/helper-split-export-declaration": {
-            "version": "7.16.7",
+        "node_modules/prismjs-terminal": {
+            "version": "1.2.3",
+            "resolved": "https://registry.npmjs.org/prismjs-terminal/-/prismjs-terminal-1.2.3.tgz",
+            "integrity": "sha512-xc0zuJ5FMqvW+DpiRkvxURlz98DdfDsZcFHdO699+oL+ykbFfgI7O4VDEgUyc07BSL2NHl3zdb8m/tZ/aaqUrw==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "@babel/types": "^7.16.7"
+                "chalk": "^5.2.0",
+                "prismjs": "^1.29.0",
+                "string-length": "^6.0.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">=16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/tap/node_modules/@babel/helper-validator-identifier": {
-            "version": "7.16.7",
+        "node_modules/prismjs-terminal/node_modules/chalk": {
+            "version": "5.3.0",
+            "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
+            "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^12.17.0 || ^14.13 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/chalk?sponsor=1"
             }
         },
-        "node_modules/tap/node_modules/@babel/helper-validator-option": {
-            "version": "7.16.7",
+        "node_modules/proc-log": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz",
+            "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
             }
         },
-        "node_modules/tap/node_modules/@babel/helpers": {
-            "version": "7.17.8",
+        "node_modules/process-on-spawn": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz",
+            "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "@babel/template": "^7.16.7",
-                "@babel/traverse": "^7.17.3",
-                "@babel/types": "^7.17.0"
+                "fromentries": "^1.2.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">=8"
             }
         },
-        "node_modules/tap/node_modules/@babel/highlight": {
-            "version": "7.16.10",
+        "node_modules/promise-inflight": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
+            "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==",
+            "dev": true
+        },
+        "node_modules/promise-retry": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
+            "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "@babel/helper-validator-identifier": "^7.16.7",
-                "chalk": "^2.0.0",
-                "js-tokens": "^4.0.0"
+                "err-code": "^2.0.2",
+                "retry": "^0.12.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">=10"
             }
         },
-        "node_modules/tap/node_modules/@babel/parser": {
-            "version": "7.17.8",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
-            "bin": {
-                "parser": "bin/babel-parser.js"
-            },
+        "node_modules/punycode": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+            "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
             "engines": {
-                "node": ">=6.0.0"
+                "node": ">=6"
+            }
+        },
+        "node_modules/queue": {
+            "version": "6.0.2",
+            "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
+            "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
+            "dependencies": {
+                "inherits": "~2.0.3"
             }
         },
-        "node_modules/tap/node_modules/@babel/plugin-proposal-object-rest-spread": {
-            "version": "7.17.3",
+        "node_modules/queue-microtask": {
+            "version": "1.2.3",
+            "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+            "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
+                },
+                {
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
+                }
+            ]
+        },
+        "node_modules/react": {
+            "version": "18.2.0",
+            "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
+            "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "@babel/compat-data": "^7.17.0",
-                "@babel/helper-compilation-targets": "^7.16.7",
-                "@babel/helper-plugin-utils": "^7.16.7",
-                "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
-                "@babel/plugin-transform-parameters": "^7.16.7"
+                "loose-envify": "^1.1.0"
             },
             "engines": {
-                "node": ">=6.9.0"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
+                "node": ">=0.10.0"
             }
         },
-        "node_modules/tap/node_modules/@babel/plugin-syntax-jsx": {
-            "version": "7.16.7",
+        "node_modules/react-dom": {
+            "version": "18.2.0",
+            "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
+            "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
+            "peer": true,
             "dependencies": {
-                "@babel/helper-plugin-utils": "^7.16.7"
-            },
-            "engines": {
-                "node": ">=6.9.0"
+                "loose-envify": "^1.1.0",
+                "scheduler": "^0.23.0"
             },
             "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
+                "react": "^18.2.0"
             }
         },
-        "node_modules/tap/node_modules/@babel/plugin-syntax-object-rest-spread": {
-            "version": "7.8.3",
+        "node_modules/react-element-to-jsx-string": {
+            "version": "15.0.0",
+            "resolved": "https://registry.npmjs.org/react-element-to-jsx-string/-/react-element-to-jsx-string-15.0.0.tgz",
+            "integrity": "sha512-UDg4lXB6BzlobN60P8fHWVPX3Kyw8ORrTeBtClmIlGdkOOE+GYQSFvmEU5iLLpwp/6v42DINwNcwOhOLfQ//FQ==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "@babel/helper-plugin-utils": "^7.8.0"
+                "@base2/pretty-print-object": "1.0.1",
+                "is-plain-object": "5.0.0",
+                "react-is": "18.1.0"
             },
             "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
+                "react": "^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0",
+                "react-dom": "^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0"
             }
         },
-        "node_modules/tap/node_modules/@babel/plugin-transform-destructuring": {
-            "version": "7.17.7",
+        "node_modules/react-is": {
+            "version": "18.1.0",
+            "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz",
+            "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==",
+            "dev": true
+        },
+        "node_modules/react-reconciler": {
+            "version": "0.29.0",
+            "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.0.tgz",
+            "integrity": "sha512-wa0fGj7Zht1EYMRhKWwoo1H9GApxYLBuhoAuXN0TlltESAjDssB+Apf0T/DngVqaMyPypDmabL37vw/2aRM98Q==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "@babel/helper-plugin-utils": "^7.16.7"
+                "loose-envify": "^1.1.0",
+                "scheduler": "^0.23.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">=0.10.0"
             },
             "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
+                "react": "^18.2.0"
             }
         },
-        "node_modules/tap/node_modules/@babel/plugin-transform-parameters": {
-            "version": "7.16.7",
+        "node_modules/read-package-json": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-7.0.0.tgz",
+            "integrity": "sha512-uL4Z10OKV4p6vbdvIXB+OzhInYtIozl/VxUBPgNkBuUi2DeRonnuspmaVAMcrkmfjKGNmRndyQAbE7/AmzGwFg==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "@babel/helper-plugin-utils": "^7.16.7"
+                "glob": "^10.2.2",
+                "json-parse-even-better-errors": "^3.0.0",
+                "normalize-package-data": "^6.0.0",
+                "npm-normalize-package-bin": "^3.0.0"
             },
             "engines": {
-                "node": ">=6.9.0"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
+                "node": "^16.14.0 || >=18.0.0"
             }
         },
-        "node_modules/tap/node_modules/@babel/plugin-transform-react-jsx": {
-            "version": "7.17.3",
+        "node_modules/read-package-json-fast": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz",
+            "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "@babel/helper-annotate-as-pure": "^7.16.7",
-                "@babel/helper-module-imports": "^7.16.7",
-                "@babel/helper-plugin-utils": "^7.16.7",
-                "@babel/plugin-syntax-jsx": "^7.16.7",
-                "@babel/types": "^7.17.0"
+                "json-parse-even-better-errors": "^3.0.0",
+                "npm-normalize-package-bin": "^3.0.0"
             },
             "engines": {
-                "node": ">=6.9.0"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
+                "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
             }
         },
-        "node_modules/tap/node_modules/@babel/template": {
-            "version": "7.16.7",
+        "node_modules/read-package-json/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/read-package-json/node_modules/glob": {
+            "version": "10.3.10",
+            "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+            "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "@babel/code-frame": "^7.16.7",
-                "@babel/parser": "^7.16.7",
-                "@babel/types": "^7.16.7"
+                "foreground-child": "^3.1.0",
+                "jackspeak": "^2.3.5",
+                "minimatch": "^9.0.1",
+                "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+                "path-scurry": "^1.10.1"
+            },
+            "bin": {
+                "glob": "dist/esm/bin.mjs"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">=16 || 14 >=14.17"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/tap/node_modules/@babel/traverse": {
-            "version": "7.17.3",
+        "node_modules/read-package-json/node_modules/minimatch": {
+            "version": "9.0.3",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+            "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "@babel/code-frame": "^7.16.7",
-                "@babel/generator": "^7.17.3",
-                "@babel/helper-environment-visitor": "^7.16.7",
-                "@babel/helper-function-name": "^7.16.7",
-                "@babel/helper-hoist-variables": "^7.16.7",
-                "@babel/helper-split-export-declaration": "^7.16.7",
-                "@babel/parser": "^7.17.3",
-                "@babel/types": "^7.17.0",
-                "debug": "^4.1.0",
-                "globals": "^11.1.0"
+                "brace-expansion": "^2.0.1"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">=16 || 14 >=14.17"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/tap/node_modules/@babel/types": {
-            "version": "7.17.0",
+        "node_modules/readable-stream": {
+            "version": "3.6.2",
+            "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+            "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "@babel/helper-validator-identifier": "^7.16.7",
-                "to-fast-properties": "^2.0.0"
+                "inherits": "^2.0.3",
+                "string_decoder": "^1.1.1",
+                "util-deprecate": "^1.0.1"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">= 6"
             }
         },
-        "node_modules/tap/node_modules/@isaacs/import-jsx": {
-            "version": "4.0.1",
+        "node_modules/readdirp": {
+            "version": "3.6.0",
+            "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+            "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "@babel/core": "^7.5.5",
-                "@babel/plugin-proposal-object-rest-spread": "^7.5.5",
-                "@babel/plugin-transform-destructuring": "^7.5.0",
-                "@babel/plugin-transform-react-jsx": "^7.3.0",
-                "caller-path": "^3.0.1",
-                "find-cache-dir": "^3.2.0",
-                "make-dir": "^3.0.2",
-                "resolve-from": "^3.0.0",
-                "rimraf": "^3.0.0"
+                "picomatch": "^2.2.1"
             },
             "engines": {
-                "node": ">=10"
+                "node": ">=8.10.0"
             }
         },
-        "node_modules/tap/node_modules/@jridgewell/resolve-uri": {
-            "version": "3.0.5",
+        "node_modules/require-directory": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+            "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "engines": {
-                "node": ">=6.0.0"
+                "node": ">=0.10.0"
             }
         },
-        "node_modules/tap/node_modules/@jridgewell/sourcemap-codec": {
-            "version": "1.4.11",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT"
+        "node_modules/resolve-from": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+            "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+            "engines": {
+                "node": ">=4"
+            }
         },
-        "node_modules/tap/node_modules/@jridgewell/trace-mapping": {
-            "version": "0.3.4",
+        "node_modules/resolve-import": {
+            "version": "1.4.2",
+            "resolved": "https://registry.npmjs.org/resolve-import/-/resolve-import-1.4.2.tgz",
+            "integrity": "sha512-ayUU3E2yeFu8ZewNEHbGorcPmHjOmCY8b50wloum8eQUuNExSyddRoWYaX0X6lj3XSufi2WUlXY3mkMcF5ISmw==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "@jridgewell/resolve-uri": "^3.0.3",
-                "@jridgewell/sourcemap-codec": "^1.4.10"
+                "glob": "^10.3.3",
+                "walk-up-path": "^3.0.1"
+            },
+            "engines": {
+                "node": ">=16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/tap/node_modules/@types/prop-types": {
-            "version": "15.7.4",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT"
-        },
-        "node_modules/tap/node_modules/@types/react": {
-            "version": "17.0.52",
+        "node_modules/resolve-import/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,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "@types/prop-types": "*",
-                "@types/scheduler": "*",
-                "csstype": "^3.0.2"
+                "balanced-match": "^1.0.0"
             }
         },
-        "node_modules/tap/node_modules/@types/scheduler": {
-            "version": "0.16.2",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT"
-        },
-        "node_modules/tap/node_modules/@types/yoga-layout": {
-            "version": "1.9.2",
+        "node_modules/resolve-import/node_modules/glob": {
+            "version": "10.3.10",
+            "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+            "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT"
-        },
-        "node_modules/tap/node_modules/ansi-escapes": {
-            "version": "4.3.2",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "type-fest": "^0.21.3"
+                "foreground-child": "^3.1.0",
+                "jackspeak": "^2.3.5",
+                "minimatch": "^9.0.1",
+                "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+                "path-scurry": "^1.10.1"
+            },
+            "bin": {
+                "glob": "dist/esm/bin.mjs"
             },
             "engines": {
-                "node": ">=8"
+                "node": ">=16 || 14 >=14.17"
             },
             "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/tap/node_modules/ansi-escapes/node_modules/type-fest": {
-            "version": "0.21.3",
+        "node_modules/resolve-import/node_modules/minimatch": {
+            "version": "9.0.3",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+            "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
             "dev": true,
-            "inBundle": true,
-            "license": "(MIT OR CC0-1.0)",
+            "dependencies": {
+                "brace-expansion": "^2.0.1"
+            },
             "engines": {
-                "node": ">=10"
+                "node": ">=16 || 14 >=14.17"
             },
             "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/tap/node_modules/ansi-regex": {
-            "version": "5.0.1",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=8"
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/tap/node_modules/ansi-styles": {
-            "version": "3.2.1",
+        "node_modules/restore-cursor": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz",
+            "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "color-convert": "^1.9.0"
+                "onetime": "^5.1.0",
+                "signal-exit": "^3.0.2"
             },
             "engines": {
-                "node": ">=4"
+                "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/tap/node_modules/ansicolors": {
-            "version": "0.3.2",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT"
+        "node_modules/restore-cursor/node_modules/signal-exit": {
+            "version": "3.0.7",
+            "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+            "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+            "dev": true
         },
-        "node_modules/tap/node_modules/astral-regex": {
-            "version": "2.0.0",
+        "node_modules/retry": {
+            "version": "0.12.0",
+            "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+            "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "engines": {
-                "node": ">=8"
+                "node": ">= 4"
             }
         },
-        "node_modules/tap/node_modules/auto-bind": {
-            "version": "4.0.0",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
+        "node_modules/reusify": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+            "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
             "engines": {
-                "node": ">=8"
+                "iojs": ">=1.0.0",
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/rimraf": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+            "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+            "dependencies": {
+                "glob": "^7.1.3"
+            },
+            "bin": {
+                "rimraf": "bin.js"
             },
             "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/tap/node_modules/balanced-match": {
-            "version": "1.0.2",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT"
-        },
-        "node_modules/tap/node_modules/brace-expansion": {
-            "version": "1.1.11",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
+        "node_modules/run-parallel": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+            "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
+                },
+                {
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
+                }
+            ],
             "dependencies": {
-                "balanced-match": "^1.0.0",
-                "concat-map": "0.0.1"
+                "queue-microtask": "^1.2.2"
             }
         },
-        "node_modules/tap/node_modules/browserslist": {
-            "version": "4.20.2",
+        "node_modules/safe-buffer": {
+            "version": "5.2.1",
+            "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+            "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
             "dev": true,
             "funding": [
                 {
-                    "type": "opencollective",
-                    "url": "https://opencollective.com/browserslist"
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
                 },
                 {
-                    "type": "tidelift",
-                    "url": "https://tidelift.com/funding/github/npm/browserslist"
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
                 }
-            ],
-            "inBundle": true,
-            "license": "MIT",
+            ]
+        },
+        "node_modules/safer-buffer": {
+            "version": "2.1.2",
+            "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+            "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+            "dev": true,
+            "optional": true
+        },
+        "node_modules/scheduler": {
+            "version": "0.23.0",
+            "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
+            "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
+            "dev": true,
             "dependencies": {
-                "caniuse-lite": "^1.0.30001317",
-                "electron-to-chromium": "^1.4.84",
-                "escalade": "^3.1.1",
-                "node-releases": "^2.0.2",
-                "picocolors": "^1.0.0"
+                "loose-envify": "^1.1.0"
+            }
+        },
+        "node_modules/semver": {
+            "version": "7.5.4",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+            "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+            "dev": true,
+            "dependencies": {
+                "lru-cache": "^6.0.0"
             },
             "bin": {
-                "browserslist": "cli.js"
+                "semver": "bin/semver.js"
             },
             "engines": {
-                "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+                "node": ">=10"
             }
         },
-        "node_modules/tap/node_modules/caller-callsite": {
-            "version": "4.1.0",
+        "node_modules/semver/node_modules/lru-cache": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+            "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "callsites": "^3.1.0"
+                "yallist": "^4.0.0"
             },
             "engines": {
-                "node": ">=8"
+                "node": ">=10"
             }
         },
-        "node_modules/tap/node_modules/caller-path": {
-            "version": "3.0.1",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
+        "node_modules/set-blocking": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+            "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+            "dev": true
+        },
+        "node_modules/shebang-command": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+            "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
             "dependencies": {
-                "caller-callsite": "^4.1.0"
+                "shebang-regex": "^3.0.0"
             },
             "engines": {
                 "node": ">=8"
             }
         },
-        "node_modules/tap/node_modules/callsites": {
-            "version": "3.1.0",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
+        "node_modules/shebang-regex": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+            "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
             "engines": {
-                "node": ">=6"
+                "node": ">=8"
             }
         },
-        "node_modules/tap/node_modules/caniuse-lite": {
-            "version": "1.0.30001319",
+        "node_modules/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,
-            "funding": [
-                {
-                    "type": "opencollective",
-                    "url": "https://opencollective.com/browserslist"
-                },
-                {
-                    "type": "tidelift",
-                    "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
-                }
-            ],
-            "inBundle": true,
-            "license": "CC-BY-4.0"
+            "engines": {
+                "node": ">=14"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            }
         },
-        "node_modules/tap/node_modules/cardinal": {
-            "version": "2.1.1",
+        "node_modules/sigstore": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.1.0.tgz",
+            "integrity": "sha512-kPIj+ZLkyI3QaM0qX8V/nSsweYND3W448pwkDgS6CQ74MfhEkIR8ToK5Iyx46KJYRjseVcD3Rp9zAmUAj6ZjPw==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "ansicolors": "~0.3.2",
-                "redeyed": "~2.1.0"
+                "@sigstore/bundle": "^2.1.0",
+                "@sigstore/protobuf-specs": "^0.2.1",
+                "@sigstore/sign": "^2.1.0",
+                "@sigstore/tuf": "^2.1.0"
             },
-            "bin": {
-                "cdl": "bin/cdl.js"
+            "engines": {
+                "node": "^16.14.0 || >=18.0.0"
             }
         },
-        "node_modules/tap/node_modules/chalk": {
-            "version": "2.4.2",
+        "node_modules/slice-ansi": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-6.0.0.tgz",
+            "integrity": "sha512-6bn4hRfkTvDfUoEQYkERg0BVF1D0vrX9HEkMl08uDiNWvVvjylLHvZFZWkDo6wjT8tUctbYl1nCOuE66ZTaUtA==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "ansi-styles": "^3.2.1",
-                "escape-string-regexp": "^1.0.5",
-                "supports-color": "^5.3.0"
+                "ansi-styles": "^6.2.1",
+                "is-fullwidth-code-point": "^4.0.0"
             },
             "engines": {
-                "node": ">=4"
+                "node": ">=14.16"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/slice-ansi?sponsor=1"
             }
         },
-        "node_modules/tap/node_modules/ci-info": {
-            "version": "2.0.0",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT"
-        },
-        "node_modules/tap/node_modules/cli-boxes": {
-            "version": "2.2.1",
+        "node_modules/slice-ansi/node_modules/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,
-            "inBundle": true,
-            "license": "MIT",
             "engines": {
-                "node": ">=6"
+                "node": ">=12"
             },
             "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
+                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
             }
         },
-        "node_modules/tap/node_modules/cli-cursor": {
-            "version": "3.1.0",
+        "node_modules/smart-buffer": {
+            "version": "4.2.0",
+            "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+            "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
-            "dependencies": {
-                "restore-cursor": "^3.1.0"
-            },
             "engines": {
-                "node": ">=8"
+                "node": ">= 6.0.0",
+                "npm": ">= 3.0.0"
             }
         },
-        "node_modules/tap/node_modules/cli-truncate": {
-            "version": "2.1.0",
+        "node_modules/socks": {
+            "version": "2.7.1",
+            "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz",
+            "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "slice-ansi": "^3.0.0",
-                "string-width": "^4.2.0"
+                "ip": "^2.0.0",
+                "smart-buffer": "^4.2.0"
             },
             "engines": {
-                "node": ">=8"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
+                "node": ">= 10.13.0",
+                "npm": ">= 3.0.0"
             }
         },
-        "node_modules/tap/node_modules/code-excerpt": {
-            "version": "3.0.0",
+        "node_modules/socks-proxy-agent": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz",
+            "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "convert-to-spaces": "^1.0.1"
+                "agent-base": "^6.0.2",
+                "debug": "^4.3.3",
+                "socks": "^2.6.2"
             },
             "engines": {
-                "node": ">=10"
+                "node": ">= 10"
             }
         },
-        "node_modules/tap/node_modules/color-convert": {
-            "version": "1.9.3",
+        "node_modules/spdx-correct": {
+            "version": "3.2.0",
+            "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
+            "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "color-name": "1.1.3"
+                "spdx-expression-parse": "^3.0.0",
+                "spdx-license-ids": "^3.0.0"
             }
         },
-        "node_modules/tap/node_modules/color-name": {
-            "version": "1.1.3",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT"
-        },
-        "node_modules/tap/node_modules/commondir": {
-            "version": "1.0.1",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT"
-        },
-        "node_modules/tap/node_modules/concat-map": {
-            "version": "0.0.1",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT"
+        "node_modules/spdx-exceptions": {
+            "version": "2.3.0",
+            "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
+            "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
+            "dev": true
         },
-        "node_modules/tap/node_modules/convert-source-map": {
-            "version": "1.8.0",
+        "node_modules/spdx-expression-parse": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+            "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "safe-buffer": "~5.1.1"
-            }
-        },
-        "node_modules/tap/node_modules/convert-to-spaces": {
-            "version": "1.0.2",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">= 4"
+                "spdx-exceptions": "^2.1.0",
+                "spdx-license-ids": "^3.0.0"
             }
         },
-        "node_modules/tap/node_modules/csstype": {
-            "version": "3.0.11",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT"
+        "node_modules/spdx-license-ids": {
+            "version": "3.0.15",
+            "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.15.tgz",
+            "integrity": "sha512-lpT8hSQp9jAKp9mhtBU4Xjon8LPGBvLIuBiSVhMEtmLecTh2mO0tlqrAMp47tBXzMr13NJMQ2lf7RpQGLJ3HsQ==",
+            "dev": true
         },
-        "node_modules/tap/node_modules/debug": {
-            "version": "4.3.4",
+        "node_modules/ssri": {
+            "version": "10.0.5",
+            "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.5.tgz",
+            "integrity": "sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "ms": "2.1.2"
+                "minipass": "^7.0.3"
             },
             "engines": {
-                "node": ">=6.0"
-            },
-            "peerDependenciesMeta": {
-                "supports-color": {
-                    "optional": true
-                }
+                "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
             }
         },
-        "node_modules/tap/node_modules/electron-to-chromium": {
-            "version": "1.4.89",
-            "dev": true,
-            "inBundle": true,
-            "license": "ISC"
-        },
-        "node_modules/tap/node_modules/emoji-regex": {
-            "version": "8.0.0",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT"
-        },
-        "node_modules/tap/node_modules/escalade": {
-            "version": "3.1.1",
+        "node_modules/stack-utils": {
+            "version": "2.0.6",
+            "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
+            "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
+            "dependencies": {
+                "escape-string-regexp": "^2.0.0"
+            },
             "engines": {
-                "node": ">=6"
+                "node": ">=10"
             }
         },
-        "node_modules/tap/node_modules/escape-string-regexp": {
-            "version": "1.0.5",
+        "node_modules/stack-utils/node_modules/escape-string-regexp": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
+            "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "engines": {
-                "node": ">=0.8.0"
+                "node": ">=8"
             }
         },
-        "node_modules/tap/node_modules/esprima": {
-            "version": "4.0.1",
+        "node_modules/string_decoder": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+            "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
             "dev": true,
-            "inBundle": true,
-            "license": "BSD-2-Clause",
-            "bin": {
-                "esparse": "bin/esparse.js",
-                "esvalidate": "bin/esvalidate.js"
-            },
-            "engines": {
-                "node": ">=4"
+            "dependencies": {
+                "safe-buffer": "~5.2.0"
             }
         },
-        "node_modules/tap/node_modules/events-to-array": {
-            "version": "1.1.2",
-            "dev": true,
-            "inBundle": true,
-            "license": "ISC"
-        },
-        "node_modules/tap/node_modules/find-cache-dir": {
-            "version": "3.3.2",
+        "node_modules/string-length": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/string-length/-/string-length-6.0.0.tgz",
+            "integrity": "sha512-1U361pxZHEQ+FeSjzqRpV+cu2vTzYeWeafXFLykiFlv4Vc0n3njgU8HrMbyik5uwm77naWMuVG8fhEF+Ovb1Kg==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "commondir": "^1.0.1",
-                "make-dir": "^3.0.2",
-                "pkg-dir": "^4.1.0"
+                "strip-ansi": "^7.1.0"
             },
             "engines": {
-                "node": ">=8"
+                "node": ">=16"
             },
             "funding": {
-                "url": "https://github.com/avajs/find-cache-dir?sponsor=1"
+                "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/tap/node_modules/find-up": {
-            "version": "4.1.0",
+        "node_modules/string-length/node_modules/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,
-            "inBundle": true,
-            "license": "MIT",
-            "dependencies": {
-                "locate-path": "^5.0.0",
-                "path-exists": "^4.0.0"
-            },
             "engines": {
-                "node": ">=8"
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/ansi-regex?sponsor=1"
             }
         },
-        "node_modules/tap/node_modules/fs.realpath": {
-            "version": "1.0.0",
+        "node_modules/string-length/node_modules/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,
-            "inBundle": true,
-            "license": "ISC"
-        },
-        "node_modules/tap/node_modules/gensync": {
-            "version": "1.0.0-beta.2",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
+            "dependencies": {
+                "ansi-regex": "^6.0.1"
+            },
             "engines": {
-                "node": ">=6.9.0"
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/strip-ansi?sponsor=1"
             }
         },
-        "node_modules/tap/node_modules/glob": {
-            "version": "7.2.3",
+        "node_modules/string-width": {
+            "version": "5.1.2",
+            "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+            "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
             "dev": true,
-            "inBundle": true,
-            "license": "ISC",
             "dependencies": {
-                "fs.realpath": "^1.0.0",
-                "inflight": "^1.0.4",
-                "inherits": "2",
-                "minimatch": "^3.1.1",
-                "once": "^1.3.0",
-                "path-is-absolute": "^1.0.0"
+                "eastasianwidth": "^0.2.0",
+                "emoji-regex": "^9.2.2",
+                "strip-ansi": "^7.0.1"
             },
             "engines": {
-                "node": "*"
+                "node": ">=12"
             },
             "funding": {
-                "url": "https://github.com/sponsors/isaacs"
+                "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/tap/node_modules/globals": {
-            "version": "11.12.0",
+        "node_modules/string-width-cjs": {
+            "name": "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,
-            "inBundle": true,
-            "license": "MIT",
+            "dependencies": {
+                "emoji-regex": "^8.0.0",
+                "is-fullwidth-code-point": "^3.0.0",
+                "strip-ansi": "^6.0.1"
+            },
             "engines": {
-                "node": ">=4"
+                "node": ">=8"
             }
         },
-        "node_modules/tap/node_modules/has-flag": {
-            "version": "3.0.0",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=4"
-            }
+        "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
         },
-        "node_modules/tap/node_modules/indent-string": {
-            "version": "4.0.0",
+        "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,
-            "inBundle": true,
-            "license": "MIT",
             "engines": {
                 "node": ">=8"
             }
         },
-        "node_modules/tap/node_modules/inflight": {
-            "version": "1.0.6",
-            "dev": true,
-            "inBundle": true,
-            "license": "ISC",
-            "dependencies": {
-                "once": "^1.3.0",
-                "wrappy": "1"
-            }
-        },
-        "node_modules/tap/node_modules/inherits": {
-            "version": "2.0.4",
-            "dev": true,
-            "inBundle": true,
-            "license": "ISC"
-        },
-        "node_modules/tap/node_modules/ink": {
-            "version": "3.2.0",
+        "node_modules/string-width/node_modules/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,
-            "inBundle": true,
-            "license": "MIT",
-            "dependencies": {
-                "ansi-escapes": "^4.2.1",
-                "auto-bind": "4.0.0",
-                "chalk": "^4.1.0",
-                "cli-boxes": "^2.2.0",
-                "cli-cursor": "^3.1.0",
-                "cli-truncate": "^2.1.0",
-                "code-excerpt": "^3.0.0",
-                "indent-string": "^4.0.0",
-                "is-ci": "^2.0.0",
-                "lodash": "^4.17.20",
-                "patch-console": "^1.0.0",
-                "react-devtools-core": "^4.19.1",
-                "react-reconciler": "^0.26.2",
-                "scheduler": "^0.20.2",
-                "signal-exit": "^3.0.2",
-                "slice-ansi": "^3.0.0",
-                "stack-utils": "^2.0.2",
-                "string-width": "^4.2.2",
-                "type-fest": "^0.12.0",
-                "widest-line": "^3.1.0",
-                "wrap-ansi": "^6.2.0",
-                "ws": "^7.5.5",
-                "yoga-layout-prebuilt": "^1.9.6"
-            },
             "engines": {
-                "node": ">=10"
-            },
-            "peerDependencies": {
-                "@types/react": ">=16.8.0",
-                "react": ">=16.8.0"
+                "node": ">=12"
             },
-            "peerDependenciesMeta": {
-                "@types/react": {
-                    "optional": true
-                }
+            "funding": {
+                "url": "https://github.com/chalk/ansi-regex?sponsor=1"
             }
         },
-        "node_modules/tap/node_modules/ink/node_modules/ansi-styles": {
-            "version": "4.3.0",
+        "node_modules/string-width/node_modules/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,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "color-convert": "^2.0.1"
+                "ansi-regex": "^6.0.1"
             },
             "engines": {
-                "node": ">=8"
+                "node": ">=12"
             },
             "funding": {
-                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+                "url": "https://github.com/chalk/strip-ansi?sponsor=1"
             }
         },
-        "node_modules/tap/node_modules/ink/node_modules/chalk": {
-            "version": "4.1.2",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
+        "node_modules/strip-ansi": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
             "dependencies": {
-                "ansi-styles": "^4.1.0",
-                "supports-color": "^7.1.0"
+                "ansi-regex": "^5.0.1"
             },
             "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/chalk?sponsor=1"
+                "node": ">=8"
             }
         },
-        "node_modules/tap/node_modules/ink/node_modules/color-convert": {
-            "version": "2.0.1",
+        "node_modules/strip-ansi-cjs": {
+            "name": "strip-ansi",
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "color-name": "~1.1.4"
+                "ansi-regex": "^5.0.1"
             },
             "engines": {
-                "node": ">=7.0.0"
+                "node": ">=8"
             }
         },
-        "node_modules/tap/node_modules/ink/node_modules/color-name": {
-            "version": "1.1.4",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT"
-        },
-        "node_modules/tap/node_modules/ink/node_modules/has-flag": {
-            "version": "4.0.0",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
+        "node_modules/strip-json-comments": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+            "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
             "engines": {
                 "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/tap/node_modules/ink/node_modules/supports-color": {
+        "node_modules/striptags": {
+            "version": "4.0.0-alpha.4",
+            "resolved": "https://registry.npmjs.org/striptags/-/striptags-4.0.0-alpha.4.tgz",
+            "integrity": "sha512-/0jWyVWhpg9ciRHfjKYBpMHXct/HrFRfsR2HU77nGPbc8SPcVSIHZlZR/0TG3MyPq2C+HiHuwx8BlbcdI/cNbw=="
+        },
+        "node_modules/supports-color": {
             "version": "7.2.0",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
+            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+            "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
             "dependencies": {
                 "has-flag": "^4.0.0"
             },
@@ -3864,760 +4807,794 @@
                 "node": ">=8"
             }
         },
-        "node_modules/tap/node_modules/is-ci": {
-            "version": "2.0.0",
+        "node_modules/sync-content": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/sync-content/-/sync-content-1.0.2.tgz",
+            "integrity": "sha512-znd3rYiiSxU3WteWyS9a6FXkTA/Wjk8WQsOyzHbineeL837dLn3DA4MRhsIX3qGcxDMH6+uuFV4axztssk7wEQ==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "ci-info": "^2.0.0"
+                "glob": "^10.2.6",
+                "mkdirp": "^3.0.1",
+                "path-scurry": "^1.9.2",
+                "rimraf": "^5.0.1"
             },
             "bin": {
-                "is-ci": "bin.js"
-            }
-        },
-        "node_modules/tap/node_modules/is-fullwidth-code-point": {
-            "version": "3.0.0",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
+                "sync-content": "dist/mjs/bin.mjs"
+            },
             "engines": {
-                "node": ">=8"
+                "node": ">=14"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/tap/node_modules/js-tokens": {
-            "version": "4.0.0",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT"
-        },
-        "node_modules/tap/node_modules/jsesc": {
-            "version": "2.5.2",
+        "node_modules/sync-content/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,
-            "inBundle": true,
-            "license": "MIT",
-            "bin": {
-                "jsesc": "bin/jsesc"
-            },
-            "engines": {
-                "node": ">=4"
+            "dependencies": {
+                "balanced-match": "^1.0.0"
             }
         },
-        "node_modules/tap/node_modules/json5": {
-            "version": "2.2.3",
+        "node_modules/sync-content/node_modules/glob": {
+            "version": "10.3.10",
+            "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+            "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
+            "dependencies": {
+                "foreground-child": "^3.1.0",
+                "jackspeak": "^2.3.5",
+                "minimatch": "^9.0.1",
+                "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+                "path-scurry": "^1.10.1"
+            },
             "bin": {
-                "json5": "lib/cli.js"
+                "glob": "dist/esm/bin.mjs"
             },
             "engines": {
-                "node": ">=6"
+                "node": ">=16 || 14 >=14.17"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/tap/node_modules/locate-path": {
-            "version": "5.0.0",
+        "node_modules/sync-content/node_modules/minimatch": {
+            "version": "9.0.3",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+            "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "p-locate": "^4.1.0"
+                "brace-expansion": "^2.0.1"
             },
             "engines": {
-                "node": ">=8"
+                "node": ">=16 || 14 >=14.17"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/tap/node_modules/lodash": {
-            "version": "4.17.21",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT"
-        },
-        "node_modules/tap/node_modules/loose-envify": {
-            "version": "1.4.0",
+        "node_modules/sync-content/node_modules/rimraf": {
+            "version": "5.0.5",
+            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz",
+            "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "js-tokens": "^3.0.0 || ^4.0.0"
+                "glob": "^10.3.7"
             },
             "bin": {
-                "loose-envify": "cli.js"
-            }
-        },
-        "node_modules/tap/node_modules/make-dir": {
-            "version": "3.1.0",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
-            "dependencies": {
-                "semver": "^6.0.0"
+                "rimraf": "dist/esm/bin.mjs"
             },
             "engines": {
-                "node": ">=8"
+                "node": ">=14"
             },
             "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/tap/node_modules/mimic-fn": {
-            "version": "2.1.0",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
+        "node_modules/tap": {
+            "version": "18.4.0",
+            "resolved": "https://registry.npmjs.org/tap/-/tap-18.4.0.tgz",
+            "integrity": "sha512-42bqz0KpoDg8F6Gs5zrTVOELq5ShaK86rCsRG6C6uJM7nUANCB3GW9Dmvy3BGHRll4wAwr+SA+iM0tvBQtrilg==",
+            "dev": true,
+            "dependencies": {
+                "@tapjs/after": "1.1.4",
+                "@tapjs/after-each": "1.1.4",
+                "@tapjs/asserts": "1.1.4",
+                "@tapjs/before": "1.1.4",
+                "@tapjs/before-each": "1.1.4",
+                "@tapjs/core": "1.3.4",
+                "@tapjs/filter": "1.2.4",
+                "@tapjs/fixture": "1.2.4",
+                "@tapjs/intercept": "1.2.4",
+                "@tapjs/mock": "1.2.2",
+                "@tapjs/node-serialize": "1.1.4",
+                "@tapjs/run": "1.4.0",
+                "@tapjs/snapshot": "1.2.4",
+                "@tapjs/spawn": "1.1.4",
+                "@tapjs/stdin": "1.1.4",
+                "@tapjs/test": "1.3.4",
+                "@tapjs/typescript": "1.2.4",
+                "@tapjs/worker": "1.1.4"
+            },
+            "bin": {
+                "tap": "dist/esm/run.mjs"
+            },
             "engines": {
-                "node": ">=6"
+                "node": ">=16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/tap/node_modules/minimatch": {
-            "version": "3.1.2",
+        "node_modules/tap-parser": {
+            "version": "15.2.0",
+            "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-15.2.0.tgz",
+            "integrity": "sha512-bDBR7cuVLfsmmc7ruerZXVBlDtJwqqWzqlO9BFNgw6gprpzjnjyfdc+fsW6mNUYSoxdVEeY7NFgrgGa81EuQ5w==",
             "dev": true,
-            "inBundle": true,
-            "license": "ISC",
             "dependencies": {
-                "brace-expansion": "^1.1.7"
+                "events-to-array": "^2.0.3",
+                "tap-yaml": "2.2.0"
+            },
+            "bin": {
+                "tap-parser": "bin/cmd.cjs"
             },
             "engines": {
-                "node": "*"
+                "node": ">=16"
             }
         },
-        "node_modules/tap/node_modules/minipass": {
-            "version": "3.3.4",
+        "node_modules/tap-yaml": {
+            "version": "2.2.0",
+            "resolved": "https://registry.npmjs.org/tap-yaml/-/tap-yaml-2.2.0.tgz",
+            "integrity": "sha512-o8I7WDNiGpuF04tGAVaNYY5rX9waCtqw9A7Y0YVSQBGcFwNUJWUPLkr2lbhgLRTxc+Tpnw4xUXlIanZc+ZAGnw==",
             "dev": true,
-            "inBundle": true,
-            "license": "ISC",
             "dependencies": {
-                "yallist": "^4.0.0"
+                "yaml": "^2.3.0",
+                "yaml-types": "^0.3.0"
             },
             "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/tap/node_modules/ms": {
-            "version": "2.1.2",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT"
-        },
-        "node_modules/tap/node_modules/node-releases": {
-            "version": "2.0.2",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT"
-        },
-        "node_modules/tap/node_modules/object-assign": {
-            "version": "4.1.1",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/tap/node_modules/once": {
-            "version": "1.4.0",
-            "dev": true,
-            "inBundle": true,
-            "license": "ISC",
-            "dependencies": {
-                "wrappy": "1"
+                "node": ">=16"
             }
         },
-        "node_modules/tap/node_modules/onetime": {
-            "version": "5.1.2",
+        "node_modules/tar": {
+            "version": "6.2.0",
+            "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
+            "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "mimic-fn": "^2.1.0"
+                "chownr": "^2.0.0",
+                "fs-minipass": "^2.0.0",
+                "minipass": "^5.0.0",
+                "minizlib": "^2.1.1",
+                "mkdirp": "^1.0.3",
+                "yallist": "^4.0.0"
             },
             "engines": {
-                "node": ">=6"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
+                "node": ">=10"
             }
         },
-        "node_modules/tap/node_modules/p-limit": {
-            "version": "2.3.0",
+        "node_modules/tar/node_modules/fs-minipass": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+            "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "p-try": "^2.0.0"
+                "minipass": "^3.0.0"
             },
             "engines": {
-                "node": ">=6"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
+                "node": ">= 8"
             }
         },
-        "node_modules/tap/node_modules/p-locate": {
-            "version": "4.1.0",
+        "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": {
+            "version": "3.3.6",
+            "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+            "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "p-limit": "^2.2.0"
+                "yallist": "^4.0.0"
             },
             "engines": {
                 "node": ">=8"
             }
         },
-        "node_modules/tap/node_modules/p-try": {
-            "version": "2.2.0",
+        "node_modules/tar/node_modules/minipass": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
+            "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "engines": {
-                "node": ">=6"
+                "node": ">=8"
             }
         },
-        "node_modules/tap/node_modules/patch-console": {
-            "version": "1.0.0",
+        "node_modules/tar/node_modules/mkdirp": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+            "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
+            "bin": {
+                "mkdirp": "bin/cmd.js"
+            },
             "engines": {
                 "node": ">=10"
             }
         },
-        "node_modules/tap/node_modules/path-exists": {
-            "version": "4.0.0",
+        "node_modules/tcompare": {
+            "version": "6.4.0",
+            "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-6.4.0.tgz",
+            "integrity": "sha512-MR0TPvFaEQ53jgMP43aHr3wKGKKPi6Th3nxHoIsBVL0AxjKdfyrIIWvYt7u30NNs57Vc6UP5ooq/sD69IhQPzw==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
+            "dependencies": {
+                "diff": "^5.1.0",
+                "react-element-to-jsx-string": "^15.0.0"
+            },
             "engines": {
-                "node": ">=8"
+                "node": ">=16"
             }
         },
-        "node_modules/tap/node_modules/path-is-absolute": {
-            "version": "1.0.1",
+        "node_modules/tcompare/node_modules/diff": {
+            "version": "5.1.0",
+            "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
+            "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "engines": {
-                "node": ">=0.10.0"
+                "node": ">=0.3.1"
             }
         },
-        "node_modules/tap/node_modules/picocolors": {
-            "version": "1.0.0",
-            "dev": true,
-            "inBundle": true,
-            "license": "ISC"
-        },
-        "node_modules/tap/node_modules/pkg-dir": {
-            "version": "4.2.0",
+        "node_modules/test-exclude": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
+            "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "find-up": "^4.0.0"
+                "@istanbuljs/schema": "^0.1.2",
+                "glob": "^7.1.4",
+                "minimatch": "^3.0.4"
             },
             "engines": {
                 "node": ">=8"
             }
         },
-        "node_modules/tap/node_modules/punycode": {
-            "version": "2.1.1",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=6"
-            }
+        "node_modules/text-table": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+            "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
         },
-        "node_modules/tap/node_modules/react": {
-            "version": "17.0.2",
+        "node_modules/to-regex-range": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+            "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "loose-envify": "^1.1.0",
-                "object-assign": "^4.1.1"
+                "is-number": "^7.0.0"
             },
             "engines": {
-                "node": ">=0.10.0"
+                "node": ">=8.0"
             }
         },
-        "node_modules/tap/node_modules/react-devtools-core": {
-            "version": "4.24.1",
+        "node_modules/trivial-deferred": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/trivial-deferred/-/trivial-deferred-2.0.0.tgz",
+            "integrity": "sha512-iGbM7X2slv9ORDVj2y2FFUq3cP/ypbtu2nQ8S38ufjL0glBABvmR9pTdsib1XtS2LUhhLMbelaBUaf/s5J3dSw==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
-            "dependencies": {
-                "shell-quote": "^1.6.1",
-                "ws": "^7"
+            "engines": {
+                "node": ">= 8"
             }
         },
-        "node_modules/tap/node_modules/react-reconciler": {
-            "version": "0.26.2",
+        "node_modules/ts-node": {
+            "name": "@isaacs/ts-node-temp-fork-for-pr-2009",
+            "version": "10.9.1",
+            "resolved": "https://registry.npmjs.org/@isaacs/ts-node-temp-fork-for-pr-2009/-/ts-node-temp-fork-for-pr-2009-10.9.1.tgz",
+            "integrity": "sha512-MY4rUonz835NsTbd4dcgKZvZFYX9IkLnYFZV9M7GQV8t39fawafLin/Qw6VXD4yfMs4HcBq8P3ddeU0QHMH1YQ==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "loose-envify": "^1.1.0",
-                "object-assign": "^4.1.1",
-                "scheduler": "^0.20.2"
+                "@cspotcode/source-map-support": "^0.8.0",
+                "@tsconfig/node14": "*",
+                "@tsconfig/node16": "*",
+                "@tsconfig/node18": "*",
+                "@tsconfig/node20": "*",
+                "acorn": "^8.4.1",
+                "acorn-walk": "^8.1.1",
+                "arg": "^4.1.0",
+                "diff": "^4.0.1",
+                "make-error": "^1.1.1",
+                "v8-compile-cache-lib": "^3.0.1"
             },
-            "engines": {
-                "node": ">=0.10.0"
+            "bin": {
+                "ts-node": "dist/bin.js",
+                "ts-node-cwd": "dist/bin-cwd.js",
+                "ts-node-esm": "dist/bin-esm.js",
+                "ts-node-script": "dist/bin-script.js",
+                "ts-node-transpile-only": "dist/bin-transpile.js"
             },
             "peerDependencies": {
-                "react": "^17.0.2"
+                "@swc/core": ">=1.2.50",
+                "@swc/wasm": ">=1.2.50",
+                "@types/node": "*",
+                "typescript": ">=4.2"
+            },
+            "peerDependenciesMeta": {
+                "@swc/core": {
+                    "optional": true
+                },
+                "@swc/wasm": {
+                    "optional": true
+                }
             }
         },
-        "node_modules/tap/node_modules/redeyed": {
-            "version": "2.1.1",
+        "node_modules/tshy": {
+            "version": "1.2.2",
+            "resolved": "https://registry.npmjs.org/tshy/-/tshy-1.2.2.tgz",
+            "integrity": "sha512-y5ItK4DKLYO+hba7h5sOaCYygNtF44qytZGyjZSE6CQSVfzUfZ2qn/GmXu737amwfCKG9EizPw3oPBWrisF1uw==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "esprima": "~4.0.0"
+                "chalk": "^5.3.0",
+                "foreground-child": "^3.1.1",
+                "mkdirp": "^3.0.1",
+                "resolve-import": "^1.4.1",
+                "rimraf": "^5.0.1",
+                "sync-content": "^1.0.2",
+                "typescript": "5.2",
+                "walk-up-path": "^3.0.1"
+            },
+            "bin": {
+                "tshy": "dist/esm/index.js"
+            },
+            "engines": {
+                "node": "16 >=16.17 || 18 >=18.16.0 || >=20.6.1"
             }
         },
-        "node_modules/tap/node_modules/resolve-from": {
-            "version": "3.0.0",
+        "node_modules/tshy/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,
-            "inBundle": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=4"
+            "dependencies": {
+                "balanced-match": "^1.0.0"
             }
         },
-        "node_modules/tap/node_modules/restore-cursor": {
-            "version": "3.1.0",
+        "node_modules/tshy/node_modules/chalk": {
+            "version": "5.3.0",
+            "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
+            "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
-            "dependencies": {
-                "onetime": "^5.1.0",
-                "signal-exit": "^3.0.2"
-            },
             "engines": {
-                "node": ">=8"
+                "node": "^12.17.0 || ^14.13 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/chalk?sponsor=1"
             }
         },
-        "node_modules/tap/node_modules/rimraf": {
-            "version": "3.0.2",
+        "node_modules/tshy/node_modules/glob": {
+            "version": "10.3.10",
+            "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+            "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
             "dev": true,
-            "inBundle": true,
-            "license": "ISC",
             "dependencies": {
-                "glob": "^7.1.3"
+                "foreground-child": "^3.1.0",
+                "jackspeak": "^2.3.5",
+                "minimatch": "^9.0.1",
+                "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+                "path-scurry": "^1.10.1"
             },
             "bin": {
-                "rimraf": "bin.js"
+                "glob": "dist/esm/bin.mjs"
+            },
+            "engines": {
+                "node": ">=16 || 14 >=14.17"
             },
             "funding": {
                 "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/tap/node_modules/safe-buffer": {
-            "version": "5.1.2",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT"
-        },
-        "node_modules/tap/node_modules/scheduler": {
-            "version": "0.20.2",
+        "node_modules/tshy/node_modules/minimatch": {
+            "version": "9.0.3",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+            "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "loose-envify": "^1.1.0",
-                "object-assign": "^4.1.1"
-            }
-        },
-        "node_modules/tap/node_modules/semver": {
-            "version": "6.3.0",
-            "dev": true,
-            "inBundle": true,
-            "license": "ISC",
-            "bin": {
-                "semver": "bin/semver.js"
-            }
-        },
-        "node_modules/tap/node_modules/shell-quote": {
-            "version": "1.7.3",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT"
-        },
-        "node_modules/tap/node_modules/signal-exit": {
-            "version": "3.0.7",
-            "dev": true,
-            "inBundle": true,
-            "license": "ISC"
-        },
-        "node_modules/tap/node_modules/slice-ansi": {
-            "version": "3.0.0",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
-            "dependencies": {
-                "ansi-styles": "^4.0.0",
-                "astral-regex": "^2.0.0",
-                "is-fullwidth-code-point": "^3.0.0"
+                "brace-expansion": "^2.0.1"
             },
             "engines": {
-                "node": ">=8"
+                "node": ">=16 || 14 >=14.17"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/tap/node_modules/slice-ansi/node_modules/ansi-styles": {
-            "version": "4.3.0",
+        "node_modules/tshy/node_modules/rimraf": {
+            "version": "5.0.5",
+            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz",
+            "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "color-convert": "^2.0.1"
+                "glob": "^10.3.7"
+            },
+            "bin": {
+                "rimraf": "dist/esm/bin.mjs"
             },
             "engines": {
-                "node": ">=8"
+                "node": ">=14"
             },
             "funding": {
-                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/tap/node_modules/slice-ansi/node_modules/color-convert": {
-            "version": "2.0.1",
+        "node_modules/tslib": {
+            "version": "2.6.2",
+            "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+            "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
+            "dev": true
+        },
+        "node_modules/tuf-js": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.1.0.tgz",
+            "integrity": "sha512-eD7YPPjVlMzdggrOeE8zwoegUaG/rt6Bt3jwoQPunRiNVzgcCE009UDFJKJjG+Gk9wFu6W/Vi+P5d/5QpdD9jA==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "color-name": "~1.1.4"
+                "@tufjs/models": "2.0.0",
+                "debug": "^4.3.4",
+                "make-fetch-happen": "^13.0.0"
             },
             "engines": {
-                "node": ">=7.0.0"
+                "node": "^16.14.0 || >=18.0.0"
             }
         },
-        "node_modules/tap/node_modules/slice-ansi/node_modules/color-name": {
-            "version": "1.1.4",
+        "node_modules/tuf-js/node_modules/make-fetch-happen": {
+            "version": "13.0.0",
+            "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.0.tgz",
+            "integrity": "sha512-7ThobcL8brtGo9CavByQrQi+23aIfgYU++wg4B87AIS8Rb2ZBt/MEaDqzA00Xwv/jUjAjYkLHjVolYuTLKda2A==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT"
-        },
-        "node_modules/tap/node_modules/source-map": {
-            "version": "0.5.7",
-            "dev": true,
-            "inBundle": true,
-            "license": "BSD-3-Clause",
+            "dependencies": {
+                "@npmcli/agent": "^2.0.0",
+                "cacache": "^18.0.0",
+                "http-cache-semantics": "^4.1.1",
+                "is-lambda": "^1.0.1",
+                "minipass": "^7.0.2",
+                "minipass-fetch": "^3.0.0",
+                "minipass-flush": "^1.0.5",
+                "minipass-pipeline": "^1.2.4",
+                "negotiator": "^0.6.3",
+                "promise-retry": "^2.0.1",
+                "ssri": "^10.0.0"
+            },
             "engines": {
-                "node": ">=0.10.0"
+                "node": "^16.14.0 || >=18.0.0"
             }
         },
-        "node_modules/tap/node_modules/stack-utils": {
-            "version": "2.0.5",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
+        "node_modules/type-check": {
+            "version": "0.4.0",
+            "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+            "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
             "dependencies": {
-                "escape-string-regexp": "^2.0.0"
+                "prelude-ls": "^1.2.1"
             },
             "engines": {
-                "node": ">=10"
+                "node": ">= 0.8.0"
             }
         },
-        "node_modules/tap/node_modules/stack-utils/node_modules/escape-string-regexp": {
-            "version": "2.0.0",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
+        "node_modules/type-fest": {
+            "version": "0.20.2",
+            "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+            "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
             "engines": {
-                "node": ">=8"
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/tap/node_modules/string-width": {
-            "version": "4.2.3",
+        "node_modules/typescript": {
+            "version": "5.2.2",
+            "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
+            "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
-            "dependencies": {
-                "emoji-regex": "^8.0.0",
-                "is-fullwidth-code-point": "^3.0.0",
-                "strip-ansi": "^6.0.1"
+            "bin": {
+                "tsc": "bin/tsc",
+                "tsserver": "bin/tsserver"
             },
             "engines": {
-                "node": ">=8"
+                "node": ">=14.17"
             }
         },
-        "node_modules/tap/node_modules/strip-ansi": {
-            "version": "6.0.1",
+        "node_modules/unique-filename": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz",
+            "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "ansi-regex": "^5.0.1"
+                "unique-slug": "^4.0.0"
             },
             "engines": {
-                "node": ">=8"
+                "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
             }
         },
-        "node_modules/tap/node_modules/supports-color": {
-            "version": "5.5.0",
+        "node_modules/unique-slug": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz",
+            "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "has-flag": "^3.0.0"
+                "imurmurhash": "^0.1.4"
             },
             "engines": {
-                "node": ">=4"
+                "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
             }
         },
-        "node_modules/tap/node_modules/tap-parser": {
-            "version": "11.0.2",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
+        "node_modules/uri-js": {
+            "version": "4.4.1",
+            "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+            "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
             "dependencies": {
-                "events-to-array": "^1.0.1",
-                "minipass": "^3.1.6",
-                "tap-yaml": "^1.0.0"
-            },
-            "bin": {
-                "tap-parser": "bin/cmd.js"
-            },
-            "engines": {
-                "node": ">= 8"
+                "punycode": "^2.1.0"
             }
         },
-        "node_modules/tap/node_modules/tap-yaml": {
+        "node_modules/util-deprecate": {
             "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+            "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+            "dev": true
+        },
+        "node_modules/uuid": {
+            "version": "8.3.2",
+            "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+            "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
             "dev": true,
-            "inBundle": true,
-            "license": "ISC",
-            "dependencies": {
-                "yaml": "^1.10.2"
+            "bin": {
+                "uuid": "dist/bin/uuid"
             }
         },
-        "node_modules/tap/node_modules/tcompare": {
-            "version": "5.0.7",
-            "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-5.0.7.tgz",
-            "integrity": "sha512-d9iddt6YYGgyxJw5bjsN7UJUO1kGOtjSlNy/4PoGYAjQS5pAT/hzIoLf1bZCw+uUxRmZJh7Yy1aA7xKVRT9B4w==",
+        "node_modules/v8-compile-cache-lib": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+            "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
+            "dev": true
+        },
+        "node_modules/v8-to-istanbul": {
+            "version": "9.1.0",
+            "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz",
+            "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==",
             "dev": true,
             "dependencies": {
-                "diff": "^4.0.2"
+                "@jridgewell/trace-mapping": "^0.3.12",
+                "@types/istanbul-lib-coverage": "^2.0.1",
+                "convert-source-map": "^1.6.0"
             },
             "engines": {
-                "node": ">=10"
+                "node": ">=10.12.0"
             }
         },
-        "node_modules/tap/node_modules/to-fast-properties": {
-            "version": "2.0.0",
+        "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": {
+            "version": "0.3.19",
+            "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz",
+            "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=4"
+            "dependencies": {
+                "@jridgewell/resolve-uri": "^3.1.0",
+                "@jridgewell/sourcemap-codec": "^1.4.14"
             }
         },
-        "node_modules/tap/node_modules/treport": {
+        "node_modules/validate-npm-package-license": {
             "version": "3.0.4",
+            "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+            "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
             "dev": true,
-            "inBundle": true,
-            "license": "ISC",
             "dependencies": {
-                "@isaacs/import-jsx": "^4.0.1",
-                "cardinal": "^2.1.1",
-                "chalk": "^3.0.0",
-                "ink": "^3.2.0",
-                "ms": "^2.1.2",
-                "tap-parser": "^11.0.0",
-                "tap-yaml": "^1.0.0",
-                "unicode-length": "^2.0.2"
-            },
-            "peerDependencies": {
-                "react": "^17.0.2"
+                "spdx-correct": "^3.0.0",
+                "spdx-expression-parse": "^3.0.0"
             }
         },
-        "node_modules/tap/node_modules/treport/node_modules/ansi-styles": {
-            "version": "4.3.0",
+        "node_modules/validate-npm-package-name": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz",
+            "integrity": "sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "color-convert": "^2.0.1"
+                "builtins": "^5.0.0"
             },
             "engines": {
-                "node": ">=8"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+                "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
             }
         },
-        "node_modules/tap/node_modules/treport/node_modules/chalk": {
-            "version": "3.0.0",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
+        "node_modules/walk-up-path": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-3.0.1.tgz",
+            "integrity": "sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==",
+            "dev": true
+        },
+        "node_modules/which": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+            "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
             "dependencies": {
-                "ansi-styles": "^4.1.0",
-                "supports-color": "^7.1.0"
+                "isexe": "^2.0.0"
+            },
+            "bin": {
+                "node-which": "bin/node-which"
             },
             "engines": {
-                "node": ">=8"
+                "node": ">= 8"
             }
         },
-        "node_modules/tap/node_modules/treport/node_modules/color-convert": {
-            "version": "2.0.1",
+        "node_modules/wide-align": {
+            "version": "1.1.5",
+            "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
+            "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "color-name": "~1.1.4"
-            },
-            "engines": {
-                "node": ">=7.0.0"
+                "string-width": "^1.0.2 || 2 || 3 || 4"
             }
         },
-        "node_modules/tap/node_modules/treport/node_modules/color-name": {
-            "version": "1.1.4",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT"
+        "node_modules/wide-align/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
         },
-        "node_modules/tap/node_modules/treport/node_modules/has-flag": {
-            "version": "4.0.0",
+        "node_modules/wide-align/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,
-            "inBundle": true,
-            "license": "MIT",
             "engines": {
                 "node": ">=8"
             }
         },
-        "node_modules/tap/node_modules/treport/node_modules/supports-color": {
-            "version": "7.2.0",
+        "node_modules/wide-align/node_modules/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,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "has-flag": "^4.0.0"
+                "emoji-regex": "^8.0.0",
+                "is-fullwidth-code-point": "^3.0.0",
+                "strip-ansi": "^6.0.1"
             },
             "engines": {
                 "node": ">=8"
             }
         },
-        "node_modules/tap/node_modules/type-fest": {
-            "version": "0.12.0",
+        "node_modules/widest-line": {
+            "version": "4.0.1",
+            "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz",
+            "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==",
             "dev": true,
-            "inBundle": true,
-            "license": "(MIT OR CC0-1.0)",
+            "dependencies": {
+                "string-width": "^5.0.1"
+            },
             "engines": {
-                "node": ">=10"
+                "node": ">=12"
             },
             "funding": {
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/tap/node_modules/unicode-length": {
-            "version": "2.0.2",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
-            "dependencies": {
-                "punycode": "^2.0.0",
-                "strip-ansi": "^3.0.1"
-            }
-        },
-        "node_modules/tap/node_modules/unicode-length/node_modules/ansi-regex": {
-            "version": "2.1.1",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
+        "node_modules/word-wrap": {
+            "version": "1.2.5",
+            "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+            "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
             "engines": {
                 "node": ">=0.10.0"
             }
         },
-        "node_modules/tap/node_modules/unicode-length/node_modules/strip-ansi": {
-            "version": "3.0.1",
+        "node_modules/wrap-ansi": {
+            "version": "8.1.0",
+            "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+            "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "ansi-regex": "^2.0.0"
+                "ansi-styles": "^6.1.0",
+                "string-width": "^5.0.1",
+                "strip-ansi": "^7.0.1"
             },
             "engines": {
-                "node": ">=0.10.0"
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
             }
         },
-        "node_modules/tap/node_modules/widest-line": {
-            "version": "3.1.0",
+        "node_modules/wrap-ansi-cjs": {
+            "name": "wrap-ansi",
+            "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,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "string-width": "^4.0.0"
+                "ansi-styles": "^4.0.0",
+                "string-width": "^4.1.0",
+                "strip-ansi": "^6.0.0"
             },
             "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+            }
+        },
+        "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
+        },
+        "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"
             }
         },
-        "node_modules/tap/node_modules/wrap-ansi": {
-            "version": "6.2.0",
+        "node_modules/wrap-ansi-cjs/node_modules/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,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "ansi-styles": "^4.0.0",
-                "string-width": "^4.1.0",
-                "strip-ansi": "^6.0.0"
+                "emoji-regex": "^8.0.0",
+                "is-fullwidth-code-point": "^3.0.0",
+                "strip-ansi": "^6.0.1"
             },
             "engines": {
                 "node": ">=8"
             }
         },
-        "node_modules/tap/node_modules/wrap-ansi/node_modules/ansi-styles": {
-            "version": "4.3.0",
+        "node_modules/wrap-ansi/node_modules/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,
-            "inBundle": true,
-            "license": "MIT",
-            "dependencies": {
-                "color-convert": "^2.0.1"
+            "engines": {
+                "node": ">=12"
             },
+            "funding": {
+                "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+            }
+        },
+        "node_modules/wrap-ansi/node_modules/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,
             "engines": {
-                "node": ">=8"
+                "node": ">=12"
             },
             "funding": {
                 "url": "https://github.com/chalk/ansi-styles?sponsor=1"
             }
         },
-        "node_modules/tap/node_modules/wrap-ansi/node_modules/color-convert": {
-            "version": "2.0.1",
+        "node_modules/wrap-ansi/node_modules/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,
-            "inBundle": true,
-            "license": "MIT",
             "dependencies": {
-                "color-name": "~1.1.4"
+                "ansi-regex": "^6.0.1"
             },
             "engines": {
-                "node": ">=7.0.0"
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/strip-ansi?sponsor=1"
             }
         },
-        "node_modules/tap/node_modules/wrap-ansi/node_modules/color-name": {
-            "version": "1.1.4",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT"
-        },
-        "node_modules/tap/node_modules/wrappy": {
+        "node_modules/wrappy": {
             "version": "1.0.2",
-            "dev": true,
-            "inBundle": true,
-            "license": "ISC"
+            "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+            "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
         },
-        "node_modules/tap/node_modules/ws": {
-            "version": "7.5.7",
+        "node_modules/ws": {
+            "version": "8.14.2",
+            "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
+            "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==",
             "dev": true,
-            "inBundle": true,
-            "license": "MIT",
             "engines": {
-                "node": ">=8.3.0"
+                "node": ">=10.0.0"
             },
             "peerDependencies": {
                 "bufferutil": "^4.0.1",
-                "utf-8-validate": "^5.0.2"
+                "utf-8-validate": ">=5.0.2"
             },
             "peerDependenciesMeta": {
                 "bufferutil": {
@@ -4628,118 +5605,103 @@
                 }
             }
         },
-        "node_modules/tap/node_modules/yallist": {
-            "version": "4.0.0",
-            "dev": true,
-            "inBundle": true,
-            "license": "ISC"
-        },
-        "node_modules/tap/node_modules/yaml": {
-            "version": "1.10.2",
+        "node_modules/y18n": {
+            "version": "5.0.8",
+            "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+            "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
             "dev": true,
-            "inBundle": true,
-            "license": "ISC",
             "engines": {
-                "node": ">= 6"
+                "node": ">=10"
             }
         },
-        "node_modules/tap/node_modules/yoga-layout-prebuilt": {
-            "version": "1.10.0",
-            "dev": true,
-            "inBundle": true,
-            "license": "MIT",
-            "dependencies": {
-                "@types/yoga-layout": "1.9.2"
-            },
-            "engines": {
-                "node": ">=8"
-            }
+        "node_modules/yallist": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+            "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+            "dev": true
         },
-        "node_modules/tcompare": {
-            "version": "6.0.0",
-            "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-6.0.0.tgz",
-            "integrity": "sha512-JeX89lSVkxTzYND0LxzFCGrXm/TqGEQ0heu1JTwplnpaYQNky6hIaO4lQBOrs+/P787i3CoK9T/O3/oEcnJXvA==",
+        "node_modules/yaml": {
+            "version": "2.3.2",
+            "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz",
+            "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==",
             "dev": true,
-            "dependencies": {
-                "diff": "^5.1.0"
-            },
             "engines": {
-                "node": ">=16"
+                "node": ">= 14"
             }
         },
-        "node_modules/tcompare/node_modules/diff": {
-            "version": "5.1.0",
-            "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
-            "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
+        "node_modules/yaml-types": {
+            "version": "0.3.0",
+            "resolved": "https://registry.npmjs.org/yaml-types/-/yaml-types-0.3.0.tgz",
+            "integrity": "sha512-i9RxAO/LZBiE0NJUy9pbN5jFz5EasYDImzRkj8Y81kkInTi1laia3P3K/wlMKzOxFQutZip8TejvQP/DwgbU7A==",
             "dev": true,
             "engines": {
-                "node": ">=0.3.1"
+                "node": ">= 16",
+                "npm": ">= 7"
+            },
+            "peerDependencies": {
+                "yaml": "^2.3.0"
             }
         },
-        "node_modules/test-exclude": {
-            "version": "6.0.0",
-            "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
-            "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
+        "node_modules/yargs": {
+            "version": "17.7.2",
+            "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+            "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
             "dev": true,
             "dependencies": {
-                "@istanbuljs/schema": "^0.1.2",
-                "glob": "^7.1.4",
-                "minimatch": "^3.0.4"
+                "cliui": "^8.0.1",
+                "escalade": "^3.1.1",
+                "get-caller-file": "^2.0.5",
+                "require-directory": "^2.1.1",
+                "string-width": "^4.2.3",
+                "y18n": "^5.0.5",
+                "yargs-parser": "^21.1.1"
             },
             "engines": {
-                "node": ">=8"
+                "node": ">=12"
             }
         },
-        "node_modules/text-table": {
-            "version": "0.2.0",
-            "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
-            "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
-        },
-        "node_modules/to-fast-properties": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
-            "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
+        "node_modules/yargs-parser": {
+            "version": "21.1.1",
+            "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+            "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
             "dev": true,
             "engines": {
-                "node": ">=4"
+                "node": ">=12"
             }
         },
-        "node_modules/to-regex-range": {
-            "version": "5.0.1",
-            "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
-            "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
-            "dev": true,
-            "dependencies": {
-                "is-number": "^7.0.0"
-            },
-            "engines": {
-                "node": ">=8.0"
-            }
+        "node_modules/yargs/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
         },
-        "node_modules/trivial-deferred": {
-            "version": "1.1.2",
-            "resolved": "https://registry.npmjs.org/trivial-deferred/-/trivial-deferred-1.1.2.tgz",
-            "integrity": "sha512-vDPiDBC3hyP6O4JrJYMImW3nl3c03Tsj9fEXc7Qc/XKa1O7gf5ZtFfIR/E0dun9SnDHdwjna1Z2rSzYgqpxh/g==",
+        "node_modules/yargs/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"
+                "node": ">=8"
             }
         },
-        "node_modules/type-check": {
-            "version": "0.4.0",
-            "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
-            "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+        "node_modules/yargs/node_modules/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,
             "dependencies": {
-                "prelude-ls": "^1.2.1"
+                "emoji-regex": "^8.0.0",
+                "is-fullwidth-code-point": "^3.0.0",
+                "strip-ansi": "^6.0.1"
             },
             "engines": {
-                "node": ">= 0.8.0"
+                "node": ">=8"
             }
         },
-        "node_modules/type-fest": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
-            "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+        "node_modules/yocto-queue": {
+            "version": "0.1.0",
+            "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+            "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
             "engines": {
                 "node": ">=10"
             },
@@ -4747,689 +5709,956 @@
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/typedarray-to-buffer": {
-            "version": "3.1.5",
-            "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
-            "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+        "node_modules/yoga-wasm-web": {
+            "version": "0.3.3",
+            "resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz",
+            "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==",
+            "dev": true
+        }
+    },
+    "dependencies": {
+        "@alcalzone/ansi-tokenize": {
+            "version": "0.1.3",
+            "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz",
+            "integrity": "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==",
             "dev": true,
+            "requires": {
+                "ansi-styles": "^6.2.1",
+                "is-fullwidth-code-point": "^4.0.0"
+            },
             "dependencies": {
-                "is-typedarray": "^1.0.0"
+                "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
+                }
             }
         },
-        "node_modules/unicode-length": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/unicode-length/-/unicode-length-2.1.0.tgz",
-            "integrity": "sha512-4bV582zTV9Q02RXBxSUMiuN/KHo5w4aTojuKTNT96DIKps/SIawFp7cS5Mu25VuY1AioGXrmYyzKZUzh8OqoUw==",
-            "dev": true,
-            "dependencies": {
-                "punycode": "^2.0.0"
-            }
+        "@base2/pretty-print-object": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz",
+            "integrity": "sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==",
+            "dev": true
+        },
+        "@bcoe/v8-coverage": {
+            "version": "0.2.3",
+            "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
+            "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
+            "dev": true
         },
-        "node_modules/update-browserslist-db": {
-            "version": "1.0.10",
-            "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
-            "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==",
+        "@cspotcode/source-map-support": {
+            "version": "0.8.1",
+            "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+            "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
             "dev": true,
-            "funding": [
-                {
-                    "type": "opencollective",
-                    "url": "https://opencollective.com/browserslist"
-                },
-                {
-                    "type": "tidelift",
-                    "url": "https://tidelift.com/funding/github/npm/browserslist"
-                }
-            ],
-            "dependencies": {
-                "escalade": "^3.1.1",
-                "picocolors": "^1.0.0"
-            },
-            "bin": {
-                "browserslist-lint": "cli.js"
-            },
-            "peerDependencies": {
-                "browserslist": ">= 4.21.0"
+            "requires": {
+                "@jridgewell/trace-mapping": "0.3.9"
             }
         },
-        "node_modules/uri-js": {
-            "version": "4.4.1",
-            "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
-            "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
-            "dependencies": {
-                "punycode": "^2.1.0"
+        "@eslint-community/eslint-utils": {
+            "version": "4.4.0",
+            "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+            "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+            "requires": {
+                "eslint-visitor-keys": "^3.3.0"
             }
         },
-        "node_modules/uuid": {
-            "version": "8.3.2",
-            "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
-            "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
-            "dev": true,
-            "bin": {
-                "uuid": "dist/bin/uuid"
-            }
+        "@eslint-community/regexpp": {
+            "version": "4.5.0",
+            "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz",
+            "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ=="
         },
-        "node_modules/which": {
+        "@eslint/eslintrc": {
             "version": "2.0.2",
-            "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
-            "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
-            "dependencies": {
-                "isexe": "^2.0.0"
-            },
-            "bin": {
-                "node-which": "bin/node-which"
-            },
-            "engines": {
-                "node": ">= 8"
+            "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz",
+            "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==",
+            "requires": {
+                "ajv": "^6.12.4",
+                "debug": "^4.3.2",
+                "espree": "^9.5.1",
+                "globals": "^13.19.0",
+                "ignore": "^5.2.0",
+                "import-fresh": "^3.2.1",
+                "js-yaml": "^4.1.0",
+                "minimatch": "^3.1.2",
+                "strip-json-comments": "^3.1.1"
             }
         },
-        "node_modules/which-module": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
-            "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==",
-            "dev": true
+        "@eslint/js": {
+            "version": "8.37.0",
+            "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.37.0.tgz",
+            "integrity": "sha512-x5vzdtOOGgFVDCUs81QRB2+liax8rFg3+7hqM+QhBG0/G3F1ZsoYl97UrqgHgQ9KKT7G6c4V+aTUCgu/n22v1A=="
         },
-        "node_modules/word-wrap": {
-            "version": "1.2.3",
-            "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
-            "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
-            "engines": {
-                "node": ">=0.10.0"
+        "@humanwhocodes/config-array": {
+            "version": "0.11.8",
+            "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
+            "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
+            "requires": {
+                "@humanwhocodes/object-schema": "^1.2.1",
+                "debug": "^4.1.1",
+                "minimatch": "^3.0.5"
             }
         },
-        "node_modules/wrap-ansi": {
-            "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",
-                "strip-ansi": "^6.0.0"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
-            }
+        "@humanwhocodes/module-importer": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+            "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="
         },
-        "node_modules/wrappy": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-            "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+        "@humanwhocodes/object-schema": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+            "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA=="
         },
-        "node_modules/write-file-atomic": {
-            "version": "3.0.3",
-            "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
-            "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
+        "@isaacs/cliui": {
+            "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",
+                "strip-ansi": "^7.0.1",
+                "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+                "wrap-ansi": "^8.1.0",
+                "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+            },
             "dependencies": {
-                "imurmurhash": "^0.1.4",
-                "is-typedarray": "^1.0.0",
-                "signal-exit": "^3.0.2",
-                "typedarray-to-buffer": "^3.1.5"
+                "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
+                },
+                "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"
+                    }
+                }
             }
         },
-        "node_modules/y18n": {
-            "version": "4.0.3",
-            "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
-            "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
+        "@istanbuljs/schema": {
+            "version": "0.1.3",
+            "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+            "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
             "dev": true
         },
-        "node_modules/yallist": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-            "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+        "@jridgewell/resolve-uri": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
+            "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
             "dev": true
         },
-        "node_modules/yaml": {
-            "version": "1.10.2",
-            "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
-            "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+        "@jridgewell/sourcemap-codec": {
+            "version": "1.4.15",
+            "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
+            "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
+            "dev": true
+        },
+        "@jridgewell/trace-mapping": {
+            "version": "0.3.9",
+            "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+            "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
             "dev": true,
-            "engines": {
-                "node": ">= 6"
+            "requires": {
+                "@jridgewell/resolve-uri": "^3.0.3",
+                "@jridgewell/sourcemap-codec": "^1.4.10"
             }
         },
-        "node_modules/yargs": {
-            "version": "15.4.1",
-            "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
-            "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
-            "dev": true,
-            "dependencies": {
-                "cliui": "^6.0.0",
-                "decamelize": "^1.2.0",
-                "find-up": "^4.1.0",
-                "get-caller-file": "^2.0.1",
-                "require-directory": "^2.1.1",
-                "require-main-filename": "^2.0.0",
-                "set-blocking": "^2.0.0",
-                "string-width": "^4.2.0",
-                "which-module": "^2.0.0",
-                "y18n": "^4.0.0",
-                "yargs-parser": "^18.1.2"
-            },
-            "engines": {
-                "node": ">=8"
+        "@nodelib/fs.scandir": {
+            "version": "2.1.5",
+            "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+            "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+            "requires": {
+                "@nodelib/fs.stat": "2.0.5",
+                "run-parallel": "^1.1.9"
             }
         },
-        "node_modules/yargs-parser": {
-            "version": "18.1.3",
-            "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
-            "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
-            "dev": true,
-            "dependencies": {
-                "camelcase": "^5.0.0",
-                "decamelize": "^1.2.0"
-            },
-            "engines": {
-                "node": ">=6"
+        "@nodelib/fs.stat": {
+            "version": "2.0.5",
+            "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+            "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="
+        },
+        "@nodelib/fs.walk": {
+            "version": "1.2.8",
+            "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+            "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+            "requires": {
+                "@nodelib/fs.scandir": "2.1.5",
+                "fastq": "^1.6.0"
             }
         },
-        "node_modules/yargs/node_modules/cliui": {
-            "version": "6.0.0",
-            "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
-            "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+        "@npmcli/agent": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.1.1.tgz",
+            "integrity": "sha512-6RlbiOAi6L6uUYF4/CDEkDZQnKw0XDsFJVrEpnib8rAx2WRMOsUyAdgnvDpX/fdkDWxtqE+NHwF465llI2wR0g==",
             "dev": true,
+            "requires": {
+                "http-proxy-agent": "^7.0.0",
+                "https-proxy-agent": "^7.0.1",
+                "lru-cache": "^10.0.1",
+                "socks-proxy-agent": "^8.0.1"
+            },
             "dependencies": {
-                "string-width": "^4.2.0",
-                "strip-ansi": "^6.0.0",
-                "wrap-ansi": "^6.2.0"
+                "agent-base": {
+                    "version": "7.1.0",
+                    "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz",
+                    "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==",
+                    "dev": true,
+                    "requires": {
+                        "debug": "^4.3.4"
+                    }
+                },
+                "http-proxy-agent": {
+                    "version": "7.0.0",
+                    "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz",
+                    "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==",
+                    "dev": true,
+                    "requires": {
+                        "agent-base": "^7.1.0",
+                        "debug": "^4.3.4"
+                    }
+                },
+                "https-proxy-agent": {
+                    "version": "7.0.2",
+                    "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz",
+                    "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==",
+                    "dev": true,
+                    "requires": {
+                        "agent-base": "^7.0.2",
+                        "debug": "4"
+                    }
+                },
+                "socks-proxy-agent": {
+                    "version": "8.0.2",
+                    "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz",
+                    "integrity": "sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==",
+                    "dev": true,
+                    "requires": {
+                        "agent-base": "^7.0.2",
+                        "debug": "^4.3.4",
+                        "socks": "^2.7.1"
+                    }
+                }
             }
         },
-        "node_modules/yargs/node_modules/wrap-ansi": {
-            "version": "6.2.0",
-            "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
-            "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+        "@npmcli/fs": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz",
+            "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==",
             "dev": true,
-            "dependencies": {
-                "ansi-styles": "^4.0.0",
-                "string-width": "^4.1.0",
-                "strip-ansi": "^6.0.0"
-            },
-            "engines": {
-                "node": ">=8"
+            "requires": {
+                "semver": "^7.3.5"
             }
         },
-        "node_modules/yocto-queue": {
-            "version": "0.1.0",
-            "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
-            "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        }
-    },
-    "dependencies": {
-        "@ampproject/remapping": {
-            "version": "2.2.0",
-            "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
-            "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==",
+        "@npmcli/git": {
+            "version": "5.0.3",
+            "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.3.tgz",
+            "integrity": "sha512-UZp9NwK+AynTrKvHn5k3KviW/hA5eENmFsu3iAPe7sWRt0lFUdsY/wXIYjpDFe7cdSNwOIzbObfwgt6eL5/2zw==",
             "dev": true,
             "requires": {
-                "@jridgewell/gen-mapping": "^0.1.0",
-                "@jridgewell/trace-mapping": "^0.3.9"
+                "@npmcli/promise-spawn": "^7.0.0",
+                "lru-cache": "^10.0.1",
+                "npm-pick-manifest": "^9.0.0",
+                "proc-log": "^3.0.0",
+                "promise-inflight": "^1.0.1",
+                "promise-retry": "^2.0.1",
+                "semver": "^7.3.5",
+                "which": "^4.0.0"
+            },
+            "dependencies": {
+                "isexe": {
+                    "version": "3.1.1",
+                    "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
+                    "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
+                    "dev": true
+                },
+                "which": {
+                    "version": "4.0.0",
+                    "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
+                    "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
+                    "dev": true,
+                    "requires": {
+                        "isexe": "^3.1.1"
+                    }
+                }
             }
         },
-        "@babel/code-frame": {
-            "version": "7.18.6",
-            "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
-            "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
+        "@npmcli/installed-package-contents": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.0.2.tgz",
+            "integrity": "sha512-xACzLPhnfD51GKvTOOuNX2/V4G4mz9/1I2MfDoye9kBM3RYe5g2YbscsaGoTlaWqkxeiapBWyseULVKpSVHtKQ==",
             "dev": true,
             "requires": {
-                "@babel/highlight": "^7.18.6"
+                "npm-bundled": "^3.0.0",
+                "npm-normalize-package-bin": "^3.0.0"
             }
         },
-        "@babel/compat-data": {
-            "version": "7.21.0",
-            "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.0.tgz",
-            "integrity": "sha512-gMuZsmsgxk/ENC3O/fRw5QY8A9/uxQbbCEypnLIiYYc/qVJtEV7ouxC3EllIIwNzMqAQee5tanFabWsUOutS7g==",
+        "@npmcli/node-gyp": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz",
+            "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==",
             "dev": true
         },
-        "@babel/core": {
-            "version": "7.21.3",
-            "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.3.tgz",
-            "integrity": "sha512-qIJONzoa/qiHghnm0l1n4i/6IIziDpzqc36FBs4pzMhDUraHqponwJLiAKm1hGLP3OSB/TVNz6rMwVGpwxxySw==",
+        "@npmcli/promise-spawn": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.0.tgz",
+            "integrity": "sha512-wBqcGsMELZna0jDblGd7UXgOby45TQaMWmbFwWX+SEotk4HV6zG2t6rT9siyLhPk4P6YYqgfL1UO8nMWDBVJXQ==",
             "dev": true,
             "requires": {
-                "@ampproject/remapping": "^2.2.0",
-                "@babel/code-frame": "^7.18.6",
-                "@babel/generator": "^7.21.3",
-                "@babel/helper-compilation-targets": "^7.20.7",
-                "@babel/helper-module-transforms": "^7.21.2",
-                "@babel/helpers": "^7.21.0",
-                "@babel/parser": "^7.21.3",
-                "@babel/template": "^7.20.7",
-                "@babel/traverse": "^7.21.3",
-                "@babel/types": "^7.21.3",
-                "convert-source-map": "^1.7.0",
-                "debug": "^4.1.0",
-                "gensync": "^1.0.0-beta.2",
-                "json5": "^2.2.2",
-                "semver": "^6.3.0"
+                "which": "^4.0.0"
+            },
+            "dependencies": {
+                "isexe": {
+                    "version": "3.1.1",
+                    "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
+                    "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
+                    "dev": true
+                },
+                "which": {
+                    "version": "4.0.0",
+                    "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
+                    "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
+                    "dev": true,
+                    "requires": {
+                        "isexe": "^3.1.1"
+                    }
+                }
             }
         },
-        "@babel/generator": {
-            "version": "7.21.3",
-            "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.3.tgz",
-            "integrity": "sha512-QS3iR1GYC/YGUnW7IdggFeN5c1poPUurnGttOV/bZgPGV+izC/D8HnD6DLwod0fsatNyVn1G3EVWMYIF0nHbeA==",
+        "@npmcli/run-script": {
+            "version": "7.0.1",
+            "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.1.tgz",
+            "integrity": "sha512-Od/JMrgkjZ8alyBE0IzeqZDiF1jgMez9Gkc/OYrCkHHiXNwM0wc6s7+h+xM7kYDZkS0tAoOLr9VvygyE5+2F7g==",
             "dev": true,
             "requires": {
-                "@babel/types": "^7.21.3",
-                "@jridgewell/gen-mapping": "^0.3.2",
-                "@jridgewell/trace-mapping": "^0.3.17",
-                "jsesc": "^2.5.1"
+                "@npmcli/node-gyp": "^3.0.0",
+                "@npmcli/promise-spawn": "^7.0.0",
+                "node-gyp": "^9.0.0",
+                "read-package-json-fast": "^3.0.0",
+                "which": "^4.0.0"
             },
             "dependencies": {
-                "@jridgewell/gen-mapping": {
-                    "version": "0.3.2",
-                    "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
-                    "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
+                "isexe": {
+                    "version": "3.1.1",
+                    "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
+                    "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
+                    "dev": true
+                },
+                "which": {
+                    "version": "4.0.0",
+                    "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
+                    "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
                     "dev": true,
                     "requires": {
-                        "@jridgewell/set-array": "^1.0.1",
-                        "@jridgewell/sourcemap-codec": "^1.4.10",
-                        "@jridgewell/trace-mapping": "^0.3.9"
+                        "isexe": "^3.1.1"
                     }
                 }
             }
         },
-        "@babel/helper-compilation-targets": {
-            "version": "7.20.7",
-            "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz",
-            "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==",
+        "@pkgjs/parseargs": {
+            "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": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.1.0.tgz",
+            "integrity": "sha512-89uOo6yh/oxaU8AeOUnVrTdVMcGk9Q1hJa7Hkvalc6G3Z3CupWk4Xe9djSgJm9fMkH69s0P0cVHUoKSOemLdng==",
             "dev": true,
             "requires": {
-                "@babel/compat-data": "^7.20.5",
-                "@babel/helper-validator-option": "^7.18.6",
-                "browserslist": "^4.21.3",
-                "lru-cache": "^5.1.1",
-                "semver": "^6.3.0"
+                "@sigstore/protobuf-specs": "^0.2.1"
             }
         },
-        "@babel/helper-environment-visitor": {
-            "version": "7.18.9",
-            "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz",
-            "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==",
+        "@sigstore/protobuf-specs": {
+            "version": "0.2.1",
+            "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.2.1.tgz",
+            "integrity": "sha512-XTWVxnWJu+c1oCshMLwnKvz8ZQJJDVOlciMfgpJBQbThVjKTCG8dwyhgLngBD2KN0ap9F/gOV8rFDEx8uh7R2A==",
             "dev": true
         },
-        "@babel/helper-function-name": {
-            "version": "7.21.0",
-            "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz",
-            "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==",
+        "@sigstore/sign": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.1.0.tgz",
+            "integrity": "sha512-4VRpfJxs+8eLqzLVrZngVNExVA/zAhVbi4UT4zmtLi4xRd7vz5qie834OgkrGsLlLB1B2nz/3wUxT1XAUBe8gw==",
             "dev": true,
             "requires": {
-                "@babel/template": "^7.20.7",
-                "@babel/types": "^7.21.0"
+                "@sigstore/bundle": "^2.1.0",
+                "@sigstore/protobuf-specs": "^0.2.1",
+                "make-fetch-happen": "^13.0.0"
+            },
+            "dependencies": {
+                "make-fetch-happen": {
+                    "version": "13.0.0",
+                    "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.0.tgz",
+                    "integrity": "sha512-7ThobcL8brtGo9CavByQrQi+23aIfgYU++wg4B87AIS8Rb2ZBt/MEaDqzA00Xwv/jUjAjYkLHjVolYuTLKda2A==",
+                    "dev": true,
+                    "requires": {
+                        "@npmcli/agent": "^2.0.0",
+                        "cacache": "^18.0.0",
+                        "http-cache-semantics": "^4.1.1",
+                        "is-lambda": "^1.0.1",
+                        "minipass": "^7.0.2",
+                        "minipass-fetch": "^3.0.0",
+                        "minipass-flush": "^1.0.5",
+                        "minipass-pipeline": "^1.2.4",
+                        "negotiator": "^0.6.3",
+                        "promise-retry": "^2.0.1",
+                        "ssri": "^10.0.0"
+                    }
+                }
             }
         },
-        "@babel/helper-hoist-variables": {
-            "version": "7.18.6",
-            "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz",
-            "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==",
+        "@sigstore/tuf": {
+            "version": "2.2.0",
+            "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.2.0.tgz",
+            "integrity": "sha512-KKATZ5orWfqd9ZG6MN8PtCIx4eevWSuGRKQvofnWXRpyMyUEpmrzg5M5BrCpjM+NfZ0RbNGOh5tCz/P2uoRqOA==",
             "dev": true,
             "requires": {
-                "@babel/types": "^7.18.6"
+                "@sigstore/protobuf-specs": "^0.2.1",
+                "tuf-js": "^2.1.0"
             }
         },
-        "@babel/helper-module-imports": {
-            "version": "7.18.6",
-            "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
-            "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
+        "@tapjs/after": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/after/-/after-1.1.4.tgz",
+            "integrity": "sha512-TVjrOwpPZt/VfdYc+X4gF/TY06gDHfzP9lfSv7hcxSaUGtvlU0xLH1xsTZS1BKM+EX1qXrCA8RYaLblAniKmaQ==",
             "dev": true,
             "requires": {
-                "@babel/types": "^7.18.6"
+                "is-actual-promise": "^1.0.0"
             }
         },
-        "@babel/helper-module-transforms": {
-            "version": "7.21.2",
-            "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz",
-            "integrity": "sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==",
+        "@tapjs/after-each": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/after-each/-/after-each-1.1.4.tgz",
+            "integrity": "sha512-vcmPQi2wXi2obK2j1nXTDo6EV8uqXONGiaPAPsj+iELr7OB3vBR1FFOQ6GWAFw0Xh8EIIUs8CWyNHn40/kmyUg==",
             "dev": true,
             "requires": {
-                "@babel/helper-environment-visitor": "^7.18.9",
-                "@babel/helper-module-imports": "^7.18.6",
-                "@babel/helper-simple-access": "^7.20.2",
-                "@babel/helper-split-export-declaration": "^7.18.6",
-                "@babel/helper-validator-identifier": "^7.19.1",
-                "@babel/template": "^7.20.7",
-                "@babel/traverse": "^7.21.2",
-                "@babel/types": "^7.21.2"
+                "function-loop": "^4.0.0"
             }
         },
-        "@babel/helper-simple-access": {
-            "version": "7.20.2",
-            "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz",
-            "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==",
+        "@tapjs/asserts": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/asserts/-/asserts-1.1.4.tgz",
+            "integrity": "sha512-5jhbvqJ88agvGEW27l/ucNK7WqQAsCCt6gTBJKdVIL8jOZz5jOVaN/UI6gqUHLO7SYxIl4SOh8N11OYizRSKfA==",
             "dev": true,
             "requires": {
-                "@babel/types": "^7.20.2"
+                "is-actual-promise": "^1.0.0",
+                "tcompare": "6.4.0",
+                "trivial-deferred": "^2.0.0"
             }
         },
-        "@babel/helper-split-export-declaration": {
-            "version": "7.18.6",
-            "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz",
-            "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==",
+        "@tapjs/before": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/before/-/before-1.1.4.tgz",
+            "integrity": "sha512-JnCg39toYCBMZKECL6dqXkpi5p9efxvug/vqMoW7XDpYSJRnRz25EUvTPFd1IE6SwVpJF2xRFL7EKUnxLN3JiQ==",
             "dev": true,
             "requires": {
-                "@babel/types": "^7.18.6"
+                "is-actual-promise": "^1.0.0"
             }
         },
-        "@babel/helper-string-parser": {
-            "version": "7.19.4",
-            "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
-            "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
-            "dev": true
+        "@tapjs/before-each": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/before-each/-/before-each-1.1.4.tgz",
+            "integrity": "sha512-DnwLTOmeifh571kvL3Ef94Ui0OpGzM/oIbjOaL9onHnLTR+cOO8yZALJp6zVg/pq/OzScDY3DQuazunolEVCQQ==",
+            "dev": true,
+            "requires": {
+                "function-loop": "^4.0.0"
+            }
         },
-        "@babel/helper-validator-identifier": {
-            "version": "7.19.1",
-            "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
-            "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
-            "dev": true
+        "@tapjs/config": {
+            "version": "2.4.0",
+            "resolved": "https://registry.npmjs.org/@tapjs/config/-/config-2.4.0.tgz",
+            "integrity": "sha512-iz8n4GFY8FM1kKro4W6kZ3mQvzjddL4j8ta1B08q9ix8K5ysfHnbamjh2syORVRGo/dZNMnKvfXTxFzZ+WIbDg==",
+            "dev": true,
+            "requires": {
+                "chalk": "^5.2.0",
+                "jackspeak": "^2.3.6",
+                "polite-json": "^4.0.1",
+                "walk-up-path": "^3.0.1"
+            },
+            "dependencies": {
+                "chalk": {
+                    "version": "5.3.0",
+                    "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
+                    "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
+                    "dev": true
+                }
+            }
         },
-        "@babel/helper-validator-option": {
-            "version": "7.21.0",
-            "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz",
-            "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==",
-            "dev": true
+        "@tapjs/core": {
+            "version": "1.3.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/core/-/core-1.3.4.tgz",
+            "integrity": "sha512-EcINYx86gDzLeZAsHMckv4Fjd4TdYJ7KduvdhD0Qy4EhROjQnaY9lPQTQxT2uwaEjpWB2Pio3ahtLzNUT2lY1g==",
+            "dev": true,
+            "requires": {
+                "@tapjs/processinfo": "^3.1.2",
+                "@tapjs/stack": "1.2.3",
+                "@tapjs/test": "1.3.4",
+                "async-hook-domain": "^4.0.1",
+                "is-actual-promise": "^1.0.0",
+                "jackspeak": "^2.3.6",
+                "minipass": "^7.0.3",
+                "signal-exit": "4.1",
+                "tap-parser": "15.2.0",
+                "tcompare": "6.4.0",
+                "trivial-deferred": "^2.0.0"
+            }
+        },
+        "@tapjs/error-serdes": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/@tapjs/error-serdes/-/error-serdes-1.1.0.tgz",
+            "integrity": "sha512-RAdsafCQ9fyudLY4EQPhfWQvRNddvSoXKEsZQWZC6G5QfdB/BYnSqaXggK5TD0XZ79Ja0ex3uB+5kBaaeLKtQA==",
+            "dev": true,
+            "requires": {
+                "minipass": "^7.0.3"
+            }
         },
-        "@babel/helpers": {
-            "version": "7.21.0",
-            "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.0.tgz",
-            "integrity": "sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==",
+        "@tapjs/filter": {
+            "version": "1.2.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/filter/-/filter-1.2.4.tgz",
+            "integrity": "sha512-YHIjat67MuuO2SzSg2Hcwwm1Y1UJ1yvD20hyy6MYGrKG8vkaU1hSu4bBheRhJ2IyqJQVgSIM+raNctlN5Bpa/A==",
             "dev": true,
             "requires": {
-                "@babel/template": "^7.20.7",
-                "@babel/traverse": "^7.21.0",
-                "@babel/types": "^7.21.0"
+                "tcompare": "6.4.0",
+                "trivial-deferred": "^2.0.0"
             }
         },
-        "@babel/highlight": {
-            "version": "7.18.6",
-            "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
-            "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
+        "@tapjs/fixture": {
+            "version": "1.2.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/fixture/-/fixture-1.2.4.tgz",
+            "integrity": "sha512-7PHkg7fbKRWThU017qkw92dovreQct3LCArUJ9OdZWFoPYRwYND7CKB3/x7qtnNftBFZbRzf562miH0+TLDDTQ==",
             "dev": true,
             "requires": {
-                "@babel/helper-validator-identifier": "^7.18.6",
-                "chalk": "^2.0.0",
-                "js-tokens": "^4.0.0"
+                "mkdirp": "^3.0.0",
+                "rimraf": "^5.0.5"
             },
             "dependencies": {
-                "ansi-styles": {
-                    "version": "3.2.1",
-                    "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-                    "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+                "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": {
-                        "color-convert": "^1.9.0"
+                        "balanced-match": "^1.0.0"
                     }
                 },
-                "chalk": {
-                    "version": "2.4.2",
-                    "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
-                    "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+                "glob": {
+                    "version": "10.3.10",
+                    "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+                    "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
                     "dev": true,
                     "requires": {
-                        "ansi-styles": "^3.2.1",
-                        "escape-string-regexp": "^1.0.5",
-                        "supports-color": "^5.3.0"
+                        "foreground-child": "^3.1.0",
+                        "jackspeak": "^2.3.5",
+                        "minimatch": "^9.0.1",
+                        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+                        "path-scurry": "^1.10.1"
                     }
                 },
-                "color-convert": {
-                    "version": "1.9.3",
-                    "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
-                    "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+                "minimatch": {
+                    "version": "9.0.3",
+                    "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+                    "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
                     "dev": true,
                     "requires": {
-                        "color-name": "1.1.3"
+                        "brace-expansion": "^2.0.1"
                     }
                 },
-                "color-name": {
-                    "version": "1.1.3",
-                    "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-                    "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
-                    "dev": true
-                },
-                "escape-string-regexp": {
-                    "version": "1.0.5",
-                    "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-                    "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
-                    "dev": true
-                },
-                "has-flag": {
-                    "version": "3.0.0",
-                    "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-                    "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
-                    "dev": true
-                },
-                "supports-color": {
-                    "version": "5.5.0",
-                    "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
-                    "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+                "rimraf": {
+                    "version": "5.0.5",
+                    "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz",
+                    "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==",
                     "dev": true,
                     "requires": {
-                        "has-flag": "^3.0.0"
+                        "glob": "^10.3.7"
                     }
                 }
             }
         },
-        "@babel/parser": {
-            "version": "7.21.3",
-            "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.3.tgz",
-            "integrity": "sha512-lobG0d7aOfQRXh8AyklEAgZGvA4FShxo6xQbUrrT/cNBPUdIDojlokwJsQyCC/eKia7ifqM0yP+2DRZ4WKw2RQ==",
-            "dev": true
+        "@tapjs/intercept": {
+            "version": "1.2.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/intercept/-/intercept-1.2.4.tgz",
+            "integrity": "sha512-aEPwa40DqJPmgnZRbED+hI1x3dSUn4o5rePW6I2ludRle3o1bHSSnucYsjhwNPz0LCpOH9q/UAivJPO66xyTBA==",
+            "dev": true,
+            "requires": {
+                "@tapjs/after": "1.1.4",
+                "@tapjs/stack": "1.2.3"
+            }
         },
-        "@babel/template": {
-            "version": "7.20.7",
-            "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz",
-            "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==",
+        "@tapjs/mock": {
+            "version": "1.2.2",
+            "resolved": "https://registry.npmjs.org/@tapjs/mock/-/mock-1.2.2.tgz",
+            "integrity": "sha512-5SgMRNaHgxjuna5YfVrT/l9bCTV4qePbqxNhwLWiL/l4fHMcF8CB7jMQ2IXsB8/0q9dKSuuxysOeiYSScNQcsA==",
             "dev": true,
             "requires": {
-                "@babel/code-frame": "^7.18.6",
-                "@babel/parser": "^7.20.7",
-                "@babel/types": "^7.20.7"
+                "@tapjs/after": "1.1.4",
+                "@tapjs/stack": "1.2.3",
+                "resolve-import": "^1.4.2",
+                "walk-up-path": "^3.0.1"
             }
         },
-        "@babel/traverse": {
-            "version": "7.21.3",
-            "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.3.tgz",
-            "integrity": "sha512-XLyopNeaTancVitYZe2MlUEvgKb6YVVPXzofHgqHijCImG33b/uTurMS488ht/Hbsb2XK3U2BnSTxKVNGV3nGQ==",
+        "@tapjs/node-serialize": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/node-serialize/-/node-serialize-1.1.4.tgz",
+            "integrity": "sha512-t0x4jC15jae4DviixIqb0v53eXkWdE3KkmKcf/eMGCqN7EL3lRyQRTOtjC3fJRWmdXYCGK/311DpoUfpgzL3sA==",
             "dev": true,
             "requires": {
-                "@babel/code-frame": "^7.18.6",
-                "@babel/generator": "^7.21.3",
-                "@babel/helper-environment-visitor": "^7.18.9",
-                "@babel/helper-function-name": "^7.21.0",
-                "@babel/helper-hoist-variables": "^7.18.6",
-                "@babel/helper-split-export-declaration": "^7.18.6",
-                "@babel/parser": "^7.21.3",
-                "@babel/types": "^7.21.3",
-                "debug": "^4.1.0",
-                "globals": "^11.1.0"
-            },
-            "dependencies": {
-                "globals": {
-                    "version": "11.12.0",
-                    "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
-                    "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
-                    "dev": true
-                }
+                "@tapjs/error-serdes": "1.1.0"
             }
         },
-        "@babel/types": {
-            "version": "7.21.3",
-            "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.3.tgz",
-            "integrity": "sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==",
+        "@tapjs/processinfo": {
+            "version": "3.1.2",
+            "resolved": "https://registry.npmjs.org/@tapjs/processinfo/-/processinfo-3.1.2.tgz",
+            "integrity": "sha512-O3lg1X7zy4sQs+jDYHu+njFQCC5hYJWRmmbLy9UVhgqQKZifS4DYqkoAedK3ixj5NQ1stMNmJGJxbEvJLw/NWA==",
             "dev": true,
             "requires": {
-                "@babel/helper-string-parser": "^7.19.4",
-                "@babel/helper-validator-identifier": "^7.19.1",
-                "to-fast-properties": "^2.0.0"
+                "pirates": "^4.0.5",
+                "process-on-spawn": "^1.0.0",
+                "signal-exit": "^4.0.2",
+                "uuid": "^8.3.2"
             }
         },
-        "@eslint-community/eslint-utils": {
-            "version": "4.4.0",
-            "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
-            "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+        "@tapjs/reporter": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmjs.org/@tapjs/reporter/-/reporter-1.3.0.tgz",
+            "integrity": "sha512-zjXwsZh895zUPM00w9q0W2u/y2ncTz4q/FYu3Jl8Ph0KcSTiGBob01Rj4+Uhhx0N5YwJxb4HOujRtAqhyqs7Gg==",
+            "dev": true,
             "requires": {
-                "eslint-visitor-keys": "^3.3.0"
+                "@tapjs/config": "2.4.0",
+                "@tapjs/test": "1.3.4",
+                "chalk": "^5.2.0",
+                "ink": "^4.4.1",
+                "minipass": "^7.0.3",
+                "ms": "^2.1.3",
+                "patch-console": "^2.0.0",
+                "prismjs": "^1.29.0",
+                "prismjs-terminal": "^1.2.3",
+                "react": "^18.2.0",
+                "string-length": "^6.0.0",
+                "tcompare": "6.4.0"
+            },
+            "dependencies": {
+                "chalk": {
+                    "version": "5.3.0",
+                    "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
+                    "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
+                    "dev": true
+                },
+                "ms": {
+                    "version": "2.1.3",
+                    "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+                    "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+                    "dev": true
+                }
             }
         },
-        "@eslint-community/regexpp": {
-            "version": "4.5.0",
-            "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz",
-            "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ=="
+        "@tapjs/run": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/@tapjs/run/-/run-1.4.0.tgz",
+            "integrity": "sha512-3LNRejFAos8iND30CiQV+RIdaiHBKjsLNq1BZ/nena7lcshKoQCFtiVpKMlqGAStMQgLygjgSo2uHbuSDD0Qww==",
+            "dev": true,
+            "requires": {
+                "@tapjs/after": "1.1.4",
+                "@tapjs/before": "1.1.4",
+                "@tapjs/config": "2.4.0",
+                "@tapjs/processinfo": "^3.1.2",
+                "@tapjs/reporter": "1.3.0",
+                "@tapjs/spawn": "1.1.4",
+                "@tapjs/stdin": "1.1.4",
+                "@tapjs/test": "1.3.4",
+                "c8": "^8.0.1",
+                "chokidar": "^3.5.3",
+                "foreground-child": "^3.1.1",
+                "glob": "^10.3.10",
+                "minipass": "^7.0.3",
+                "mkdirp": "^3.0.1",
+                "opener": "^1.5.2",
+                "pacote": "^17.0.3",
+                "path-scurry": "^1.9.2",
+                "resolve-import": "^1.4.2",
+                "rimraf": "^5.0.5",
+                "semver": "^7.5.4",
+                "signal-exit": "^4.1.0",
+                "tap-yaml": "2.2.0",
+                "tcompare": "6.4.0",
+                "trivial-deferred": "^2.0.0",
+                "ts-node": "npm:@isaacs/ts-node-temp-fork-for-pr-2009@^10.9.1",
+                "which": "^4.0.0"
+            },
+            "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.3.10",
+                    "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+                    "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
+                    "dev": true,
+                    "requires": {
+                        "foreground-child": "^3.1.0",
+                        "jackspeak": "^2.3.5",
+                        "minimatch": "^9.0.1",
+                        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+                        "path-scurry": "^1.10.1"
+                    }
+                },
+                "isexe": {
+                    "version": "3.1.1",
+                    "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
+                    "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
+                    "dev": true
+                },
+                "minimatch": {
+                    "version": "9.0.3",
+                    "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+                    "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+                    "dev": true,
+                    "requires": {
+                        "brace-expansion": "^2.0.1"
+                    }
+                },
+                "rimraf": {
+                    "version": "5.0.5",
+                    "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz",
+                    "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==",
+                    "dev": true,
+                    "requires": {
+                        "glob": "^10.3.7"
+                    }
+                },
+                "which": {
+                    "version": "4.0.0",
+                    "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
+                    "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
+                    "dev": true,
+                    "requires": {
+                        "isexe": "^3.1.1"
+                    }
+                }
+            }
         },
-        "@eslint/eslintrc": {
-            "version": "2.0.2",
-            "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz",
-            "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==",
+        "@tapjs/snapshot": {
+            "version": "1.2.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/snapshot/-/snapshot-1.2.4.tgz",
+            "integrity": "sha512-8pStZczbArIC6+s8TblHTs/Mr5RGApWZA91Eey5UuU5MX3IPUw77MPQpPOoh2zrefa8VZRmHM7IgQq8SKyYjyQ==",
+            "dev": true,
             "requires": {
-                "ajv": "^6.12.4",
-                "debug": "^4.3.2",
-                "espree": "^9.5.1",
-                "globals": "^13.19.0",
-                "ignore": "^5.2.0",
-                "import-fresh": "^3.2.1",
-                "js-yaml": "^4.1.0",
-                "minimatch": "^3.1.2",
-                "strip-json-comments": "^3.1.1"
+                "is-actual-promise": "^1.0.0",
+                "tcompare": "6.4.0",
+                "trivial-deferred": "^2.0.0"
             }
         },
-        "@eslint/js": {
-            "version": "8.37.0",
-            "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.37.0.tgz",
-            "integrity": "sha512-x5vzdtOOGgFVDCUs81QRB2+liax8rFg3+7hqM+QhBG0/G3F1ZsoYl97UrqgHgQ9KKT7G6c4V+aTUCgu/n22v1A=="
+        "@tapjs/spawn": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/spawn/-/spawn-1.1.4.tgz",
+            "integrity": "sha512-H3/VBi/Zfnb53PbpNmT/OYhIdqk8k6pGnM+WNLB8KBzwLa23q75P0jSYAEhzX3sZO+JIiaHACj/SxvttFapDtg==",
+            "dev": true,
+            "requires": {}
         },
-        "@humanwhocodes/config-array": {
-            "version": "0.11.8",
-            "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
-            "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
+        "@tapjs/stack": {
+            "version": "1.2.3",
+            "resolved": "https://registry.npmjs.org/@tapjs/stack/-/stack-1.2.3.tgz",
+            "integrity": "sha512-LY7Rxse2QY+DczTCoqOA4rxjqhnCgXYZeynrhzOsiut6IVnDWnqjUvZMq1XYnk5G69lhgG5lTDHmZrKP33BKgg==",
+            "dev": true,
             "requires": {
-                "@humanwhocodes/object-schema": "^1.2.1",
-                "debug": "^4.1.1",
-                "minimatch": "^3.0.5"
+                "tcompare": "6.4.0",
+                "trivial-deferred": "^2.0.0"
             }
         },
-        "@humanwhocodes/module-importer": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
-            "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="
-        },
-        "@humanwhocodes/object-schema": {
-            "version": "1.2.1",
-            "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
-            "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA=="
+        "@tapjs/stdin": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/stdin/-/stdin-1.1.4.tgz",
+            "integrity": "sha512-yQzeiWaWRFd5jXVy3F0Q4inQqVmEGynFfWz2cbQYJFm/CNCcKFM1t4uIRRqtNdfJwSrr19m8Lq0qqfT7pHV/yg==",
+            "dev": true,
+            "requires": {}
         },
-        "@istanbuljs/load-nyc-config": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
-            "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==",
+        "@tapjs/test": {
+            "version": "1.3.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/test/-/test-1.3.4.tgz",
+            "integrity": "sha512-ud2T10OhxdQw4f7Wo4G+5/Vyw5JYgfb5bDmKo0B3xmMgVvIFpUS/4V2Zq+59DZGXmEgjO0KPhb8NvOpOHAy/fg==",
             "dev": true,
             "requires": {
-                "camelcase": "^5.3.1",
-                "find-up": "^4.1.0",
-                "get-package-type": "^0.1.0",
-                "js-yaml": "^3.13.1",
-                "resolve-from": "^5.0.0"
+                "@tapjs/after": "1.1.4",
+                "@tapjs/after-each": "1.1.4",
+                "@tapjs/asserts": "1.1.4",
+                "@tapjs/before": "1.1.4",
+                "@tapjs/before-each": "1.1.4",
+                "@tapjs/filter": "1.2.4",
+                "@tapjs/fixture": "1.2.4",
+                "@tapjs/intercept": "1.2.4",
+                "@tapjs/mock": "1.2.2",
+                "@tapjs/node-serialize": "1.1.4",
+                "@tapjs/snapshot": "1.2.4",
+                "@tapjs/spawn": "1.1.4",
+                "@tapjs/stdin": "1.1.4",
+                "@tapjs/typescript": "1.2.4",
+                "@tapjs/worker": "1.1.4",
+                "glob": "^10.3.10",
+                "jackspeak": "^2.3.6",
+                "mkdirp": "^3.0.0",
+                "resolve-import": "^1.4.1",
+                "rimraf": "^5.0.5",
+                "sync-content": "^1.0.1",
+                "tap-parser": "15.2.0",
+                "ts-node": "npm:@isaacs/ts-node-temp-fork-for-pr-2009@^10.9.1",
+                "tshy": "^1.2.2",
+                "typescript": "5.2"
             },
             "dependencies": {
-                "argparse": {
-                    "version": "1.0.10",
-                    "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
-                    "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+                "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": {
-                        "sprintf-js": "~1.0.2"
+                        "balanced-match": "^1.0.0"
                     }
                 },
-                "js-yaml": {
-                    "version": "3.14.1",
-                    "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
-                    "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+                "glob": {
+                    "version": "10.3.10",
+                    "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+                    "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
                     "dev": true,
                     "requires": {
-                        "argparse": "^1.0.7",
-                        "esprima": "^4.0.0"
+                        "foreground-child": "^3.1.0",
+                        "jackspeak": "^2.3.5",
+                        "minimatch": "^9.0.1",
+                        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+                        "path-scurry": "^1.10.1"
                     }
                 },
-                "resolve-from": {
-                    "version": "5.0.0",
-                    "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
-                    "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
-                    "dev": true
+                "minimatch": {
+                    "version": "9.0.3",
+                    "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+                    "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+                    "dev": true,
+                    "requires": {
+                        "brace-expansion": "^2.0.1"
+                    }
+                },
+                "rimraf": {
+                    "version": "5.0.5",
+                    "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz",
+                    "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==",
+                    "dev": true,
+                    "requires": {
+                        "glob": "^10.3.7"
+                    }
                 }
             }
         },
-        "@istanbuljs/schema": {
-            "version": "0.1.3",
-            "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
-            "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
-            "dev": true
-        },
-        "@jridgewell/gen-mapping": {
-            "version": "0.1.1",
-            "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
-            "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==",
+        "@tapjs/typescript": {
+            "version": "1.2.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/typescript/-/typescript-1.2.4.tgz",
+            "integrity": "sha512-exhSckFlKLr0RFHKYBJb3N6CftoafH5GwNeAWN0yua+FmzwDleGvgKThW3l/xeOF7BeCq/m4zu9HWrwjkPaDhQ==",
             "dev": true,
             "requires": {
-                "@jridgewell/set-array": "^1.0.0",
-                "@jridgewell/sourcemap-codec": "^1.4.10"
+                "ts-node": "npm:@isaacs/ts-node-temp-fork-for-pr-2009@^10.9.1"
             }
         },
-        "@jridgewell/resolve-uri": {
-            "version": "3.1.0",
-            "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
-            "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
+        "@tapjs/worker": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/@tapjs/worker/-/worker-1.1.4.tgz",
+            "integrity": "sha512-HcaafOWghXpMtLaCk8BOIMQcphZU2Gi0OSUb6vzgxKQ4iQxTsBkJSnZ1+4F8Qed9EWZ9n6zaggjy7/fDLVdJRg==",
+            "dev": true,
+            "requires": {}
+        },
+        "@tootallnate/once": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
+            "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
             "dev": true
         },
-        "@jridgewell/set-array": {
-            "version": "1.1.2",
-            "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
-            "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+        "@tsconfig/node14": {
+            "version": "14.1.0",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-14.1.0.tgz",
+            "integrity": "sha512-VmsCG04YR58ciHBeJKBDNMWWfYbyP8FekWVuTlpstaUPlat1D0x/tXzkWP7yCMU0eSz9V4OZU0LBWTFJ3xZf6w==",
             "dev": true
         },
-        "@jridgewell/sourcemap-codec": {
-            "version": "1.4.14",
-            "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
-            "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
+        "@tsconfig/node16": {
+            "version": "16.1.1",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-16.1.1.tgz",
+            "integrity": "sha512-+pio93ejHN4nINX4pXqfnR/fPLRtJBaT4ORaa5RH0Oc1zoYmo2B2koG+M328CQhHKn1Wj6FcOxCDFXAot9NhvA==",
             "dev": true
         },
-        "@jridgewell/trace-mapping": {
-            "version": "0.3.17",
-            "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz",
-            "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==",
+        "@tsconfig/node18": {
+            "version": "18.2.2",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-18.2.2.tgz",
+            "integrity": "sha512-d6McJeGsuoRlwWZmVIeE8CUA27lu6jLjvv1JzqmpsytOYYbVi1tHZEnwCNVOXnj4pyLvneZlFlpXUK+X9wBWyw==",
+            "dev": true
+        },
+        "@tsconfig/node20": {
+            "version": "20.1.2",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.2.tgz",
+            "integrity": "sha512-madaWq2k+LYMEhmcp0fs+OGaLFk0OenpHa4gmI4VEmCKX4PJntQ6fnnGADVFrVkBj0wIdAlQnK/MrlYTHsa1gQ==",
+            "dev": true
+        },
+        "@tufjs/canonical-json": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
+            "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==",
+            "dev": true
+        },
+        "@tufjs/models": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-2.0.0.tgz",
+            "integrity": "sha512-c8nj8BaOExmZKO2DXhDfegyhSGcG9E/mPN3U13L+/PsoWm1uaGiHHjxqSHQiasDBQwDA3aHuw9+9spYAP1qvvg==",
             "dev": true,
             "requires": {
-                "@jridgewell/resolve-uri": "3.1.0",
-                "@jridgewell/sourcemap-codec": "1.4.14"
+                "@tufjs/canonical-json": "2.0.0",
+                "minimatch": "^9.0.3"
+            },
+            "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"
+                    }
+                },
+                "minimatch": {
+                    "version": "9.0.3",
+                    "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+                    "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+                    "dev": true,
+                    "requires": {
+                        "brace-expansion": "^2.0.1"
+                    }
+                }
             }
         },
-        "@nodelib/fs.scandir": {
-            "version": "2.1.5",
-            "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
-            "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
-            "requires": {
-                "@nodelib/fs.stat": "2.0.5",
-                "run-parallel": "^1.1.9"
-            }
+        "@types/istanbul-lib-coverage": {
+            "version": "2.0.4",
+            "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
+            "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==",
+            "dev": true
         },
-        "@nodelib/fs.stat": {
-            "version": "2.0.5",
-            "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
-            "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="
+        "@types/node": {
+            "version": "20.8.0",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.0.tgz",
+            "integrity": "sha512-LzcWltT83s1bthcvjBmiBvGJiiUe84NWRHkw+ZV6Fr41z2FbIzvc815dk2nQ3RAKMuN2fkenM/z3Xv2QzEpYxQ==",
+            "dev": true,
+            "peer": true
         },
-        "@nodelib/fs.walk": {
-            "version": "1.2.8",
-            "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
-            "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
-            "requires": {
-                "@nodelib/fs.scandir": "2.1.5",
-                "fastq": "^1.6.0"
-            }
+        "abbrev": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+            "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+            "dev": true
         },
         "acorn": {
             "version": "8.8.2",
@@ -5442,6 +6671,30 @@
             "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
             "requires": {}
         },
+        "acorn-walk": {
+            "version": "8.2.0",
+            "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
+            "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
+            "dev": true
+        },
+        "agent-base": {
+            "version": "6.0.2",
+            "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+            "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+            "dev": true,
+            "requires": {
+                "debug": "4"
+            }
+        },
+        "agentkeepalive": {
+            "version": "4.5.0",
+            "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz",
+            "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==",
+            "dev": true,
+            "requires": {
+                "humanize-ms": "^1.2.1"
+            }
+        },
         "aggregate-error": {
             "version": "3.1.0",
             "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
@@ -5450,6 +6703,14 @@
             "requires": {
                 "clean-stack": "^2.0.0",
                 "indent-string": "^4.0.0"
+            },
+            "dependencies": {
+                "indent-string": {
+                    "version": "4.0.0",
+                    "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+                    "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+                    "dev": true
+                }
             }
         },
         "ajv": {
@@ -5463,6 +6724,23 @@
                 "uri-js": "^4.2.2"
             }
         },
+        "ansi-escapes": {
+            "version": "6.2.0",
+            "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz",
+            "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==",
+            "dev": true,
+            "requires": {
+                "type-fest": "^3.0.0"
+            },
+            "dependencies": {
+                "type-fest": {
+                    "version": "3.13.1",
+                    "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz",
+                    "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==",
+                    "dev": true
+                }
+            }
+        },
         "ansi-regex": {
             "version": "5.0.1",
             "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -5486,19 +6764,26 @@
                 "picomatch": "^2.0.4"
             }
         },
-        "append-transform": {
+        "aproba": {
             "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz",
-            "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==",
+            "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
+            "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
+            "dev": true
+        },
+        "are-we-there-yet": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz",
+            "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==",
             "dev": true,
             "requires": {
-                "default-require-extensions": "^3.0.0"
+                "delegates": "^1.0.0",
+                "readable-stream": "^3.6.0"
             }
         },
-        "archy": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz",
-            "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==",
+        "arg": {
+            "version": "4.1.3",
+            "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+            "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
             "dev": true
         },
         "argparse": {
@@ -5507,9 +6792,15 @@
             "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
         },
         "async-hook-domain": {
-            "version": "2.0.4",
-            "resolved": "https://registry.npmjs.org/async-hook-domain/-/async-hook-domain-2.0.4.tgz",
-            "integrity": "sha512-14LjCmlK1PK8eDtTezR6WX8TMaYNIzBIsd2D1sGoGjgx0BuNMMoSdk7i/drlbtamy0AWv9yv2tkB+ASdmeqFIw==",
+            "version": "4.0.1",
+            "resolved": "https://registry.npmjs.org/async-hook-domain/-/async-hook-domain-4.0.1.tgz",
+            "integrity": "sha512-bSktexGodAjfHWIrSrrqxqWzf1hWBZBpmPNZv+TYUMyWa2eoefFc6q6H1+KtdHYSz35lrhWdmXt/XK9wNEZvww==",
+            "dev": true
+        },
+        "auto-bind": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz",
+            "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==",
             "dev": true
         },
         "balanced-match": {
@@ -5523,12 +6814,6 @@
             "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
             "dev": true
         },
-        "bind-obj-methods": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/bind-obj-methods/-/bind-obj-methods-3.0.0.tgz",
-            "integrity": "sha512-nLEaaz3/sEzNSyPWRsN9HNsqwk1AUyECtGj+XwGdIi3xABnEqecvXtIJ0wehQXuuER5uZ/5fTs2usONgYjG+iw==",
-            "dev": true
-        },
         "brace-expansion": {
             "version": "1.1.11",
             "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -5547,34 +6832,104 @@
                 "fill-range": "^7.0.1"
             }
         },
-        "browserslist": {
-            "version": "4.21.5",
-            "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz",
-            "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==",
+        "builtins": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz",
+            "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==",
             "dev": true,
             "requires": {
-                "caniuse-lite": "^1.0.30001449",
-                "electron-to-chromium": "^1.4.284",
-                "node-releases": "^2.0.8",
-                "update-browserslist-db": "^1.0.10"
+                "semver": "^7.0.0"
             }
         },
-        "buffer-from": {
-            "version": "1.1.2",
-            "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
-            "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
-            "dev": true
+        "c8": {
+            "version": "8.0.1",
+            "resolved": "https://registry.npmjs.org/c8/-/c8-8.0.1.tgz",
+            "integrity": "sha512-EINpopxZNH1mETuI0DzRA4MZpAUH+IFiRhnmFD3vFr3vdrgxqi3VfE3KL0AIL+zDq8rC9bZqwM/VDmmoe04y7w==",
+            "dev": true,
+            "requires": {
+                "@bcoe/v8-coverage": "^0.2.3",
+                "@istanbuljs/schema": "^0.1.3",
+                "find-up": "^5.0.0",
+                "foreground-child": "^2.0.0",
+                "istanbul-lib-coverage": "^3.2.0",
+                "istanbul-lib-report": "^3.0.1",
+                "istanbul-reports": "^3.1.6",
+                "rimraf": "^3.0.2",
+                "test-exclude": "^6.0.0",
+                "v8-to-istanbul": "^9.0.0",
+                "yargs": "^17.7.2",
+                "yargs-parser": "^21.1.1"
+            },
+            "dependencies": {
+                "foreground-child": {
+                    "version": "2.0.0",
+                    "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz",
+                    "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==",
+                    "dev": true,
+                    "requires": {
+                        "cross-spawn": "^7.0.0",
+                        "signal-exit": "^3.0.2"
+                    }
+                },
+                "signal-exit": {
+                    "version": "3.0.7",
+                    "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+                    "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+                    "dev": true
+                }
+            }
         },
-        "caching-transform": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz",
-            "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==",
+        "cacache": {
+            "version": "18.0.0",
+            "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.0.tgz",
+            "integrity": "sha512-I7mVOPl3PUCeRub1U8YoGz2Lqv9WOBpobZ8RyWFXmReuILz+3OAyTa5oH3QPdtKZD7N0Yk00aLfzn0qvp8dZ1w==",
             "dev": true,
             "requires": {
-                "hasha": "^5.0.0",
-                "make-dir": "^3.0.0",
-                "package-hash": "^4.0.0",
-                "write-file-atomic": "^3.0.0"
+                "@npmcli/fs": "^3.1.0",
+                "fs-minipass": "^3.0.0",
+                "glob": "^10.2.2",
+                "lru-cache": "^10.0.1",
+                "minipass": "^7.0.3",
+                "minipass-collect": "^1.0.2",
+                "minipass-flush": "^1.0.5",
+                "minipass-pipeline": "^1.2.4",
+                "p-map": "^4.0.0",
+                "ssri": "^10.0.0",
+                "tar": "^6.1.11",
+                "unique-filename": "^3.0.0"
+            },
+            "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.3.10",
+                    "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+                    "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
+                    "dev": true,
+                    "requires": {
+                        "foreground-child": "^3.1.0",
+                        "jackspeak": "^2.3.5",
+                        "minimatch": "^9.0.1",
+                        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+                        "path-scurry": "^1.10.1"
+                    }
+                },
+                "minimatch": {
+                    "version": "9.0.3",
+                    "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+                    "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+                    "dev": true,
+                    "requires": {
+                        "brace-expansion": "^2.0.1"
+                    }
+                }
             }
         },
         "callsites": {
@@ -5582,18 +6937,6 @@
             "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
             "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="
         },
-        "camelcase": {
-            "version": "5.3.1",
-            "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
-            "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
-            "dev": true
-        },
-        "caniuse-lite": {
-            "version": "1.0.30001469",
-            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001469.tgz",
-            "integrity": "sha512-Rcp7221ScNqQPP3W+lVOYDyjdR6dC+neEQCttoNr5bAyz54AboB4iwpnWgyi8P4YUsPybVzT4LgWiBbI3drL4g==",
-            "dev": true
-        },
         "chalk": {
             "version": "4.1.2",
             "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -5630,26 +6973,126 @@
                 }
             }
         },
+        "chownr": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+            "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+            "dev": true
+        },
         "chroma-js": {
             "version": "2.4.2",
             "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz",
             "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A=="
         },
+        "ci-info": {
+            "version": "3.8.0",
+            "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
+            "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
+            "dev": true
+        },
         "clean-stack": {
             "version": "2.2.0",
             "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
             "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
             "dev": true
         },
+        "cli-boxes": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz",
+            "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==",
+            "dev": true
+        },
+        "cli-cursor": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
+            "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==",
+            "dev": true,
+            "requires": {
+                "restore-cursor": "^4.0.0"
+            }
+        },
+        "cli-truncate": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz",
+            "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==",
+            "dev": true,
+            "requires": {
+                "slice-ansi": "^5.0.0",
+                "string-width": "^5.0.0"
+            },
+            "dependencies": {
+                "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
+                },
+                "slice-ansi": {
+                    "version": "5.0.0",
+                    "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
+                    "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
+                    "dev": true,
+                    "requires": {
+                        "ansi-styles": "^6.0.0",
+                        "is-fullwidth-code-point": "^4.0.0"
+                    }
+                }
+            }
+        },
         "cliui": {
-            "version": "7.0.4",
-            "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
-            "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+            "version": "8.0.1",
+            "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+            "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
             "dev": true,
             "requires": {
                 "string-width": "^4.2.0",
-                "strip-ansi": "^6.0.0",
+                "strip-ansi": "^6.0.1",
                 "wrap-ansi": "^7.0.0"
+            },
+            "dependencies": {
+                "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
+                },
+                "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
+                },
+                "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",
+                        "strip-ansi": "^6.0.1"
+                    }
+                },
+                "wrap-ansi": {
+                    "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,
+                    "requires": {
+                        "ansi-styles": "^4.0.0",
+                        "string-width": "^4.1.0",
+                        "strip-ansi": "^6.0.0"
+                    }
+                }
+            }
+        },
+        "code-excerpt": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz",
+            "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==",
+            "dev": true,
+            "requires": {
+                "convert-to-spaces": "^2.0.1"
             }
         },
         "color-convert": {
@@ -5676,23 +7119,29 @@
             "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz",
             "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w=="
         },
-        "commondir": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
-            "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
-            "dev": true
-        },
         "concat-map": {
             "version": "0.0.1",
             "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
             "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
         },
+        "console-control-strings": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+            "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
+            "dev": true
+        },
         "convert-source-map": {
             "version": "1.9.0",
             "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
             "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
             "dev": true
         },
+        "convert-to-spaces": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz",
+            "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==",
+            "dev": true
+        },
         "cross-spawn": {
             "version": "7.0.3",
             "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -5711,25 +7160,16 @@
                 "ms": "2.1.2"
             }
         },
-        "decamelize": {
-            "version": "1.2.0",
-            "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
-            "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
-            "dev": true
-        },
         "deep-is": {
             "version": "0.1.4",
             "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
             "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
         },
-        "default-require-extensions": {
-            "version": "3.0.1",
-            "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz",
-            "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==",
-            "dev": true,
-            "requires": {
-                "strip-bom": "^4.0.0"
-            }
+        "delegates": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+            "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
+            "dev": true
         },
         "diff": {
             "version": "4.0.2",
@@ -5745,22 +7185,38 @@
                 "esutils": "^2.0.2"
             }
         },
-        "electron-to-chromium": {
-            "version": "1.4.340",
-            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.340.tgz",
-            "integrity": "sha512-zx8hqumOqltKsv/MF50yvdAlPF9S/4PXbyfzJS6ZGhbddGkRegdwImmfSVqCkEziYzrIGZ/TlrzBND4FysfkDg==",
+        "eastasianwidth": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+            "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
             "dev": true
         },
         "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==",
+            "version": "9.2.2",
+            "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+            "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
             "dev": true
         },
-        "es6-error": {
-            "version": "4.1.1",
-            "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
-            "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
+        "encoding": {
+            "version": "0.1.13",
+            "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
+            "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
+            "dev": true,
+            "optional": true,
+            "requires": {
+                "iconv-lite": "^0.6.2"
+            }
+        },
+        "env-paths": {
+            "version": "2.2.1",
+            "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+            "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+            "dev": true
+        },
+        "err-code": {
+            "version": "2.0.3",
+            "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
+            "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
             "dev": true
         },
         "escalade": {
@@ -5819,41 +7275,6 @@
                 "strip-ansi": "^6.0.1",
                 "strip-json-comments": "^3.1.0",
                 "text-table": "^0.2.0"
-            },
-            "dependencies": {
-                "find-up": {
-                    "version": "5.0.0",
-                    "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
-                    "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
-                    "requires": {
-                        "locate-path": "^6.0.0",
-                        "path-exists": "^4.0.0"
-                    }
-                },
-                "locate-path": {
-                    "version": "6.0.0",
-                    "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
-                    "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
-                    "requires": {
-                        "p-locate": "^5.0.0"
-                    }
-                },
-                "p-limit": {
-                    "version": "3.1.0",
-                    "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
-                    "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
-                    "requires": {
-                        "yocto-queue": "^0.1.0"
-                    }
-                },
-                "p-locate": {
-                    "version": "5.0.0",
-                    "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
-                    "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
-                    "requires": {
-                        "p-limit": "^3.0.2"
-                    }
-                }
             }
         },
         "eslint-scope": {
@@ -5880,12 +7301,6 @@
                 "eslint-visitor-keys": "^3.4.0"
             }
         },
-        "esprima": {
-            "version": "4.0.1",
-            "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
-            "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
-            "dev": true
-        },
         "esquery": {
             "version": "1.5.0",
             "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
@@ -5913,9 +7328,15 @@
             "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
         },
         "events-to-array": {
-            "version": "1.1.2",
-            "resolved": "https://registry.npmjs.org/events-to-array/-/events-to-array-1.1.2.tgz",
-            "integrity": "sha512-inRWzRY7nG+aXZxBzEqYKB3HPgwflZRopAjDCHv0whhRx+MTUr1ei0ICZUypdyE0HRm4L2d5VEcIqLD6yl+BFA==",
+            "version": "2.0.3",
+            "resolved": "https://registry.npmjs.org/events-to-array/-/events-to-array-2.0.3.tgz",
+            "integrity": "sha512-f/qE2gImHRa4Cp2y1stEOSgw8wTFyUdVJX7G//bMwbaV9JqISFxg99NbmVQeP7YLnDUZ2un851jlaDrlpmGehQ==",
+            "dev": true
+        },
+        "exponential-backoff": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz",
+            "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==",
             "dev": true
         },
         "fast-deep-equal": {
@@ -5958,33 +7379,15 @@
                 "to-regex-range": "^5.0.1"
             }
         },
-        "find-cache-dir": {
-            "version": "3.3.2",
-            "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
-            "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
-            "dev": true,
-            "requires": {
-                "commondir": "^1.0.1",
-                "make-dir": "^3.0.2",
-                "pkg-dir": "^4.1.0"
-            }
-        },
         "find-up": {
-            "version": "4.1.0",
-            "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
-            "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
-            "dev": true,
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+            "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
             "requires": {
-                "locate-path": "^5.0.0",
+                "locate-path": "^6.0.0",
                 "path-exists": "^4.0.0"
             }
         },
-        "findit": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/findit/-/findit-2.0.0.tgz",
-            "integrity": "sha512-ENZS237/Hr8bjczn5eKuBohLgaD0JyUd0arxretR1f9RO46vZHA1b2y0VorgGV3WaOT3c+78P8h7v4JGJ1i/rg==",
-            "dev": true
-        },
         "flat-cache": {
             "version": "3.0.4",
             "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
@@ -6000,13 +7403,13 @@
             "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg=="
         },
         "foreground-child": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz",
-            "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==",
+            "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": "^3.0.2"
+                "signal-exit": "^4.0.1"
             }
         },
         "fromentries": {
@@ -6015,11 +7418,14 @@
             "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==",
             "dev": true
         },
-        "fs-exists-cached": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/fs-exists-cached/-/fs-exists-cached-1.0.0.tgz",
-            "integrity": "sha512-kSxoARUDn4F2RPXX48UXnaFKwVU7Ivd/6qpzZL29MCDmr9sTvybv4gFCp+qaI4fM9m0z9fgz/yJvi56GAz+BZg==",
-            "dev": true
+        "fs-minipass": {
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz",
+            "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==",
+            "dev": true,
+            "requires": {
+                "minipass": "^7.0.3"
+            }
         },
         "fs.realpath": {
             "version": "1.0.0",
@@ -6033,30 +7439,71 @@
             "dev": true,
             "optional": true
         },
-        "function-loop": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmjs.org/function-loop/-/function-loop-2.0.1.tgz",
-            "integrity": "sha512-ktIR+O6i/4h+j/ZhZJNdzeI4i9lEPeEK6UPR2EVyTVBqOwcU3Za9xYKLH64ZR9HmcROyRrOkizNyjjtWJzDDkQ==",
+        "function-bind": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+            "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
             "dev": true
         },
-        "gensync": {
-            "version": "1.0.0-beta.2",
-            "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
-            "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+        "function-loop": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/function-loop/-/function-loop-4.0.0.tgz",
+            "integrity": "sha512-f34iQBedYF3XcI93uewZZOnyscDragxgTK/eTvVB74k3fCD0ZorOi5BV9GS4M8rz/JoNi0Kl3qX5Y9MH3S/CLQ==",
             "dev": true
         },
+        "gauge": {
+            "version": "4.0.4",
+            "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz",
+            "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==",
+            "dev": true,
+            "requires": {
+                "aproba": "^1.0.3 || ^2.0.0",
+                "color-support": "^1.1.3",
+                "console-control-strings": "^1.1.0",
+                "has-unicode": "^2.0.1",
+                "signal-exit": "^3.0.7",
+                "string-width": "^4.2.3",
+                "strip-ansi": "^6.0.1",
+                "wide-align": "^1.1.5"
+            },
+            "dependencies": {
+                "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
+                },
+                "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
+                },
+                "signal-exit": {
+                    "version": "3.0.7",
+                    "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+                    "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+                    "dev": true
+                },
+                "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",
+                        "strip-ansi": "^6.0.1"
+                    }
+                }
+            }
+        },
         "get-caller-file": {
             "version": "2.0.5",
             "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
             "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
             "dev": true
         },
-        "get-package-type": {
-            "version": "0.1.0",
-            "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
-            "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
-            "dev": true
-        },
         "glob": {
             "version": "7.2.3",
             "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -6097,45 +7544,134 @@
             "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
             "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ=="
         },
+        "has": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+            "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+            "dev": true,
+            "requires": {
+                "function-bind": "^1.1.1"
+            }
+        },
         "has-flag": {
             "version": "4.0.0",
             "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
             "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
         },
-        "hasha": {
-            "version": "5.2.2",
-            "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz",
-            "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==",
-            "dev": true,
-            "requires": {
-                "is-stream": "^2.0.0",
-                "type-fest": "^0.8.0"
-            },
-            "dependencies": {
-                "type-fest": {
-                    "version": "0.8.1",
-                    "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
-                    "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
-                    "dev": true
-                }
-            }
+        "has-unicode": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+            "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
+            "dev": true
         },
         "he": {
             "version": "1.2.0",
             "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
             "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
         },
+        "hosted-git-info": {
+            "version": "7.0.1",
+            "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz",
+            "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==",
+            "dev": true,
+            "requires": {
+                "lru-cache": "^10.0.1"
+            }
+        },
         "html-escaper": {
             "version": "2.0.2",
             "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
             "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
             "dev": true
         },
+        "http-cache-semantics": {
+            "version": "4.1.1",
+            "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
+            "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
+            "dev": true
+        },
+        "http-proxy-agent": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+            "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+            "dev": true,
+            "requires": {
+                "@tootallnate/once": "2",
+                "agent-base": "6",
+                "debug": "4"
+            }
+        },
+        "https-proxy-agent": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+            "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+            "dev": true,
+            "requires": {
+                "agent-base": "6",
+                "debug": "4"
+            }
+        },
+        "humanize-ms": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
+            "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
+            "dev": true,
+            "requires": {
+                "ms": "^2.0.0"
+            }
+        },
+        "iconv-lite": {
+            "version": "0.6.3",
+            "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+            "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+            "dev": true,
+            "optional": true,
+            "requires": {
+                "safer-buffer": ">= 2.1.2 < 3.0.0"
+            }
+        },
         "ignore": {
             "version": "5.2.4",
             "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
             "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ=="
         },
+        "ignore-walk": {
+            "version": "6.0.3",
+            "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.3.tgz",
+            "integrity": "sha512-C7FfFoTA+bI10qfeydT8aZbvr91vAEU+2W5BZUlzPec47oNb07SsOfwYrtxuvOYdUApPP/Qlh4DtAO51Ekk2QA==",
+            "dev": true,
+            "requires": {
+                "minimatch": "^9.0.0"
+            },
+            "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"
+                    }
+                },
+                "minimatch": {
+                    "version": "9.0.3",
+                    "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+                    "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+                    "dev": true,
+                    "requires": {
+                        "brace-expansion": "^2.0.1"
+                    }
+                }
+            }
+        },
+        "image-size": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.2.tgz",
+            "integrity": "sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg==",
+            "requires": {
+                "queue": "6.0.2"
+            }
+        },
         "import-fresh": {
             "version": "3.3.0",
             "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -6151,9 +7687,9 @@
             "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="
         },
         "indent-string": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
-            "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz",
+            "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==",
             "dev": true
         },
         "inflight": {
@@ -6170,6 +7706,71 @@
             "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
             "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
         },
+        "ink": {
+            "version": "4.4.1",
+            "resolved": "https://registry.npmjs.org/ink/-/ink-4.4.1.tgz",
+            "integrity": "sha512-rXckvqPBB0Krifk5rn/5LvQGmyXwCUpBfmTwbkQNBY9JY8RSl3b8OftBNEYxg4+SWUhEKcPifgope28uL9inlA==",
+            "dev": true,
+            "requires": {
+                "@alcalzone/ansi-tokenize": "^0.1.3",
+                "ansi-escapes": "^6.0.0",
+                "auto-bind": "^5.0.1",
+                "chalk": "^5.2.0",
+                "cli-boxes": "^3.0.0",
+                "cli-cursor": "^4.0.0",
+                "cli-truncate": "^3.1.0",
+                "code-excerpt": "^4.0.0",
+                "indent-string": "^5.0.0",
+                "is-ci": "^3.0.1",
+                "is-lower-case": "^2.0.2",
+                "is-upper-case": "^2.0.2",
+                "lodash": "^4.17.21",
+                "patch-console": "^2.0.0",
+                "react-reconciler": "^0.29.0",
+                "scheduler": "^0.23.0",
+                "signal-exit": "^3.0.7",
+                "slice-ansi": "^6.0.0",
+                "stack-utils": "^2.0.6",
+                "string-width": "^5.1.2",
+                "type-fest": "^0.12.0",
+                "widest-line": "^4.0.1",
+                "wrap-ansi": "^8.1.0",
+                "ws": "^8.12.0",
+                "yoga-wasm-web": "~0.3.3"
+            },
+            "dependencies": {
+                "chalk": {
+                    "version": "5.3.0",
+                    "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
+                    "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
+                    "dev": true
+                },
+                "signal-exit": {
+                    "version": "3.0.7",
+                    "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+                    "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+                    "dev": true
+                },
+                "type-fest": {
+                    "version": "0.12.0",
+                    "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.12.0.tgz",
+                    "integrity": "sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg==",
+                    "dev": true
+                }
+            }
+        },
+        "ip": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
+            "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==",
+            "dev": true
+        },
+        "is-actual-promise": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/is-actual-promise/-/is-actual-promise-1.0.0.tgz",
+            "integrity": "sha512-DWSmKTiEoY3Y9LGHG9TVnFgydCCu+3fLJi4rv3fpi0gL/lKoILekh/oF/nO3/Lq1l5Rqo+tQt5TWzxMmYIhWyg==",
+            "dev": true
+        },
         "is-binary-path": {
             "version": "2.1.0",
             "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -6179,15 +7780,33 @@
                 "binary-extensions": "^2.0.0"
             }
         },
+        "is-ci": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
+            "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==",
+            "dev": true,
+            "requires": {
+                "ci-info": "^3.2.0"
+            }
+        },
+        "is-core-module": {
+            "version": "2.13.0",
+            "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz",
+            "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==",
+            "dev": true,
+            "requires": {
+                "has": "^1.0.3"
+            }
+        },
         "is-extglob": {
             "version": "2.1.1",
             "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
             "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="
         },
         "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==",
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
+            "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
             "dev": true
         },
         "is-glob": {
@@ -6198,6 +7817,21 @@
                 "is-extglob": "^2.1.1"
             }
         },
+        "is-lambda": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz",
+            "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==",
+            "dev": true
+        },
+        "is-lower-case": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/is-lower-case/-/is-lower-case-2.0.2.tgz",
+            "integrity": "sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ==",
+            "dev": true,
+            "requires": {
+                "tslib": "^2.0.3"
+            }
+        },
         "is-number": {
             "version": "7.0.0",
             "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -6209,23 +7843,20 @@
             "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
             "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="
         },
-        "is-stream": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
-            "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
-            "dev": true
-        },
-        "is-typedarray": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
-            "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
+        "is-plain-object": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+            "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
             "dev": true
         },
-        "is-windows": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
-            "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
-            "dev": true
+        "is-upper-case": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/is-upper-case/-/is-upper-case-2.0.2.tgz",
+            "integrity": "sha512-44pxmxAvnnAOwBg4tHPnkfvgjPwbc5QIsSstNU+YcJ1ovxVzCWpSGosPJOZh/a1tdl81fbgnLc9LLv+x2ywbPQ==",
+            "dev": true,
+            "requires": {
+                "tslib": "^2.0.3"
+            }
         },
         "isexe": {
             "version": "2.0.0",
@@ -6238,67 +7869,21 @@
             "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==",
             "dev": true
         },
-        "istanbul-lib-hook": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz",
-            "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==",
-            "dev": true,
-            "requires": {
-                "append-transform": "^2.0.0"
-            }
-        },
-        "istanbul-lib-instrument": {
-            "version": "4.0.3",
-            "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz",
-            "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==",
-            "dev": true,
-            "requires": {
-                "@babel/core": "^7.7.5",
-                "@istanbuljs/schema": "^0.1.2",
-                "istanbul-lib-coverage": "^3.0.0",
-                "semver": "^6.3.0"
-            }
-        },
-        "istanbul-lib-processinfo": {
-            "version": "2.0.3",
-            "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz",
-            "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==",
-            "dev": true,
-            "requires": {
-                "archy": "^1.0.0",
-                "cross-spawn": "^7.0.3",
-                "istanbul-lib-coverage": "^3.2.0",
-                "p-map": "^3.0.0",
-                "rimraf": "^3.0.0",
-                "uuid": "^8.3.2"
-            }
-        },
         "istanbul-lib-report": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz",
-            "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==",
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+            "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
             "dev": true,
             "requires": {
                 "istanbul-lib-coverage": "^3.0.0",
-                "make-dir": "^3.0.0",
+                "make-dir": "^4.0.0",
                 "supports-color": "^7.1.0"
             }
         },
-        "istanbul-lib-source-maps": {
-            "version": "4.0.1",
-            "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz",
-            "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==",
-            "dev": true,
-            "requires": {
-                "debug": "^4.1.1",
-                "istanbul-lib-coverage": "^3.0.0",
-                "source-map": "^0.6.1"
-            }
-        },
         "istanbul-reports": {
-            "version": "3.1.5",
-            "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz",
-            "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==",
+            "version": "3.1.6",
+            "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz",
+            "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==",
             "dev": true,
             "requires": {
                 "html-escaper": "^2.0.0",
@@ -6306,12 +7891,13 @@
             }
         },
         "jackspeak": {
-            "version": "1.4.2",
-            "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-1.4.2.tgz",
-            "integrity": "sha512-GHeGTmnuaHnvS+ZctRB01bfxARuu9wW83ENbuiweu07SFcVlZrJpcshSre/keGT7YGBhLHg/+rXCNSrsEHKU4Q==",
+            "version": "2.3.6",
+            "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
+            "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
             "dev": true,
             "requires": {
-                "cliui": "^7.0.4"
+                "@isaacs/cliui": "^8.0.2",
+                "@pkgjs/parseargs": "^0.11.0"
             }
         },
         "js-sdsl": {
@@ -6333,10 +7919,10 @@
                 "argparse": "^2.0.1"
             }
         },
-        "jsesc": {
-            "version": "2.5.2",
-            "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
-            "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
+        "json-parse-even-better-errors": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz",
+            "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==",
             "dev": true
         },
         "json-schema-traverse": {
@@ -6349,10 +7935,10 @@
             "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
             "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="
         },
-        "json5": {
-            "version": "2.2.3",
-            "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
-            "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+        "jsonparse": {
+            "version": "1.3.1",
+            "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
+            "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==",
             "dev": true
         },
         "levn": {
@@ -6364,51 +7950,18 @@
                 "type-check": "~0.4.0"
             }
         },
-        "libtap": {
-            "version": "1.4.0",
-            "resolved": "https://registry.npmjs.org/libtap/-/libtap-1.4.0.tgz",
-            "integrity": "sha512-STLFynswQ2A6W14JkabgGetBNk6INL1REgJ9UeNKw5llXroC2cGLgKTqavv0sl8OLVztLLipVKMcQ7yeUcqpmg==",
-            "dev": true,
-            "requires": {
-                "async-hook-domain": "^2.0.4",
-                "bind-obj-methods": "^3.0.0",
-                "diff": "^4.0.2",
-                "function-loop": "^2.0.1",
-                "minipass": "^3.1.5",
-                "own-or": "^1.0.0",
-                "own-or-env": "^1.0.2",
-                "signal-exit": "^3.0.4",
-                "stack-utils": "^2.0.4",
-                "tap-parser": "^11.0.0",
-                "tap-yaml": "^1.0.0",
-                "tcompare": "^5.0.6",
-                "trivial-deferred": "^1.0.1"
-            },
-            "dependencies": {
-                "tcompare": {
-                    "version": "5.0.7",
-                    "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-5.0.7.tgz",
-                    "integrity": "sha512-d9iddt6YYGgyxJw5bjsN7UJUO1kGOtjSlNy/4PoGYAjQS5pAT/hzIoLf1bZCw+uUxRmZJh7Yy1aA7xKVRT9B4w==",
-                    "dev": true,
-                    "requires": {
-                        "diff": "^4.0.2"
-                    }
-                }
-            }
-        },
         "locate-path": {
-            "version": "5.0.0",
-            "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
-            "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
-            "dev": true,
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+            "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
             "requires": {
-                "p-locate": "^4.1.0"
+                "p-locate": "^5.0.0"
             }
         },
-        "lodash.flattendeep": {
-            "version": "4.4.0",
-            "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz",
-            "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==",
+        "lodash": {
+            "version": "4.17.21",
+            "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+            "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
             "dev": true
         },
         "lodash.merge": {
@@ -6416,30 +7969,130 @@
             "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
             "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
         },
-        "lru-cache": {
-            "version": "5.1.1",
-            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
-            "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+        "loose-envify": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+            "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
             "dev": true,
             "requires": {
-                "yallist": "^3.0.2"
-            },
-            "dependencies": {
-                "yallist": {
-                    "version": "3.1.1",
-                    "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
-                    "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
-                    "dev": true
-                }
+                "js-tokens": "^3.0.0 || ^4.0.0"
             }
         },
+        "lru-cache": {
+            "version": "10.0.1",
+            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz",
+            "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==",
+            "dev": true
+        },
         "make-dir": {
-            "version": "3.1.0",
-            "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
-            "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+            "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+            "dev": true,
+            "requires": {
+                "semver": "^7.5.3"
+            }
+        },
+        "make-error": {
+            "version": "1.3.6",
+            "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+            "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+            "dev": true
+        },
+        "make-fetch-happen": {
+            "version": "11.1.1",
+            "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz",
+            "integrity": "sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==",
             "dev": true,
             "requires": {
-                "semver": "^6.0.0"
+                "agentkeepalive": "^4.2.1",
+                "cacache": "^17.0.0",
+                "http-cache-semantics": "^4.1.1",
+                "http-proxy-agent": "^5.0.0",
+                "https-proxy-agent": "^5.0.0",
+                "is-lambda": "^1.0.1",
+                "lru-cache": "^7.7.1",
+                "minipass": "^5.0.0",
+                "minipass-fetch": "^3.0.0",
+                "minipass-flush": "^1.0.5",
+                "minipass-pipeline": "^1.2.4",
+                "negotiator": "^0.6.3",
+                "promise-retry": "^2.0.1",
+                "socks-proxy-agent": "^7.0.0",
+                "ssri": "^10.0.0"
+            },
+            "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"
+                    }
+                },
+                "cacache": {
+                    "version": "17.1.4",
+                    "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.4.tgz",
+                    "integrity": "sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A==",
+                    "dev": true,
+                    "requires": {
+                        "@npmcli/fs": "^3.1.0",
+                        "fs-minipass": "^3.0.0",
+                        "glob": "^10.2.2",
+                        "lru-cache": "^7.7.1",
+                        "minipass": "^7.0.3",
+                        "minipass-collect": "^1.0.2",
+                        "minipass-flush": "^1.0.5",
+                        "minipass-pipeline": "^1.2.4",
+                        "p-map": "^4.0.0",
+                        "ssri": "^10.0.0",
+                        "tar": "^6.1.11",
+                        "unique-filename": "^3.0.0"
+                    },
+                    "dependencies": {
+                        "minipass": {
+                            "version": "7.0.4",
+                            "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
+                            "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
+                            "dev": true
+                        }
+                    }
+                },
+                "glob": {
+                    "version": "10.3.10",
+                    "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+                    "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
+                    "dev": true,
+                    "requires": {
+                        "foreground-child": "^3.1.0",
+                        "jackspeak": "^2.3.5",
+                        "minimatch": "^9.0.1",
+                        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+                        "path-scurry": "^1.10.1"
+                    }
+                },
+                "lru-cache": {
+                    "version": "7.18.3",
+                    "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+                    "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+                    "dev": true
+                },
+                "minimatch": {
+                    "version": "9.0.3",
+                    "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+                    "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+                    "dev": true,
+                    "requires": {
+                        "brace-expansion": "^2.0.1"
+                    }
+                },
+                "minipass": {
+                    "version": "5.0.0",
+                    "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
+                    "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+                    "dev": true
+                }
             }
         },
         "marked": {
@@ -6447,6 +8100,12 @@
             "resolved": "https://registry.npmjs.org/marked/-/marked-5.0.2.tgz",
             "integrity": "sha512-TXksm9GwqXCRNbFUZmMtqNLvy3K2cQHuWmyBDLOrY1e6i9UvZpOTJXoz7fBjYkJkaUFzV9hBFxMuZSyQt8R6KQ=="
         },
+        "mimic-fn": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+            "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+            "dev": true
+        },
         "minimatch": {
             "version": "3.1.2",
             "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -6456,18 +8115,149 @@
             }
         },
         "minipass": {
-            "version": "3.3.6",
-            "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
-            "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+            "version": "7.0.4",
+            "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
+            "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
+            "dev": true
+        },
+        "minipass-collect": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz",
+            "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==",
             "dev": true,
             "requires": {
+                "minipass": "^3.0.0"
+            },
+            "dependencies": {
+                "minipass": {
+                    "version": "3.3.6",
+                    "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+                    "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+                    "dev": true,
+                    "requires": {
+                        "yallist": "^4.0.0"
+                    }
+                }
+            }
+        },
+        "minipass-fetch": {
+            "version": "3.0.4",
+            "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.4.tgz",
+            "integrity": "sha512-jHAqnA728uUpIaFm7NWsCnqKT6UqZz7GcI/bDpPATuwYyKwJwW0remxSCxUlKiEty+eopHGa3oc8WxgQ1FFJqg==",
+            "dev": true,
+            "requires": {
+                "encoding": "^0.1.13",
+                "minipass": "^7.0.3",
+                "minipass-sized": "^1.0.3",
+                "minizlib": "^2.1.2"
+            }
+        },
+        "minipass-flush": {
+            "version": "1.0.5",
+            "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz",
+            "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
+            "dev": true,
+            "requires": {
+                "minipass": "^3.0.0"
+            },
+            "dependencies": {
+                "minipass": {
+                    "version": "3.3.6",
+                    "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+                    "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+                    "dev": true,
+                    "requires": {
+                        "yallist": "^4.0.0"
+                    }
+                }
+            }
+        },
+        "minipass-json-stream": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz",
+            "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==",
+            "dev": true,
+            "requires": {
+                "jsonparse": "^1.3.1",
+                "minipass": "^3.0.0"
+            },
+            "dependencies": {
+                "minipass": {
+                    "version": "3.3.6",
+                    "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+                    "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+                    "dev": true,
+                    "requires": {
+                        "yallist": "^4.0.0"
+                    }
+                }
+            }
+        },
+        "minipass-pipeline": {
+            "version": "1.2.4",
+            "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
+            "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
+            "dev": true,
+            "requires": {
+                "minipass": "^3.0.0"
+            },
+            "dependencies": {
+                "minipass": {
+                    "version": "3.3.6",
+                    "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+                    "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+                    "dev": true,
+                    "requires": {
+                        "yallist": "^4.0.0"
+                    }
+                }
+            }
+        },
+        "minipass-sized": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz",
+            "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==",
+            "dev": true,
+            "requires": {
+                "minipass": "^3.0.0"
+            },
+            "dependencies": {
+                "minipass": {
+                    "version": "3.3.6",
+                    "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+                    "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+                    "dev": true,
+                    "requires": {
+                        "yallist": "^4.0.0"
+                    }
+                }
+            }
+        },
+        "minizlib": {
+            "version": "2.1.2",
+            "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+            "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+            "dev": true,
+            "requires": {
+                "minipass": "^3.0.0",
                 "yallist": "^4.0.0"
+            },
+            "dependencies": {
+                "minipass": {
+                    "version": "3.3.6",
+                    "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+                    "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+                    "dev": true,
+                    "requires": {
+                        "yallist": "^4.0.0"
+                    }
+                }
             }
         },
         "mkdirp": {
-            "version": "1.0.4",
-            "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
-            "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
+            "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
             "dev": true
         },
         "ms": {
@@ -6480,20 +8270,51 @@
             "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
             "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
         },
-        "node-preload": {
-            "version": "0.2.1",
-            "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz",
-            "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==",
+        "negotiator": {
+            "version": "0.6.3",
+            "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+            "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+            "dev": true
+        },
+        "node-gyp": {
+            "version": "9.4.0",
+            "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.0.tgz",
+            "integrity": "sha512-dMXsYP6gc9rRbejLXmTbVRYjAHw7ppswsKyMxuxJxxOHzluIO1rGp9TOQgjFJ+2MCqcOcQTOPB/8Xwhr+7s4Eg==",
             "dev": true,
             "requires": {
-                "process-on-spawn": "^1.0.0"
+                "env-paths": "^2.2.0",
+                "exponential-backoff": "^3.1.1",
+                "glob": "^7.1.4",
+                "graceful-fs": "^4.2.6",
+                "make-fetch-happen": "^11.0.3",
+                "nopt": "^6.0.0",
+                "npmlog": "^6.0.0",
+                "rimraf": "^3.0.2",
+                "semver": "^7.3.5",
+                "tar": "^6.1.2",
+                "which": "^2.0.2"
             }
         },
-        "node-releases": {
-            "version": "2.0.10",
-            "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz",
-            "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==",
-            "dev": true
+        "nopt": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz",
+            "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==",
+            "dev": true,
+            "requires": {
+                "abbrev": "^1.0.0"
+            }
+        },
+        "normalize-package-data": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.0.tgz",
+            "integrity": "sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==",
+            "dev": true,
+            "requires": {
+                "hosted-git-info": "^7.0.0",
+                "is-core-module": "^2.8.1",
+                "semver": "^7.3.5",
+                "validate-npm-package-license": "^3.0.4"
+            }
         },
         "normalize-path": {
             "version": "3.0.0",
@@ -6501,49 +8322,111 @@
             "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
             "dev": true
         },
-        "nyc": {
-            "version": "15.1.0",
-            "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz",
-            "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==",
+        "npm-bundled": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.0.tgz",
+            "integrity": "sha512-Vq0eyEQy+elFpzsKjMss9kxqb9tG3YHg4dsyWuUENuzvSUWe1TCnW/vV9FkhvBk/brEDoDiVd+M1Btosa6ImdQ==",
             "dev": true,
             "requires": {
-                "@istanbuljs/load-nyc-config": "^1.0.0",
-                "@istanbuljs/schema": "^0.1.2",
-                "caching-transform": "^4.0.0",
-                "convert-source-map": "^1.7.0",
-                "decamelize": "^1.2.0",
-                "find-cache-dir": "^3.2.0",
-                "find-up": "^4.1.0",
-                "foreground-child": "^2.0.0",
-                "get-package-type": "^0.1.0",
-                "glob": "^7.1.6",
-                "istanbul-lib-coverage": "^3.0.0",
-                "istanbul-lib-hook": "^3.0.0",
-                "istanbul-lib-instrument": "^4.0.0",
-                "istanbul-lib-processinfo": "^2.0.2",
-                "istanbul-lib-report": "^3.0.0",
-                "istanbul-lib-source-maps": "^4.0.0",
-                "istanbul-reports": "^3.0.2",
-                "make-dir": "^3.0.0",
-                "node-preload": "^0.2.1",
-                "p-map": "^3.0.0",
-                "process-on-spawn": "^1.0.0",
-                "resolve-from": "^5.0.0",
-                "rimraf": "^3.0.0",
-                "signal-exit": "^3.0.2",
-                "spawn-wrap": "^2.0.0",
-                "test-exclude": "^6.0.0",
-                "yargs": "^15.0.2"
-            },
-            "dependencies": {
-                "resolve-from": {
-                    "version": "5.0.0",
-                    "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
-                    "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
-                    "dev": true
+                "npm-normalize-package-bin": "^3.0.0"
+            }
+        },
+        "npm-install-checks": {
+            "version": "6.2.0",
+            "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.2.0.tgz",
+            "integrity": "sha512-744wat5wAAHsxa4590mWO0tJ8PKxR8ORZsH9wGpQc3nWTzozMAgBN/XyqYw7mg3yqLM8dLwEnwSfKMmXAjF69g==",
+            "dev": true,
+            "requires": {
+                "semver": "^7.1.1"
+            }
+        },
+        "npm-normalize-package-bin": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz",
+            "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==",
+            "dev": true
+        },
+        "npm-package-arg": {
+            "version": "11.0.1",
+            "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.1.tgz",
+            "integrity": "sha512-M7s1BD4NxdAvBKUPqqRW957Xwcl/4Zvo8Aj+ANrzvIPzGJZElrH7Z//rSaec2ORcND6FHHLnZeY8qgTpXDMFQQ==",
+            "dev": true,
+            "requires": {
+                "hosted-git-info": "^7.0.0",
+                "proc-log": "^3.0.0",
+                "semver": "^7.3.5",
+                "validate-npm-package-name": "^5.0.0"
+            }
+        },
+        "npm-packlist": {
+            "version": "8.0.0",
+            "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.0.tgz",
+            "integrity": "sha512-ErAGFB5kJUciPy1mmx/C2YFbvxoJ0QJ9uwkCZOeR6CqLLISPZBOiFModAbSXnjjlwW5lOhuhXva+fURsSGJqyw==",
+            "dev": true,
+            "requires": {
+                "ignore-walk": "^6.0.0"
+            }
+        },
+        "npm-pick-manifest": {
+            "version": "9.0.0",
+            "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.0.0.tgz",
+            "integrity": "sha512-VfvRSs/b6n9ol4Qb+bDwNGUXutpy76x6MARw/XssevE0TnctIKcmklJZM5Z7nqs5z5aW+0S63pgCNbpkUNNXBg==",
+            "dev": true,
+            "requires": {
+                "npm-install-checks": "^6.0.0",
+                "npm-normalize-package-bin": "^3.0.0",
+                "npm-package-arg": "^11.0.0",
+                "semver": "^7.3.5"
+            }
+        },
+        "npm-registry-fetch": {
+            "version": "16.0.0",
+            "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-16.0.0.tgz",
+            "integrity": "sha512-JFCpAPUpvpwfSydv99u85yhP68rNIxSFmDpNbNnRWKSe3gpjHnWL8v320gATwRzjtgmZ9Jfe37+ZPOLZPwz6BQ==",
+            "dev": true,
+            "requires": {
+                "make-fetch-happen": "^13.0.0",
+                "minipass": "^7.0.2",
+                "minipass-fetch": "^3.0.0",
+                "minipass-json-stream": "^1.0.1",
+                "minizlib": "^2.1.2",
+                "npm-package-arg": "^11.0.0",
+                "proc-log": "^3.0.0"
+            },
+            "dependencies": {
+                "make-fetch-happen": {
+                    "version": "13.0.0",
+                    "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.0.tgz",
+                    "integrity": "sha512-7ThobcL8brtGo9CavByQrQi+23aIfgYU++wg4B87AIS8Rb2ZBt/MEaDqzA00Xwv/jUjAjYkLHjVolYuTLKda2A==",
+                    "dev": true,
+                    "requires": {
+                        "@npmcli/agent": "^2.0.0",
+                        "cacache": "^18.0.0",
+                        "http-cache-semantics": "^4.1.1",
+                        "is-lambda": "^1.0.1",
+                        "minipass": "^7.0.2",
+                        "minipass-fetch": "^3.0.0",
+                        "minipass-flush": "^1.0.5",
+                        "minipass-pipeline": "^1.2.4",
+                        "negotiator": "^0.6.3",
+                        "promise-retry": "^2.0.1",
+                        "ssri": "^10.0.0"
+                    }
                 }
             }
         },
+        "npmlog": {
+            "version": "6.0.2",
+            "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz",
+            "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==",
+            "dev": true,
+            "requires": {
+                "are-we-there-yet": "^3.0.0",
+                "console-control-strings": "^1.1.0",
+                "gauge": "^4.0.3",
+                "set-blocking": "^2.0.0"
+            }
+        },
         "once": {
             "version": "1.4.0",
             "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -6552,6 +8435,15 @@
                 "wrappy": "1"
             }
         },
+        "onetime": {
+            "version": "5.1.2",
+            "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+            "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+            "dev": true,
+            "requires": {
+                "mimic-fn": "^2.1.0"
+            }
+        },
         "opener": {
             "version": "1.5.2",
             "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
@@ -6571,64 +8463,55 @@
                 "word-wrap": "^1.2.3"
             }
         },
-        "own-or": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/own-or/-/own-or-1.0.0.tgz",
-            "integrity": "sha512-NfZr5+Tdf6MB8UI9GLvKRs4cXY8/yB0w3xtt84xFdWy8hkGjn+JFc60VhzS/hFRfbyxFcGYMTjnF4Me+RbbqrA==",
-            "dev": true
-        },
-        "own-or-env": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/own-or-env/-/own-or-env-1.0.2.tgz",
-            "integrity": "sha512-NQ7v0fliWtK7Lkb+WdFqe6ky9XAzYmlkXthQrBbzlYbmFKoAYbDDcwmOm6q8kOuwSRXW8bdL5ORksploUJmWgw==",
-            "dev": true,
-            "requires": {
-                "own-or": "^1.0.0"
-            }
-        },
         "p-limit": {
-            "version": "2.3.0",
-            "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
-            "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
-            "dev": true,
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+            "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
             "requires": {
-                "p-try": "^2.0.0"
+                "yocto-queue": "^0.1.0"
             }
         },
         "p-locate": {
-            "version": "4.1.0",
-            "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
-            "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
-            "dev": true,
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+            "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
             "requires": {
-                "p-limit": "^2.2.0"
+                "p-limit": "^3.0.2"
             }
         },
         "p-map": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz",
-            "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==",
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
+            "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
             "dev": true,
             "requires": {
                 "aggregate-error": "^3.0.0"
             }
         },
-        "p-try": {
-            "version": "2.2.0",
-            "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
-            "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
-            "dev": true
-        },
-        "package-hash": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz",
-            "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==",
+        "pacote": {
+            "version": "17.0.4",
+            "resolved": "https://registry.npmjs.org/pacote/-/pacote-17.0.4.tgz",
+            "integrity": "sha512-eGdLHrV/g5b5MtD5cTPyss+JxOlaOloSMG3UwPMAvL8ywaLJ6beONPF40K4KKl/UI6q5hTKCJq5rCu8tkF+7Dg==",
             "dev": true,
             "requires": {
-                "graceful-fs": "^4.1.15",
-                "hasha": "^5.0.0",
-                "lodash.flattendeep": "^4.4.0",
-                "release-zalgo": "^1.0.0"
+                "@npmcli/git": "^5.0.0",
+                "@npmcli/installed-package-contents": "^2.0.1",
+                "@npmcli/promise-spawn": "^7.0.0",
+                "@npmcli/run-script": "^7.0.0",
+                "cacache": "^18.0.0",
+                "fs-minipass": "^3.0.0",
+                "minipass": "^7.0.2",
+                "npm-package-arg": "^11.0.0",
+                "npm-packlist": "^8.0.0",
+                "npm-pick-manifest": "^9.0.0",
+                "npm-registry-fetch": "^16.0.0",
+                "proc-log": "^3.0.0",
+                "promise-retry": "^2.0.1",
+                "read-package-json": "^7.0.0",
+                "read-package-json-fast": "^3.0.0",
+                "sigstore": "^2.0.0",
+                "ssri": "^10.0.0",
+                "tar": "^6.1.11"
             }
         },
         "parent-module": {
@@ -6639,6 +8522,12 @@
                 "callsites": "^3.0.0"
             }
         },
+        "patch-console": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz",
+            "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==",
+            "dev": true
+        },
         "path-exists": {
             "version": "4.0.0",
             "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -6654,11 +8543,15 @@
             "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
             "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
         },
-        "picocolors": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
-            "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
-            "dev": true
+        "path-scurry": {
+            "version": "1.10.1",
+            "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
+            "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
+            "dev": true,
+            "requires": {
+                "lru-cache": "^9.1.1 || ^10.0.0",
+                "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+            }
         },
         "picomatch": {
             "version": "2.3.1",
@@ -6666,20 +8559,54 @@
             "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
             "dev": true
         },
-        "pkg-dir": {
-            "version": "4.2.0",
-            "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
-            "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
-            "dev": true,
-            "requires": {
-                "find-up": "^4.0.0"
-            }
+        "pirates": {
+            "version": "4.0.6",
+            "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
+            "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
+            "dev": true
+        },
+        "polite-json": {
+            "version": "4.0.1",
+            "resolved": "https://registry.npmjs.org/polite-json/-/polite-json-4.0.1.tgz",
+            "integrity": "sha512-8LI5ZeCPBEb4uBbcYKNVwk4jgqNx1yHReWoW4H4uUihWlSqZsUDfSITrRhjliuPgxsNPFhNSudGO2Zu4cbWinQ==",
+            "dev": true
         },
         "prelude-ls": {
             "version": "1.2.1",
             "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
             "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="
         },
+        "prismjs": {
+            "version": "1.29.0",
+            "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
+            "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
+            "dev": true
+        },
+        "prismjs-terminal": {
+            "version": "1.2.3",
+            "resolved": "https://registry.npmjs.org/prismjs-terminal/-/prismjs-terminal-1.2.3.tgz",
+            "integrity": "sha512-xc0zuJ5FMqvW+DpiRkvxURlz98DdfDsZcFHdO699+oL+ykbFfgI7O4VDEgUyc07BSL2NHl3zdb8m/tZ/aaqUrw==",
+            "dev": true,
+            "requires": {
+                "chalk": "^5.2.0",
+                "prismjs": "^1.29.0",
+                "string-length": "^6.0.0"
+            },
+            "dependencies": {
+                "chalk": {
+                    "version": "5.3.0",
+                    "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
+                    "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
+                    "dev": true
+                }
+            }
+        },
+        "proc-log": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz",
+            "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==",
+            "dev": true
+        },
         "process-on-spawn": {
             "version": "1.0.0",
             "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz",
@@ -6689,16 +8616,153 @@
                 "fromentries": "^1.2.0"
             }
         },
+        "promise-inflight": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
+            "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==",
+            "dev": true
+        },
+        "promise-retry": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
+            "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
+            "dev": true,
+            "requires": {
+                "err-code": "^2.0.2",
+                "retry": "^0.12.0"
+            }
+        },
         "punycode": {
             "version": "2.1.1",
             "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
             "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
         },
+        "queue": {
+            "version": "6.0.2",
+            "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
+            "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
+            "requires": {
+                "inherits": "~2.0.3"
+            }
+        },
         "queue-microtask": {
             "version": "1.2.3",
             "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
             "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
         },
+        "react": {
+            "version": "18.2.0",
+            "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
+            "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
+            "dev": true,
+            "requires": {
+                "loose-envify": "^1.1.0"
+            }
+        },
+        "react-dom": {
+            "version": "18.2.0",
+            "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
+            "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
+            "dev": true,
+            "peer": true,
+            "requires": {
+                "loose-envify": "^1.1.0",
+                "scheduler": "^0.23.0"
+            }
+        },
+        "react-element-to-jsx-string": {
+            "version": "15.0.0",
+            "resolved": "https://registry.npmjs.org/react-element-to-jsx-string/-/react-element-to-jsx-string-15.0.0.tgz",
+            "integrity": "sha512-UDg4lXB6BzlobN60P8fHWVPX3Kyw8ORrTeBtClmIlGdkOOE+GYQSFvmEU5iLLpwp/6v42DINwNcwOhOLfQ//FQ==",
+            "dev": true,
+            "requires": {
+                "@base2/pretty-print-object": "1.0.1",
+                "is-plain-object": "5.0.0",
+                "react-is": "18.1.0"
+            }
+        },
+        "react-is": {
+            "version": "18.1.0",
+            "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz",
+            "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==",
+            "dev": true
+        },
+        "react-reconciler": {
+            "version": "0.29.0",
+            "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.0.tgz",
+            "integrity": "sha512-wa0fGj7Zht1EYMRhKWwoo1H9GApxYLBuhoAuXN0TlltESAjDssB+Apf0T/DngVqaMyPypDmabL37vw/2aRM98Q==",
+            "dev": true,
+            "requires": {
+                "loose-envify": "^1.1.0",
+                "scheduler": "^0.23.0"
+            }
+        },
+        "read-package-json": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-7.0.0.tgz",
+            "integrity": "sha512-uL4Z10OKV4p6vbdvIXB+OzhInYtIozl/VxUBPgNkBuUi2DeRonnuspmaVAMcrkmfjKGNmRndyQAbE7/AmzGwFg==",
+            "dev": true,
+            "requires": {
+                "glob": "^10.2.2",
+                "json-parse-even-better-errors": "^3.0.0",
+                "normalize-package-data": "^6.0.0",
+                "npm-normalize-package-bin": "^3.0.0"
+            },
+            "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.3.10",
+                    "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+                    "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
+                    "dev": true,
+                    "requires": {
+                        "foreground-child": "^3.1.0",
+                        "jackspeak": "^2.3.5",
+                        "minimatch": "^9.0.1",
+                        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+                        "path-scurry": "^1.10.1"
+                    }
+                },
+                "minimatch": {
+                    "version": "9.0.3",
+                    "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+                    "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+                    "dev": true,
+                    "requires": {
+                        "brace-expansion": "^2.0.1"
+                    }
+                }
+            }
+        },
+        "read-package-json-fast": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz",
+            "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==",
+            "dev": true,
+            "requires": {
+                "json-parse-even-better-errors": "^3.0.0",
+                "npm-normalize-package-bin": "^3.0.0"
+            }
+        },
+        "readable-stream": {
+            "version": "3.6.2",
+            "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+            "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+            "dev": true,
+            "requires": {
+                "inherits": "^2.0.3",
+                "string_decoder": "^1.1.1",
+                "util-deprecate": "^1.0.1"
+            }
+        },
         "readdirp": {
             "version": "3.6.0",
             "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -6708,32 +8772,84 @@
                 "picomatch": "^2.2.1"
             }
         },
-        "release-zalgo": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz",
-            "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==",
-            "dev": true,
-            "requires": {
-                "es6-error": "^4.0.1"
-            }
-        },
         "require-directory": {
             "version": "2.1.1",
             "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
             "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
             "dev": true
         },
-        "require-main-filename": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
-            "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
-            "dev": true
-        },
         "resolve-from": {
             "version": "4.0.0",
             "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
             "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
         },
+        "resolve-import": {
+            "version": "1.4.2",
+            "resolved": "https://registry.npmjs.org/resolve-import/-/resolve-import-1.4.2.tgz",
+            "integrity": "sha512-ayUU3E2yeFu8ZewNEHbGorcPmHjOmCY8b50wloum8eQUuNExSyddRoWYaX0X6lj3XSufi2WUlXY3mkMcF5ISmw==",
+            "dev": true,
+            "requires": {
+                "glob": "^10.3.3",
+                "walk-up-path": "^3.0.1"
+            },
+            "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.3.10",
+                    "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+                    "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
+                    "dev": true,
+                    "requires": {
+                        "foreground-child": "^3.1.0",
+                        "jackspeak": "^2.3.5",
+                        "minimatch": "^9.0.1",
+                        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+                        "path-scurry": "^1.10.1"
+                    }
+                },
+                "minimatch": {
+                    "version": "9.0.3",
+                    "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+                    "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+                    "dev": true,
+                    "requires": {
+                        "brace-expansion": "^2.0.1"
+                    }
+                }
+            }
+        },
+        "restore-cursor": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz",
+            "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==",
+            "dev": true,
+            "requires": {
+                "onetime": "^5.1.0",
+                "signal-exit": "^3.0.2"
+            },
+            "dependencies": {
+                "signal-exit": {
+                    "version": "3.0.7",
+                    "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+                    "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+                    "dev": true
+                }
+            }
+        },
+        "retry": {
+            "version": "0.12.0",
+            "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+            "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+            "dev": true
+        },
         "reusify": {
             "version": "1.0.4",
             "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -6755,12 +8871,48 @@
                 "queue-microtask": "^1.2.2"
             }
         },
-        "semver": {
-            "version": "6.3.0",
-            "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-            "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+        "safe-buffer": {
+            "version": "5.2.1",
+            "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+            "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
             "dev": true
         },
+        "safer-buffer": {
+            "version": "2.1.2",
+            "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+            "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+            "dev": true,
+            "optional": true
+        },
+        "scheduler": {
+            "version": "0.23.0",
+            "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
+            "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
+            "dev": true,
+            "requires": {
+                "loose-envify": "^1.1.0"
+            }
+        },
+        "semver": {
+            "version": "7.5.4",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+            "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+            "dev": true,
+            "requires": {
+                "lru-cache": "^6.0.0"
+            },
+            "dependencies": {
+                "lru-cache": {
+                    "version": "6.0.0",
+                    "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+                    "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+                    "dev": true,
+                    "requires": {
+                        "yallist": "^4.0.0"
+                    }
+                }
+            }
+        },
         "set-blocking": {
             "version": "2.0.0",
             "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@@ -6781,47 +8933,109 @@
             "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
         },
         "signal-exit": {
-            "version": "3.0.7",
-            "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
-            "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+            "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
             "dev": true
         },
-        "source-map": {
-            "version": "0.6.1",
-            "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-            "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+        "sigstore": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.1.0.tgz",
+            "integrity": "sha512-kPIj+ZLkyI3QaM0qX8V/nSsweYND3W448pwkDgS6CQ74MfhEkIR8ToK5Iyx46KJYRjseVcD3Rp9zAmUAj6ZjPw==",
+            "dev": true,
+            "requires": {
+                "@sigstore/bundle": "^2.1.0",
+                "@sigstore/protobuf-specs": "^0.2.1",
+                "@sigstore/sign": "^2.1.0",
+                "@sigstore/tuf": "^2.1.0"
+            }
+        },
+        "slice-ansi": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-6.0.0.tgz",
+            "integrity": "sha512-6bn4hRfkTvDfUoEQYkERg0BVF1D0vrX9HEkMl08uDiNWvVvjylLHvZFZWkDo6wjT8tUctbYl1nCOuE66ZTaUtA==",
+            "dev": true,
+            "requires": {
+                "ansi-styles": "^6.2.1",
+                "is-fullwidth-code-point": "^4.0.0"
+            },
+            "dependencies": {
+                "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
+                }
+            }
+        },
+        "smart-buffer": {
+            "version": "4.2.0",
+            "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+            "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
             "dev": true
         },
-        "source-map-support": {
-            "version": "0.5.21",
-            "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
-            "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+        "socks": {
+            "version": "2.7.1",
+            "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz",
+            "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==",
             "dev": true,
             "requires": {
-                "buffer-from": "^1.0.0",
-                "source-map": "^0.6.0"
+                "ip": "^2.0.0",
+                "smart-buffer": "^4.2.0"
             }
         },
-        "spawn-wrap": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz",
-            "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==",
+        "socks-proxy-agent": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz",
+            "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==",
             "dev": true,
             "requires": {
-                "foreground-child": "^2.0.0",
-                "is-windows": "^1.0.2",
-                "make-dir": "^3.0.0",
-                "rimraf": "^3.0.0",
-                "signal-exit": "^3.0.2",
-                "which": "^2.0.1"
+                "agent-base": "^6.0.2",
+                "debug": "^4.3.3",
+                "socks": "^2.6.2"
             }
         },
-        "sprintf-js": {
-            "version": "1.0.3",
-            "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
-            "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+        "spdx-correct": {
+            "version": "3.2.0",
+            "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
+            "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
+            "dev": true,
+            "requires": {
+                "spdx-expression-parse": "^3.0.0",
+                "spdx-license-ids": "^3.0.0"
+            }
+        },
+        "spdx-exceptions": {
+            "version": "2.3.0",
+            "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
+            "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
+            "dev": true
+        },
+        "spdx-expression-parse": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+            "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+            "dev": true,
+            "requires": {
+                "spdx-exceptions": "^2.1.0",
+                "spdx-license-ids": "^3.0.0"
+            }
+        },
+        "spdx-license-ids": {
+            "version": "3.0.15",
+            "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.15.tgz",
+            "integrity": "sha512-lpT8hSQp9jAKp9mhtBU4Xjon8LPGBvLIuBiSVhMEtmLecTh2mO0tlqrAMp47tBXzMr13NJMQ2lf7RpQGLJ3HsQ==",
             "dev": true
         },
+        "ssri": {
+            "version": "10.0.5",
+            "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.5.tgz",
+            "integrity": "sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==",
+            "dev": true,
+            "requires": {
+                "minipass": "^7.0.3"
+            }
+        },
         "stack-utils": {
             "version": "2.0.6",
             "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
@@ -6839,8 +9053,71 @@
                 }
             }
         },
+        "string_decoder": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+            "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+            "dev": true,
+            "requires": {
+                "safe-buffer": "~5.2.0"
+            }
+        },
+        "string-length": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/string-length/-/string-length-6.0.0.tgz",
+            "integrity": "sha512-1U361pxZHEQ+FeSjzqRpV+cu2vTzYeWeafXFLykiFlv4Vc0n3njgU8HrMbyik5uwm77naWMuVG8fhEF+Ovb1Kg==",
+            "dev": true,
+            "requires": {
+                "strip-ansi": "^7.1.0"
+            },
+            "dependencies": {
+                "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
+                },
+                "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"
+                    }
+                }
+            }
+        },
         "string-width": {
-            "version": "4.2.3",
+            "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",
+                "strip-ansi": "^7.0.1"
+            },
+            "dependencies": {
+                "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
+                },
+                "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"
+                    }
+                }
+            }
+        },
+        "string-width-cjs": {
+            "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,
@@ -6848,6 +9125,20 @@
                 "emoji-regex": "^8.0.0",
                 "is-fullwidth-code-point": "^3.0.0",
                 "strip-ansi": "^6.0.1"
+            },
+            "dependencies": {
+                "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
+                },
+                "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
+                }
             }
         },
         "strip-ansi": {
@@ -6858,11 +9149,14 @@
                 "ansi-regex": "^5.0.1"
             }
         },
-        "strip-bom": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
-            "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
-            "dev": true
+        "strip-ansi-cjs": {
+            "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"
+            }
         },
         "strip-json-comments": {
             "version": "3.1.1",
@@ -6882,1324 +9176,162 @@
                 "has-flag": "^4.0.0"
             }
         },
-        "tap": {
-            "version": "16.3.4",
-            "resolved": "https://registry.npmjs.org/tap/-/tap-16.3.4.tgz",
-            "integrity": "sha512-SAexdt2ZF4XBgye6TPucFI2y7VE0qeFXlXucJIV1XDPCs+iJodk0MYacr1zR6Ycltzz7PYg8zrblDXKbAZM2LQ==",
+        "sync-content": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/sync-content/-/sync-content-1.0.2.tgz",
+            "integrity": "sha512-znd3rYiiSxU3WteWyS9a6FXkTA/Wjk8WQsOyzHbineeL837dLn3DA4MRhsIX3qGcxDMH6+uuFV4axztssk7wEQ==",
             "dev": true,
             "requires": {
-                "@isaacs/import-jsx": "^4.0.1",
-                "@types/react": "^17.0.52",
-                "chokidar": "^3.3.0",
-                "findit": "^2.0.0",
-                "foreground-child": "^2.0.0",
-                "fs-exists-cached": "^1.0.0",
-                "glob": "^7.2.3",
-                "ink": "^3.2.0",
-                "isexe": "^2.0.0",
-                "istanbul-lib-processinfo": "^2.0.3",
-                "jackspeak": "^1.4.2",
-                "libtap": "^1.4.0",
-                "minipass": "^3.3.4",
-                "mkdirp": "^1.0.4",
-                "nyc": "^15.1.0",
-                "opener": "^1.5.1",
-                "react": "^17.0.2",
-                "rimraf": "^3.0.0",
-                "signal-exit": "^3.0.6",
-                "source-map-support": "^0.5.16",
-                "tap-mocha-reporter": "^5.0.3",
-                "tap-parser": "^11.0.2",
-                "tap-yaml": "^1.0.2",
-                "tcompare": "^5.0.7",
-                "treport": "^3.0.4",
-                "which": "^2.0.2"
+                "glob": "^10.2.6",
+                "mkdirp": "^3.0.1",
+                "path-scurry": "^1.9.2",
+                "rimraf": "^5.0.1"
             },
             "dependencies": {
-                "@ampproject/remapping": {
-                    "version": "2.1.2",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@jridgewell/trace-mapping": "^0.3.0"
-                    }
-                },
-                "@babel/code-frame": {
-                    "version": "7.16.7",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@babel/highlight": "^7.16.7"
-                    }
-                },
-                "@babel/compat-data": {
-                    "version": "7.17.7",
-                    "bundled": true,
-                    "dev": true
-                },
-                "@babel/core": {
-                    "version": "7.17.8",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@ampproject/remapping": "^2.1.0",
-                        "@babel/code-frame": "^7.16.7",
-                        "@babel/generator": "^7.17.7",
-                        "@babel/helper-compilation-targets": "^7.17.7",
-                        "@babel/helper-module-transforms": "^7.17.7",
-                        "@babel/helpers": "^7.17.8",
-                        "@babel/parser": "^7.17.8",
-                        "@babel/template": "^7.16.7",
-                        "@babel/traverse": "^7.17.3",
-                        "@babel/types": "^7.17.0",
-                        "convert-source-map": "^1.7.0",
-                        "debug": "^4.1.0",
-                        "gensync": "^1.0.0-beta.2",
-                        "json5": "^2.1.2",
-                        "semver": "^6.3.0"
-                    }
-                },
-                "@babel/generator": {
-                    "version": "7.17.7",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@babel/types": "^7.17.0",
-                        "jsesc": "^2.5.1",
-                        "source-map": "^0.5.0"
-                    }
-                },
-                "@babel/helper-annotate-as-pure": {
-                    "version": "7.16.7",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@babel/types": "^7.16.7"
-                    }
-                },
-                "@babel/helper-compilation-targets": {
-                    "version": "7.17.7",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@babel/compat-data": "^7.17.7",
-                        "@babel/helper-validator-option": "^7.16.7",
-                        "browserslist": "^4.17.5",
-                        "semver": "^6.3.0"
-                    }
-                },
-                "@babel/helper-environment-visitor": {
-                    "version": "7.16.7",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@babel/types": "^7.16.7"
-                    }
-                },
-                "@babel/helper-function-name": {
-                    "version": "7.16.7",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@babel/helper-get-function-arity": "^7.16.7",
-                        "@babel/template": "^7.16.7",
-                        "@babel/types": "^7.16.7"
-                    }
-                },
-                "@babel/helper-get-function-arity": {
-                    "version": "7.16.7",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@babel/types": "^7.16.7"
-                    }
-                },
-                "@babel/helper-hoist-variables": {
-                    "version": "7.16.7",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@babel/types": "^7.16.7"
-                    }
-                },
-                "@babel/helper-module-imports": {
-                    "version": "7.16.7",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@babel/types": "^7.16.7"
-                    }
-                },
-                "@babel/helper-module-transforms": {
-                    "version": "7.17.7",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@babel/helper-environment-visitor": "^7.16.7",
-                        "@babel/helper-module-imports": "^7.16.7",
-                        "@babel/helper-simple-access": "^7.17.7",
-                        "@babel/helper-split-export-declaration": "^7.16.7",
-                        "@babel/helper-validator-identifier": "^7.16.7",
-                        "@babel/template": "^7.16.7",
-                        "@babel/traverse": "^7.17.3",
-                        "@babel/types": "^7.17.0"
-                    }
-                },
-                "@babel/helper-plugin-utils": {
-                    "version": "7.16.7",
-                    "bundled": true,
-                    "dev": true
-                },
-                "@babel/helper-simple-access": {
-                    "version": "7.17.7",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@babel/types": "^7.17.0"
-                    }
-                },
-                "@babel/helper-split-export-declaration": {
-                    "version": "7.16.7",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@babel/types": "^7.16.7"
-                    }
-                },
-                "@babel/helper-validator-identifier": {
-                    "version": "7.16.7",
-                    "bundled": true,
-                    "dev": true
-                },
-                "@babel/helper-validator-option": {
-                    "version": "7.16.7",
-                    "bundled": true,
-                    "dev": true
-                },
-                "@babel/helpers": {
-                    "version": "7.17.8",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@babel/template": "^7.16.7",
-                        "@babel/traverse": "^7.17.3",
-                        "@babel/types": "^7.17.0"
-                    }
-                },
-                "@babel/highlight": {
-                    "version": "7.16.10",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@babel/helper-validator-identifier": "^7.16.7",
-                        "chalk": "^2.0.0",
-                        "js-tokens": "^4.0.0"
-                    }
-                },
-                "@babel/parser": {
-                    "version": "7.17.8",
-                    "bundled": true,
-                    "dev": true
-                },
-                "@babel/plugin-proposal-object-rest-spread": {
-                    "version": "7.17.3",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@babel/compat-data": "^7.17.0",
-                        "@babel/helper-compilation-targets": "^7.16.7",
-                        "@babel/helper-plugin-utils": "^7.16.7",
-                        "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
-                        "@babel/plugin-transform-parameters": "^7.16.7"
-                    }
-                },
-                "@babel/plugin-syntax-jsx": {
-                    "version": "7.16.7",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@babel/helper-plugin-utils": "^7.16.7"
-                    }
-                },
-                "@babel/plugin-syntax-object-rest-spread": {
-                    "version": "7.8.3",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@babel/helper-plugin-utils": "^7.8.0"
-                    }
-                },
-                "@babel/plugin-transform-destructuring": {
-                    "version": "7.17.7",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@babel/helper-plugin-utils": "^7.16.7"
-                    }
-                },
-                "@babel/plugin-transform-parameters": {
-                    "version": "7.16.7",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@babel/helper-plugin-utils": "^7.16.7"
-                    }
-                },
-                "@babel/plugin-transform-react-jsx": {
-                    "version": "7.17.3",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@babel/helper-annotate-as-pure": "^7.16.7",
-                        "@babel/helper-module-imports": "^7.16.7",
-                        "@babel/helper-plugin-utils": "^7.16.7",
-                        "@babel/plugin-syntax-jsx": "^7.16.7",
-                        "@babel/types": "^7.17.0"
-                    }
-                },
-                "@babel/template": {
-                    "version": "7.16.7",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@babel/code-frame": "^7.16.7",
-                        "@babel/parser": "^7.16.7",
-                        "@babel/types": "^7.16.7"
-                    }
-                },
-                "@babel/traverse": {
-                    "version": "7.17.3",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@babel/code-frame": "^7.16.7",
-                        "@babel/generator": "^7.17.3",
-                        "@babel/helper-environment-visitor": "^7.16.7",
-                        "@babel/helper-function-name": "^7.16.7",
-                        "@babel/helper-hoist-variables": "^7.16.7",
-                        "@babel/helper-split-export-declaration": "^7.16.7",
-                        "@babel/parser": "^7.17.3",
-                        "@babel/types": "^7.17.0",
-                        "debug": "^4.1.0",
-                        "globals": "^11.1.0"
-                    }
-                },
-                "@babel/types": {
-                    "version": "7.17.0",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@babel/helper-validator-identifier": "^7.16.7",
-                        "to-fast-properties": "^2.0.0"
-                    }
-                },
-                "@isaacs/import-jsx": {
-                    "version": "4.0.1",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@babel/core": "^7.5.5",
-                        "@babel/plugin-proposal-object-rest-spread": "^7.5.5",
-                        "@babel/plugin-transform-destructuring": "^7.5.0",
-                        "@babel/plugin-transform-react-jsx": "^7.3.0",
-                        "caller-path": "^3.0.1",
-                        "find-cache-dir": "^3.2.0",
-                        "make-dir": "^3.0.2",
-                        "resolve-from": "^3.0.0",
-                        "rimraf": "^3.0.0"
-                    }
-                },
-                "@jridgewell/resolve-uri": {
-                    "version": "3.0.5",
-                    "bundled": true,
-                    "dev": true
-                },
-                "@jridgewell/sourcemap-codec": {
-                    "version": "1.4.11",
-                    "bundled": true,
-                    "dev": true
-                },
-                "@jridgewell/trace-mapping": {
-                    "version": "0.3.4",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@jridgewell/resolve-uri": "^3.0.3",
-                        "@jridgewell/sourcemap-codec": "^1.4.10"
-                    }
-                },
-                "@types/prop-types": {
-                    "version": "15.7.4",
-                    "bundled": true,
-                    "dev": true
-                },
-                "@types/react": {
-                    "version": "17.0.52",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@types/prop-types": "*",
-                        "@types/scheduler": "*",
-                        "csstype": "^3.0.2"
-                    }
-                },
-                "@types/scheduler": {
-                    "version": "0.16.2",
-                    "bundled": true,
-                    "dev": true
-                },
-                "@types/yoga-layout": {
-                    "version": "1.9.2",
-                    "bundled": true,
-                    "dev": true
-                },
-                "ansi-escapes": {
-                    "version": "4.3.2",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "type-fest": "^0.21.3"
-                    },
-                    "dependencies": {
-                        "type-fest": {
-                            "version": "0.21.3",
-                            "bundled": true,
-                            "dev": true
-                        }
-                    }
-                },
-                "ansi-regex": {
-                    "version": "5.0.1",
-                    "bundled": true,
-                    "dev": true
-                },
-                "ansi-styles": {
-                    "version": "3.2.1",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "color-convert": "^1.9.0"
-                    }
-                },
-                "ansicolors": {
-                    "version": "0.3.2",
-                    "bundled": true,
-                    "dev": true
-                },
-                "astral-regex": {
-                    "version": "2.0.0",
-                    "bundled": true,
-                    "dev": true
-                },
-                "auto-bind": {
-                    "version": "4.0.0",
-                    "bundled": true,
-                    "dev": true
-                },
-                "balanced-match": {
-                    "version": "1.0.2",
-                    "bundled": true,
-                    "dev": true
-                },
                 "brace-expansion": {
-                    "version": "1.1.11",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "balanced-match": "^1.0.0",
-                        "concat-map": "0.0.1"
-                    }
-                },
-                "browserslist": {
-                    "version": "4.20.2",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "caniuse-lite": "^1.0.30001317",
-                        "electron-to-chromium": "^1.4.84",
-                        "escalade": "^3.1.1",
-                        "node-releases": "^2.0.2",
-                        "picocolors": "^1.0.0"
-                    }
-                },
-                "caller-callsite": {
-                    "version": "4.1.0",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "callsites": "^3.1.0"
-                    }
-                },
-                "caller-path": {
-                    "version": "3.0.1",
-                    "bundled": true,
+                    "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": {
-                        "caller-callsite": "^4.1.0"
+                        "balanced-match": "^1.0.0"
                     }
                 },
-                "callsites": {
-                    "version": "3.1.0",
-                    "bundled": true,
-                    "dev": true
-                },
-                "caniuse-lite": {
-                    "version": "1.0.30001319",
-                    "bundled": true,
-                    "dev": true
-                },
-                "cardinal": {
-                    "version": "2.1.1",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "ansicolors": "~0.3.2",
-                        "redeyed": "~2.1.0"
-                    }
-                },
-                "chalk": {
-                    "version": "2.4.2",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "ansi-styles": "^3.2.1",
-                        "escape-string-regexp": "^1.0.5",
-                        "supports-color": "^5.3.0"
-                    }
-                },
-                "ci-info": {
-                    "version": "2.0.0",
-                    "bundled": true,
-                    "dev": true
-                },
-                "cli-boxes": {
-                    "version": "2.2.1",
-                    "bundled": true,
-                    "dev": true
-                },
-                "cli-cursor": {
-                    "version": "3.1.0",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "restore-cursor": "^3.1.0"
-                    }
-                },
-                "cli-truncate": {
-                    "version": "2.1.0",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "slice-ansi": "^3.0.0",
-                        "string-width": "^4.2.0"
-                    }
-                },
-                "code-excerpt": {
-                    "version": "3.0.0",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "convert-to-spaces": "^1.0.1"
-                    }
-                },
-                "color-convert": {
-                    "version": "1.9.3",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "color-name": "1.1.3"
-                    }
-                },
-                "color-name": {
-                    "version": "1.1.3",
-                    "bundled": true,
-                    "dev": true
-                },
-                "commondir": {
-                    "version": "1.0.1",
-                    "bundled": true,
-                    "dev": true
-                },
-                "concat-map": {
-                    "version": "0.0.1",
-                    "bundled": true,
-                    "dev": true
-                },
-                "convert-source-map": {
-                    "version": "1.8.0",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "safe-buffer": "~5.1.1"
-                    }
-                },
-                "convert-to-spaces": {
-                    "version": "1.0.2",
-                    "bundled": true,
-                    "dev": true
-                },
-                "csstype": {
-                    "version": "3.0.11",
-                    "bundled": true,
-                    "dev": true
-                },
-                "debug": {
-                    "version": "4.3.4",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "ms": "2.1.2"
-                    }
-                },
-                "electron-to-chromium": {
-                    "version": "1.4.89",
-                    "bundled": true,
-                    "dev": true
-                },
-                "emoji-regex": {
-                    "version": "8.0.0",
-                    "bundled": true,
-                    "dev": true
-                },
-                "escalade": {
-                    "version": "3.1.1",
-                    "bundled": true,
-                    "dev": true
-                },
-                "escape-string-regexp": {
-                    "version": "1.0.5",
-                    "bundled": true,
-                    "dev": true
-                },
-                "esprima": {
-                    "version": "4.0.1",
-                    "bundled": true,
-                    "dev": true
-                },
-                "events-to-array": {
-                    "version": "1.1.2",
-                    "bundled": true,
-                    "dev": true
-                },
-                "find-cache-dir": {
-                    "version": "3.3.2",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "commondir": "^1.0.1",
-                        "make-dir": "^3.0.2",
-                        "pkg-dir": "^4.1.0"
-                    }
-                },
-                "find-up": {
-                    "version": "4.1.0",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "locate-path": "^5.0.0",
-                        "path-exists": "^4.0.0"
-                    }
-                },
-                "fs.realpath": {
-                    "version": "1.0.0",
-                    "bundled": true,
-                    "dev": true
-                },
-                "gensync": {
-                    "version": "1.0.0-beta.2",
-                    "bundled": true,
-                    "dev": true
-                },
                 "glob": {
-                    "version": "7.2.3",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "fs.realpath": "^1.0.0",
-                        "inflight": "^1.0.4",
-                        "inherits": "2",
-                        "minimatch": "^3.1.1",
-                        "once": "^1.3.0",
-                        "path-is-absolute": "^1.0.0"
-                    }
-                },
-                "globals": {
-                    "version": "11.12.0",
-                    "bundled": true,
-                    "dev": true
-                },
-                "has-flag": {
-                    "version": "3.0.0",
-                    "bundled": true,
-                    "dev": true
-                },
-                "indent-string": {
-                    "version": "4.0.0",
-                    "bundled": true,
-                    "dev": true
-                },
-                "inflight": {
-                    "version": "1.0.6",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "once": "^1.3.0",
-                        "wrappy": "1"
-                    }
-                },
-                "inherits": {
-                    "version": "2.0.4",
-                    "bundled": true,
-                    "dev": true
-                },
-                "ink": {
-                    "version": "3.2.0",
-                    "bundled": true,
+                    "version": "10.3.10",
+                    "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+                    "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
                     "dev": true,
                     "requires": {
-                        "ansi-escapes": "^4.2.1",
-                        "auto-bind": "4.0.0",
-                        "chalk": "^4.1.0",
-                        "cli-boxes": "^2.2.0",
-                        "cli-cursor": "^3.1.0",
-                        "cli-truncate": "^2.1.0",
-                        "code-excerpt": "^3.0.0",
-                        "indent-string": "^4.0.0",
-                        "is-ci": "^2.0.0",
-                        "lodash": "^4.17.20",
-                        "patch-console": "^1.0.0",
-                        "react-devtools-core": "^4.19.1",
-                        "react-reconciler": "^0.26.2",
-                        "scheduler": "^0.20.2",
-                        "signal-exit": "^3.0.2",
-                        "slice-ansi": "^3.0.0",
-                        "stack-utils": "^2.0.2",
-                        "string-width": "^4.2.2",
-                        "type-fest": "^0.12.0",
-                        "widest-line": "^3.1.0",
-                        "wrap-ansi": "^6.2.0",
-                        "ws": "^7.5.5",
-                        "yoga-layout-prebuilt": "^1.9.6"
-                    },
-                    "dependencies": {
-                        "ansi-styles": {
-                            "version": "4.3.0",
-                            "bundled": true,
-                            "dev": true,
-                            "requires": {
-                                "color-convert": "^2.0.1"
-                            }
-                        },
-                        "chalk": {
-                            "version": "4.1.2",
-                            "bundled": true,
-                            "dev": true,
-                            "requires": {
-                                "ansi-styles": "^4.1.0",
-                                "supports-color": "^7.1.0"
-                            }
-                        },
-                        "color-convert": {
-                            "version": "2.0.1",
-                            "bundled": true,
-                            "dev": true,
-                            "requires": {
-                                "color-name": "~1.1.4"
-                            }
-                        },
-                        "color-name": {
-                            "version": "1.1.4",
-                            "bundled": true,
-                            "dev": true
-                        },
-                        "has-flag": {
-                            "version": "4.0.0",
-                            "bundled": true,
-                            "dev": true
-                        },
-                        "supports-color": {
-                            "version": "7.2.0",
-                            "bundled": true,
-                            "dev": true,
-                            "requires": {
-                                "has-flag": "^4.0.0"
-                            }
-                        }
+                        "foreground-child": "^3.1.0",
+                        "jackspeak": "^2.3.5",
+                        "minimatch": "^9.0.1",
+                        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+                        "path-scurry": "^1.10.1"
                     }
                 },
-                "is-ci": {
-                    "version": "2.0.0",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "ci-info": "^2.0.0"
-                    }
-                },
-                "is-fullwidth-code-point": {
-                    "version": "3.0.0",
-                    "bundled": true,
-                    "dev": true
-                },
-                "js-tokens": {
-                    "version": "4.0.0",
-                    "bundled": true,
-                    "dev": true
-                },
-                "jsesc": {
-                    "version": "2.5.2",
-                    "bundled": true,
-                    "dev": true
-                },
-                "json5": {
-                    "version": "2.2.3",
-                    "bundled": true,
-                    "dev": true
-                },
-                "locate-path": {
-                    "version": "5.0.0",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "p-locate": "^4.1.0"
-                    }
-                },
-                "lodash": {
-                    "version": "4.17.21",
-                    "bundled": true,
-                    "dev": true
-                },
-                "loose-envify": {
-                    "version": "1.4.0",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "js-tokens": "^3.0.0 || ^4.0.0"
-                    }
-                },
-                "make-dir": {
-                    "version": "3.1.0",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "semver": "^6.0.0"
-                    }
-                },
-                "mimic-fn": {
-                    "version": "2.1.0",
-                    "bundled": true,
-                    "dev": true
-                },
                 "minimatch": {
-                    "version": "3.1.2",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "brace-expansion": "^1.1.7"
-                    }
-                },
-                "minipass": {
-                    "version": "3.3.4",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "yallist": "^4.0.0"
-                    }
-                },
-                "ms": {
-                    "version": "2.1.2",
-                    "bundled": true,
-                    "dev": true
-                },
-                "node-releases": {
-                    "version": "2.0.2",
-                    "bundled": true,
-                    "dev": true
-                },
-                "object-assign": {
-                    "version": "4.1.1",
-                    "bundled": true,
-                    "dev": true
-                },
-                "once": {
-                    "version": "1.4.0",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "wrappy": "1"
-                    }
-                },
-                "onetime": {
-                    "version": "5.1.2",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "mimic-fn": "^2.1.0"
-                    }
-                },
-                "p-limit": {
-                    "version": "2.3.0",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "p-try": "^2.0.0"
-                    }
-                },
-                "p-locate": {
-                    "version": "4.1.0",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "p-limit": "^2.2.0"
-                    }
-                },
-                "p-try": {
-                    "version": "2.2.0",
-                    "bundled": true,
-                    "dev": true
-                },
-                "patch-console": {
-                    "version": "1.0.0",
-                    "bundled": true,
-                    "dev": true
-                },
-                "path-exists": {
-                    "version": "4.0.0",
-                    "bundled": true,
-                    "dev": true
-                },
-                "path-is-absolute": {
-                    "version": "1.0.1",
-                    "bundled": true,
-                    "dev": true
-                },
-                "picocolors": {
-                    "version": "1.0.0",
-                    "bundled": true,
-                    "dev": true
-                },
-                "pkg-dir": {
-                    "version": "4.2.0",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "find-up": "^4.0.0"
-                    }
-                },
-                "punycode": {
-                    "version": "2.1.1",
-                    "bundled": true,
-                    "dev": true
-                },
-                "react": {
-                    "version": "17.0.2",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "loose-envify": "^1.1.0",
-                        "object-assign": "^4.1.1"
-                    }
-                },
-                "react-devtools-core": {
-                    "version": "4.24.1",
-                    "bundled": true,
+                    "version": "9.0.3",
+                    "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+                    "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
                     "dev": true,
                     "requires": {
-                        "shell-quote": "^1.6.1",
-                        "ws": "^7"
-                    }
-                },
-                "react-reconciler": {
-                    "version": "0.26.2",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "loose-envify": "^1.1.0",
-                        "object-assign": "^4.1.1",
-                        "scheduler": "^0.20.2"
-                    }
-                },
-                "redeyed": {
-                    "version": "2.1.1",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "esprima": "~4.0.0"
-                    }
-                },
-                "resolve-from": {
-                    "version": "3.0.0",
-                    "bundled": true,
-                    "dev": true
-                },
-                "restore-cursor": {
-                    "version": "3.1.0",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "onetime": "^5.1.0",
-                        "signal-exit": "^3.0.2"
+                        "brace-expansion": "^2.0.1"
                     }
                 },
                 "rimraf": {
-                    "version": "3.0.2",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "glob": "^7.1.3"
-                    }
-                },
-                "safe-buffer": {
-                    "version": "5.1.2",
-                    "bundled": true,
-                    "dev": true
-                },
-                "scheduler": {
-                    "version": "0.20.2",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "loose-envify": "^1.1.0",
-                        "object-assign": "^4.1.1"
-                    }
-                },
-                "semver": {
-                    "version": "6.3.0",
-                    "bundled": true,
-                    "dev": true
-                },
-                "shell-quote": {
-                    "version": "1.7.3",
-                    "bundled": true,
-                    "dev": true
-                },
-                "signal-exit": {
-                    "version": "3.0.7",
-                    "bundled": true,
-                    "dev": true
-                },
-                "slice-ansi": {
-                    "version": "3.0.0",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "ansi-styles": "^4.0.0",
-                        "astral-regex": "^2.0.0",
-                        "is-fullwidth-code-point": "^3.0.0"
-                    },
-                    "dependencies": {
-                        "ansi-styles": {
-                            "version": "4.3.0",
-                            "bundled": true,
-                            "dev": true,
-                            "requires": {
-                                "color-convert": "^2.0.1"
-                            }
-                        },
-                        "color-convert": {
-                            "version": "2.0.1",
-                            "bundled": true,
-                            "dev": true,
-                            "requires": {
-                                "color-name": "~1.1.4"
-                            }
-                        },
-                        "color-name": {
-                            "version": "1.1.4",
-                            "bundled": true,
-                            "dev": true
-                        }
-                    }
-                },
-                "source-map": {
-                    "version": "0.5.7",
-                    "bundled": true,
-                    "dev": true
-                },
-                "stack-utils": {
-                    "version": "2.0.5",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "escape-string-regexp": "^2.0.0"
-                    },
-                    "dependencies": {
-                        "escape-string-regexp": {
-                            "version": "2.0.0",
-                            "bundled": true,
-                            "dev": true
-                        }
-                    }
-                },
-                "string-width": {
-                    "version": "4.2.3",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "emoji-regex": "^8.0.0",
-                        "is-fullwidth-code-point": "^3.0.0",
-                        "strip-ansi": "^6.0.1"
-                    }
-                },
-                "strip-ansi": {
-                    "version": "6.0.1",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "ansi-regex": "^5.0.1"
-                    }
-                },
-                "supports-color": {
-                    "version": "5.5.0",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "has-flag": "^3.0.0"
-                    }
-                },
-                "tap-parser": {
-                    "version": "11.0.2",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "events-to-array": "^1.0.1",
-                        "minipass": "^3.1.6",
-                        "tap-yaml": "^1.0.0"
-                    }
-                },
-                "tap-yaml": {
-                    "version": "1.0.2",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "yaml": "^1.10.2"
-                    }
-                },
-                "tcompare": {
-                    "version": "5.0.7",
-                    "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-5.0.7.tgz",
-                    "integrity": "sha512-d9iddt6YYGgyxJw5bjsN7UJUO1kGOtjSlNy/4PoGYAjQS5pAT/hzIoLf1bZCw+uUxRmZJh7Yy1aA7xKVRT9B4w==",
-                    "dev": true,
-                    "requires": {
-                        "diff": "^4.0.2"
-                    }
-                },
-                "to-fast-properties": {
-                    "version": "2.0.0",
-                    "bundled": true,
-                    "dev": true
-                },
-                "treport": {
-                    "version": "3.0.4",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@isaacs/import-jsx": "^4.0.1",
-                        "cardinal": "^2.1.1",
-                        "chalk": "^3.0.0",
-                        "ink": "^3.2.0",
-                        "ms": "^2.1.2",
-                        "tap-parser": "^11.0.0",
-                        "tap-yaml": "^1.0.0",
-                        "unicode-length": "^2.0.2"
-                    },
-                    "dependencies": {
-                        "ansi-styles": {
-                            "version": "4.3.0",
-                            "bundled": true,
-                            "dev": true,
-                            "requires": {
-                                "color-convert": "^2.0.1"
-                            }
-                        },
-                        "chalk": {
-                            "version": "3.0.0",
-                            "bundled": true,
-                            "dev": true,
-                            "requires": {
-                                "ansi-styles": "^4.1.0",
-                                "supports-color": "^7.1.0"
-                            }
-                        },
-                        "color-convert": {
-                            "version": "2.0.1",
-                            "bundled": true,
-                            "dev": true,
-                            "requires": {
-                                "color-name": "~1.1.4"
-                            }
-                        },
-                        "color-name": {
-                            "version": "1.1.4",
-                            "bundled": true,
-                            "dev": true
-                        },
-                        "has-flag": {
-                            "version": "4.0.0",
-                            "bundled": true,
-                            "dev": true
-                        },
-                        "supports-color": {
-                            "version": "7.2.0",
-                            "bundled": true,
-                            "dev": true,
-                            "requires": {
-                                "has-flag": "^4.0.0"
-                            }
-                        }
-                    }
-                },
-                "type-fest": {
-                    "version": "0.12.0",
-                    "bundled": true,
-                    "dev": true
-                },
-                "unicode-length": {
-                    "version": "2.0.2",
-                    "bundled": true,
+                    "version": "5.0.5",
+                    "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz",
+                    "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==",
                     "dev": true,
                     "requires": {
-                        "punycode": "^2.0.0",
-                        "strip-ansi": "^3.0.1"
-                    },
-                    "dependencies": {
-                        "ansi-regex": {
-                            "version": "2.1.1",
-                            "bundled": true,
-                            "dev": true
-                        },
-                        "strip-ansi": {
-                            "version": "3.0.1",
-                            "bundled": true,
-                            "dev": true,
-                            "requires": {
-                                "ansi-regex": "^2.0.0"
-                            }
-                        }
-                    }
-                },
-                "widest-line": {
-                    "version": "3.1.0",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "string-width": "^4.0.0"
-                    }
-                },
-                "wrap-ansi": {
-                    "version": "6.2.0",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "ansi-styles": "^4.0.0",
-                        "string-width": "^4.1.0",
-                        "strip-ansi": "^6.0.0"
-                    },
-                    "dependencies": {
-                        "ansi-styles": {
-                            "version": "4.3.0",
-                            "bundled": true,
-                            "dev": true,
-                            "requires": {
-                                "color-convert": "^2.0.1"
-                            }
-                        },
-                        "color-convert": {
-                            "version": "2.0.1",
-                            "bundled": true,
-                            "dev": true,
-                            "requires": {
-                                "color-name": "~1.1.4"
-                            }
-                        },
-                        "color-name": {
-                            "version": "1.1.4",
-                            "bundled": true,
-                            "dev": true
-                        }
-                    }
-                },
-                "wrappy": {
-                    "version": "1.0.2",
-                    "bundled": true,
-                    "dev": true
-                },
-                "ws": {
-                    "version": "7.5.7",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {}
-                },
-                "yallist": {
-                    "version": "4.0.0",
-                    "bundled": true,
-                    "dev": true
-                },
-                "yaml": {
-                    "version": "1.10.2",
-                    "bundled": true,
-                    "dev": true
-                },
-                "yoga-layout-prebuilt": {
-                    "version": "1.10.0",
-                    "bundled": true,
-                    "dev": true,
-                    "requires": {
-                        "@types/yoga-layout": "1.9.2"
+                        "glob": "^10.3.7"
                     }
                 }
             }
         },
-        "tap-mocha-reporter": {
-            "version": "5.0.3",
-            "resolved": "https://registry.npmjs.org/tap-mocha-reporter/-/tap-mocha-reporter-5.0.3.tgz",
-            "integrity": "sha512-6zlGkaV4J+XMRFkN0X+yuw6xHbE9jyCZ3WUKfw4KxMyRGOpYSRuuQTRJyWX88WWuLdVTuFbxzwXhXuS2XE6o0g==",
+        "tap": {
+            "version": "18.4.0",
+            "resolved": "https://registry.npmjs.org/tap/-/tap-18.4.0.tgz",
+            "integrity": "sha512-42bqz0KpoDg8F6Gs5zrTVOELq5ShaK86rCsRG6C6uJM7nUANCB3GW9Dmvy3BGHRll4wAwr+SA+iM0tvBQtrilg==",
             "dev": true,
             "requires": {
-                "color-support": "^1.1.0",
-                "debug": "^4.1.1",
-                "diff": "^4.0.1",
-                "escape-string-regexp": "^2.0.0",
-                "glob": "^7.0.5",
-                "tap-parser": "^11.0.0",
-                "tap-yaml": "^1.0.0",
-                "unicode-length": "^2.0.2"
-            },
-            "dependencies": {
-                "escape-string-regexp": {
-                    "version": "2.0.0",
-                    "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
-                    "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
-                    "dev": true
-                }
+                "@tapjs/after": "1.1.4",
+                "@tapjs/after-each": "1.1.4",
+                "@tapjs/asserts": "1.1.4",
+                "@tapjs/before": "1.1.4",
+                "@tapjs/before-each": "1.1.4",
+                "@tapjs/core": "1.3.4",
+                "@tapjs/filter": "1.2.4",
+                "@tapjs/fixture": "1.2.4",
+                "@tapjs/intercept": "1.2.4",
+                "@tapjs/mock": "1.2.2",
+                "@tapjs/node-serialize": "1.1.4",
+                "@tapjs/run": "1.4.0",
+                "@tapjs/snapshot": "1.2.4",
+                "@tapjs/spawn": "1.1.4",
+                "@tapjs/stdin": "1.1.4",
+                "@tapjs/test": "1.3.4",
+                "@tapjs/typescript": "1.2.4",
+                "@tapjs/worker": "1.1.4"
             }
         },
         "tap-parser": {
-            "version": "11.0.2",
-            "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-11.0.2.tgz",
-            "integrity": "sha512-6qGlC956rcORw+fg7Fv1iCRAY8/bU9UabUAhs3mXRH6eRmVZcNPLheSXCYaVaYeSwx5xa/1HXZb1537YSvwDZg==",
+            "version": "15.2.0",
+            "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-15.2.0.tgz",
+            "integrity": "sha512-bDBR7cuVLfsmmc7ruerZXVBlDtJwqqWzqlO9BFNgw6gprpzjnjyfdc+fsW6mNUYSoxdVEeY7NFgrgGa81EuQ5w==",
             "dev": true,
             "requires": {
-                "events-to-array": "^1.0.1",
-                "minipass": "^3.1.6",
-                "tap-yaml": "^1.0.0"
+                "events-to-array": "^2.0.3",
+                "tap-yaml": "2.2.0"
             }
         },
         "tap-yaml": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/tap-yaml/-/tap-yaml-1.0.2.tgz",
-            "integrity": "sha512-GegASpuqBnRNdT1U+yuUPZ8rEU64pL35WPBpCISWwff4dErS2/438barz7WFJl4Nzh3Y05tfPidZnH+GaV1wMg==",
+            "version": "2.2.0",
+            "resolved": "https://registry.npmjs.org/tap-yaml/-/tap-yaml-2.2.0.tgz",
+            "integrity": "sha512-o8I7WDNiGpuF04tGAVaNYY5rX9waCtqw9A7Y0YVSQBGcFwNUJWUPLkr2lbhgLRTxc+Tpnw4xUXlIanZc+ZAGnw==",
             "dev": true,
             "requires": {
-                "yaml": "^1.10.2"
+                "yaml": "^2.3.0",
+                "yaml-types": "^0.3.0"
+            }
+        },
+        "tar": {
+            "version": "6.2.0",
+            "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
+            "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
+            "dev": true,
+            "requires": {
+                "chownr": "^2.0.0",
+                "fs-minipass": "^2.0.0",
+                "minipass": "^5.0.0",
+                "minizlib": "^2.1.1",
+                "mkdirp": "^1.0.3",
+                "yallist": "^4.0.0"
+            },
+            "dependencies": {
+                "fs-minipass": {
+                    "version": "2.1.0",
+                    "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+                    "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+                    "dev": true,
+                    "requires": {
+                        "minipass": "^3.0.0"
+                    },
+                    "dependencies": {
+                        "minipass": {
+                            "version": "3.3.6",
+                            "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+                            "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+                            "dev": true,
+                            "requires": {
+                                "yallist": "^4.0.0"
+                            }
+                        }
+                    }
+                },
+                "minipass": {
+                    "version": "5.0.0",
+                    "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
+                    "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+                    "dev": true
+                },
+                "mkdirp": {
+                    "version": "1.0.4",
+                    "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+                    "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+                    "dev": true
+                }
             }
         },
         "tcompare": {
-            "version": "6.0.0",
-            "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-6.0.0.tgz",
-            "integrity": "sha512-JeX89lSVkxTzYND0LxzFCGrXm/TqGEQ0heu1JTwplnpaYQNky6hIaO4lQBOrs+/P787i3CoK9T/O3/oEcnJXvA==",
+            "version": "6.4.0",
+            "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-6.4.0.tgz",
+            "integrity": "sha512-MR0TPvFaEQ53jgMP43aHr3wKGKKPi6Th3nxHoIsBVL0AxjKdfyrIIWvYt7u30NNs57Vc6UP5ooq/sD69IhQPzw==",
             "dev": true,
             "requires": {
-                "diff": "^5.1.0"
+                "diff": "^5.1.0",
+                "react-element-to-jsx-string": "^15.0.0"
             },
             "dependencies": {
                 "diff": {
@@ -8226,12 +9358,6 @@
             "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
             "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
         },
-        "to-fast-properties": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
-            "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
-            "dev": true
-        },
         "to-regex-range": {
             "version": "5.0.1",
             "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -8242,11 +9368,132 @@
             }
         },
         "trivial-deferred": {
-            "version": "1.1.2",
-            "resolved": "https://registry.npmjs.org/trivial-deferred/-/trivial-deferred-1.1.2.tgz",
-            "integrity": "sha512-vDPiDBC3hyP6O4JrJYMImW3nl3c03Tsj9fEXc7Qc/XKa1O7gf5ZtFfIR/E0dun9SnDHdwjna1Z2rSzYgqpxh/g==",
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/trivial-deferred/-/trivial-deferred-2.0.0.tgz",
+            "integrity": "sha512-iGbM7X2slv9ORDVj2y2FFUq3cP/ypbtu2nQ8S38ufjL0glBABvmR9pTdsib1XtS2LUhhLMbelaBUaf/s5J3dSw==",
             "dev": true
         },
+        "ts-node": {
+            "version": "npm:@isaacs/ts-node-temp-fork-for-pr-2009@10.9.1",
+            "resolved": "https://registry.npmjs.org/@isaacs/ts-node-temp-fork-for-pr-2009/-/ts-node-temp-fork-for-pr-2009-10.9.1.tgz",
+            "integrity": "sha512-MY4rUonz835NsTbd4dcgKZvZFYX9IkLnYFZV9M7GQV8t39fawafLin/Qw6VXD4yfMs4HcBq8P3ddeU0QHMH1YQ==",
+            "dev": true,
+            "requires": {
+                "@cspotcode/source-map-support": "^0.8.0",
+                "@tsconfig/node14": "*",
+                "@tsconfig/node16": "*",
+                "@tsconfig/node18": "*",
+                "@tsconfig/node20": "*",
+                "acorn": "^8.4.1",
+                "acorn-walk": "^8.1.1",
+                "arg": "^4.1.0",
+                "diff": "^4.0.1",
+                "make-error": "^1.1.1",
+                "v8-compile-cache-lib": "^3.0.1"
+            }
+        },
+        "tshy": {
+            "version": "1.2.2",
+            "resolved": "https://registry.npmjs.org/tshy/-/tshy-1.2.2.tgz",
+            "integrity": "sha512-y5ItK4DKLYO+hba7h5sOaCYygNtF44qytZGyjZSE6CQSVfzUfZ2qn/GmXu737amwfCKG9EizPw3oPBWrisF1uw==",
+            "dev": true,
+            "requires": {
+                "chalk": "^5.3.0",
+                "foreground-child": "^3.1.1",
+                "mkdirp": "^3.0.1",
+                "resolve-import": "^1.4.1",
+                "rimraf": "^5.0.1",
+                "sync-content": "^1.0.2",
+                "typescript": "5.2",
+                "walk-up-path": "^3.0.1"
+            },
+            "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"
+                    }
+                },
+                "chalk": {
+                    "version": "5.3.0",
+                    "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
+                    "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
+                    "dev": true
+                },
+                "glob": {
+                    "version": "10.3.10",
+                    "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+                    "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
+                    "dev": true,
+                    "requires": {
+                        "foreground-child": "^3.1.0",
+                        "jackspeak": "^2.3.5",
+                        "minimatch": "^9.0.1",
+                        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+                        "path-scurry": "^1.10.1"
+                    }
+                },
+                "minimatch": {
+                    "version": "9.0.3",
+                    "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+                    "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+                    "dev": true,
+                    "requires": {
+                        "brace-expansion": "^2.0.1"
+                    }
+                },
+                "rimraf": {
+                    "version": "5.0.5",
+                    "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz",
+                    "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==",
+                    "dev": true,
+                    "requires": {
+                        "glob": "^10.3.7"
+                    }
+                }
+            }
+        },
+        "tslib": {
+            "version": "2.6.2",
+            "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+            "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
+            "dev": true
+        },
+        "tuf-js": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.1.0.tgz",
+            "integrity": "sha512-eD7YPPjVlMzdggrOeE8zwoegUaG/rt6Bt3jwoQPunRiNVzgcCE009UDFJKJjG+Gk9wFu6W/Vi+P5d/5QpdD9jA==",
+            "dev": true,
+            "requires": {
+                "@tufjs/models": "2.0.0",
+                "debug": "^4.3.4",
+                "make-fetch-happen": "^13.0.0"
+            },
+            "dependencies": {
+                "make-fetch-happen": {
+                    "version": "13.0.0",
+                    "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.0.tgz",
+                    "integrity": "sha512-7ThobcL8brtGo9CavByQrQi+23aIfgYU++wg4B87AIS8Rb2ZBt/MEaDqzA00Xwv/jUjAjYkLHjVolYuTLKda2A==",
+                    "dev": true,
+                    "requires": {
+                        "@npmcli/agent": "^2.0.0",
+                        "cacache": "^18.0.0",
+                        "http-cache-semantics": "^4.1.1",
+                        "is-lambda": "^1.0.1",
+                        "minipass": "^7.0.2",
+                        "minipass-fetch": "^3.0.0",
+                        "minipass-flush": "^1.0.5",
+                        "minipass-pipeline": "^1.2.4",
+                        "negotiator": "^0.6.3",
+                        "promise-retry": "^2.0.1",
+                        "ssri": "^10.0.0"
+                    }
+                }
+            }
+        },
         "type-check": {
             "version": "0.4.0",
             "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -8260,32 +9507,28 @@
             "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
             "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="
         },
-        "typedarray-to-buffer": {
-            "version": "3.1.5",
-            "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
-            "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
-            "dev": true,
-            "requires": {
-                "is-typedarray": "^1.0.0"
-            }
+        "typescript": {
+            "version": "5.2.2",
+            "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
+            "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
+            "dev": true
         },
-        "unicode-length": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/unicode-length/-/unicode-length-2.1.0.tgz",
-            "integrity": "sha512-4bV582zTV9Q02RXBxSUMiuN/KHo5w4aTojuKTNT96DIKps/SIawFp7cS5Mu25VuY1AioGXrmYyzKZUzh8OqoUw==",
+        "unique-filename": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz",
+            "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==",
             "dev": true,
             "requires": {
-                "punycode": "^2.0.0"
+                "unique-slug": "^4.0.0"
             }
         },
-        "update-browserslist-db": {
-            "version": "1.0.10",
-            "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
-            "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==",
+        "unique-slug": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz",
+            "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==",
             "dev": true,
             "requires": {
-                "escalade": "^3.1.1",
-                "picocolors": "^1.0.0"
+                "imurmurhash": "^0.1.4"
             }
         },
         "uri-js": {
@@ -8296,12 +9539,72 @@
                 "punycode": "^2.1.0"
             }
         },
+        "util-deprecate": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+            "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+            "dev": true
+        },
         "uuid": {
             "version": "8.3.2",
             "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
             "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
             "dev": true
         },
+        "v8-compile-cache-lib": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+            "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
+            "dev": true
+        },
+        "v8-to-istanbul": {
+            "version": "9.1.0",
+            "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz",
+            "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==",
+            "dev": true,
+            "requires": {
+                "@jridgewell/trace-mapping": "^0.3.12",
+                "@types/istanbul-lib-coverage": "^2.0.1",
+                "convert-source-map": "^1.6.0"
+            },
+            "dependencies": {
+                "@jridgewell/trace-mapping": {
+                    "version": "0.3.19",
+                    "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz",
+                    "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==",
+                    "dev": true,
+                    "requires": {
+                        "@jridgewell/resolve-uri": "^3.1.0",
+                        "@jridgewell/sourcemap-codec": "^1.4.14"
+                    }
+                }
+            }
+        },
+        "validate-npm-package-license": {
+            "version": "3.0.4",
+            "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+            "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+            "dev": true,
+            "requires": {
+                "spdx-correct": "^3.0.0",
+                "spdx-expression-parse": "^3.0.0"
+            }
+        },
+        "validate-npm-package-name": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz",
+            "integrity": "sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==",
+            "dev": true,
+            "requires": {
+                "builtins": "^5.0.0"
+            }
+        },
+        "walk-up-path": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-3.0.1.tgz",
+            "integrity": "sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==",
+            "dev": true
+        },
         "which": {
             "version": "2.0.2",
             "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -8310,19 +9613,90 @@
                 "isexe": "^2.0.0"
             }
         },
-        "which-module": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
-            "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==",
-            "dev": true
+        "wide-align": {
+            "version": "1.1.5",
+            "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
+            "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
+            "dev": true,
+            "requires": {
+                "string-width": "^1.0.2 || 2 || 3 || 4"
+            },
+            "dependencies": {
+                "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
+                },
+                "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
+                },
+                "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",
+                        "strip-ansi": "^6.0.1"
+                    }
+                }
+            }
+        },
+        "widest-line": {
+            "version": "4.0.1",
+            "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz",
+            "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==",
+            "dev": true,
+            "requires": {
+                "string-width": "^5.0.1"
+            }
         },
         "word-wrap": {
-            "version": "1.2.3",
-            "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
-            "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ=="
+            "version": "1.2.5",
+            "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+            "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="
         },
         "wrap-ansi": {
-            "version": "7.0.0",
+            "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",
+                "strip-ansi": "^7.0.1"
+            },
+            "dependencies": {
+                "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
+                },
+                "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
+                },
+                "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"
+                    }
+                }
+            }
+        },
+        "wrap-ansi-cjs": {
+            "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,
@@ -8330,6 +9704,31 @@
                 "ansi-styles": "^4.0.0",
                 "string-width": "^4.1.0",
                 "strip-ansi": "^6.0.0"
+            },
+            "dependencies": {
+                "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
+                },
+                "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
+                },
+                "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",
+                        "strip-ansi": "^6.0.1"
+                    }
+                }
             }
         },
         "wrappy": {
@@ -8337,22 +9736,17 @@
             "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
             "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
         },
-        "write-file-atomic": {
-            "version": "3.0.3",
-            "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
-            "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
+        "ws": {
+            "version": "8.14.2",
+            "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
+            "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==",
             "dev": true,
-            "requires": {
-                "imurmurhash": "^0.1.4",
-                "is-typedarray": "^1.0.0",
-                "signal-exit": "^3.0.2",
-                "typedarray-to-buffer": "^3.1.5"
-            }
+            "requires": {}
         },
         "y18n": {
-            "version": "4.0.3",
-            "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
-            "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
+            "version": "5.0.8",
+            "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+            "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
             "dev": true
         },
         "yallist": {
@@ -8362,68 +9756,74 @@
             "dev": true
         },
         "yaml": {
-            "version": "1.10.2",
-            "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
-            "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+            "version": "2.3.2",
+            "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz",
+            "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==",
             "dev": true
         },
+        "yaml-types": {
+            "version": "0.3.0",
+            "resolved": "https://registry.npmjs.org/yaml-types/-/yaml-types-0.3.0.tgz",
+            "integrity": "sha512-i9RxAO/LZBiE0NJUy9pbN5jFz5EasYDImzRkj8Y81kkInTi1laia3P3K/wlMKzOxFQutZip8TejvQP/DwgbU7A==",
+            "dev": true,
+            "requires": {}
+        },
         "yargs": {
-            "version": "15.4.1",
-            "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
-            "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+            "version": "17.7.2",
+            "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+            "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
             "dev": true,
             "requires": {
-                "cliui": "^6.0.0",
-                "decamelize": "^1.2.0",
-                "find-up": "^4.1.0",
-                "get-caller-file": "^2.0.1",
+                "cliui": "^8.0.1",
+                "escalade": "^3.1.1",
+                "get-caller-file": "^2.0.5",
                 "require-directory": "^2.1.1",
-                "require-main-filename": "^2.0.0",
-                "set-blocking": "^2.0.0",
-                "string-width": "^4.2.0",
-                "which-module": "^2.0.0",
-                "y18n": "^4.0.0",
-                "yargs-parser": "^18.1.2"
+                "string-width": "^4.2.3",
+                "y18n": "^5.0.5",
+                "yargs-parser": "^21.1.1"
             },
             "dependencies": {
-                "cliui": {
-                    "version": "6.0.0",
-                    "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
-                    "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
-                    "dev": true,
-                    "requires": {
-                        "string-width": "^4.2.0",
-                        "strip-ansi": "^6.0.0",
-                        "wrap-ansi": "^6.2.0"
-                    }
+                "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
                 },
-                "wrap-ansi": {
-                    "version": "6.2.0",
-                    "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
-                    "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+                "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
+                },
+                "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": {
-                        "ansi-styles": "^4.0.0",
-                        "string-width": "^4.1.0",
-                        "strip-ansi": "^6.0.0"
+                        "emoji-regex": "^8.0.0",
+                        "is-fullwidth-code-point": "^3.0.0",
+                        "strip-ansi": "^6.0.1"
                     }
                 }
             }
         },
         "yargs-parser": {
-            "version": "18.1.3",
-            "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
-            "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
-            "dev": true,
-            "requires": {
-                "camelcase": "^5.0.0",
-                "decamelize": "^1.2.0"
-            }
+            "version": "21.1.1",
+            "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+            "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+            "dev": true
         },
         "yocto-queue": {
             "version": "0.1.0",
             "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
             "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
+        },
+        "yoga-wasm-web": {
+            "version": "0.3.3",
+            "resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz",
+            "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==",
+            "dev": true
         }
     }
 }
diff --git a/package.json b/package.json
index 02789a7..194c406 100644
--- a/package.json
+++ b/package.json
@@ -8,11 +8,19 @@
         "hsmusic": "./src/upd8.js"
     },
     "scripts": {
-        "test": "tap 'test/snapshot/*.js' 'test/unit/**/*.js'",
+        "test": "tap",
         "dev": "eslint src && node src/upd8.js"
     },
     "imports": {
         "#colors": "./src/util/colors.js",
+        "#composite": "./src/data/things/composite.js",
+        "#composite/control-flow": "./src/data/composite/control-flow/index.js",
+        "#composite/data": "./src/data/composite/data/index.js",
+        "#composite/wiki-data": "./src/data/composite/wiki-data/index.js",
+        "#composite/wiki-properties": "./src/data/composite/wiki-properties/index.js",
+        "#composite/things/album": "./src/data/composite/things/album/index.js",
+        "#composite/things/flash": "./src/data/composite/things/flash/index.js",
+        "#composite/things/track": "./src/data/composite/things/track/index.js",
         "#content-dependencies": "./src/content/dependencies/index.js",
         "#content-function": "./src/content-function.js",
         "#cli": "./src/util/cli.js",
@@ -38,6 +46,7 @@
         "command-exists": "^1.2.9",
         "eslint": "^8.37.0",
         "he": "^1.2.0",
+        "image-size": "^1.0.2",
         "js-yaml": "^4.1.0",
         "marked": "^5.0.2",
         "striptags": "^4.0.0-alpha.4",
@@ -46,7 +55,7 @@
     "license": "GPL-3.0",
     "devDependencies": {
         "chokidar": "^3.5.3",
-        "tap": "^16.3.4",
+        "tap": "^18.4.0",
         "tcompare": "^6.0.0"
     },
     "tap": {
diff --git a/src/content/dependencies/generateAlbumCommentaryPage.js b/src/content/dependencies/generateAlbumCommentaryPage.js
index de61925..3ad1549 100644
--- a/src/content/dependencies/generateAlbumCommentaryPage.js
+++ b/src/content/dependencies/generateAlbumCommentaryPage.js
@@ -2,10 +2,13 @@ import {stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: [
+    'generateAlbumCoverArtwork',
     'generateAlbumNavAccent',
+    'generateAlbumSidebarTrackSection',
     'generateAlbumStyleRules',
     'generateColorStyleVariables',
     'generateContentHeading',
+    'generateTrackCoverArtwork',
     'generatePageLayout',
     'linkAlbum',
     'linkTrack',
@@ -21,7 +24,7 @@ export default {
       relation('generatePageLayout');
 
     relations.albumStyleRules =
-      relation('generateAlbumStyleRules', album);
+      relation('generateAlbumStyleRules', album, null);
 
     relations.albumLink =
       relation('linkAlbum', album);
@@ -30,6 +33,11 @@ export default {
       relation('generateAlbumNavAccent', album, null);
 
     if (album.commentary) {
+      if (album.hasCoverArt) {
+        relations.albumCommentaryCover =
+          relation('generateAlbumCoverArtwork', album);
+      }
+
       relations.albumCommentaryContent =
         relation('transformContent', album.commentary);
     }
@@ -46,6 +54,13 @@ export default {
       tracksWithCommentary
         .map(track => relation('linkTrack', track));
 
+    relations.trackCommentaryCovers =
+      tracksWithCommentary
+        .map(track =>
+          (track.hasUniqueCoverArt
+            ? relation('generateTrackCoverArtwork', track)
+            : null));
+
     relations.trackCommentaryContent =
       tracksWithCommentary
         .map(track => relation('transformContent', track.commentary));
@@ -57,6 +72,13 @@ export default {
             ? null
             : relation('generateColorStyleVariables')));
 
+    relations.sidebarAlbumLink =
+      relation('linkAlbum', album);
+
+    relations.sidebarTrackSections =
+      album.trackSections.map(trackSection =>
+        relation('generateAlbumSidebarTrackSection', album, null, trackSection));
+
     return relations;
   },
 
@@ -129,6 +151,9 @@ export default {
               {class: ['content-heading']},
               language.$('albumCommentaryPage.entry.title.albumCommentary')),
 
+            relations.albumCommentaryCover
+              ?.slots({mode: 'commentary'}),
+
             html.tag('blockquote',
               relations.albumCommentaryContent),
           ],
@@ -137,15 +162,19 @@ export default {
             heading: relations.trackCommentaryHeadings,
             link: relations.trackCommentaryLinks,
             directory: data.trackCommentaryDirectories,
+            cover: relations.trackCommentaryCovers,
             content: relations.trackCommentaryContent,
             colorVariables: relations.trackCommentaryColorVariables,
             color: data.trackCommentaryColors,
-          }).map(({heading, link, directory, content, colorVariables, color}) => [
+          }).map(({heading, link, directory, cover, content, colorVariables, color}) => [
               heading.slots({
                 tag: 'h3',
                 id: directory,
                 title: link,
               }),
+
+              cover?.slots({mode: 'commentary'}),
+
               html.tag('blockquote',
                 (color
                   ? {style: colorVariables.slot('color', color).content}
@@ -170,6 +199,17 @@ export default {
               }),
           },
         ],
+
+        leftSidebarStickyMode: 'column',
+        leftSidebarContent: [
+          html.tag('h1', relations.sidebarAlbumLink),
+          relations.sidebarTrackSections.map(section =>
+            section.slots({
+              anchor: true,
+              open: true,
+              mode: 'commentary',
+            })),
+        ],
       });
   },
 };
diff --git a/src/content/dependencies/generateAlbumGalleryNoTrackArtworksLine.js b/src/content/dependencies/generateAlbumGalleryNoTrackArtworksLine.js
new file mode 100644
index 0000000..ad99cb8
--- /dev/null
+++ b/src/content/dependencies/generateAlbumGalleryNoTrackArtworksLine.js
@@ -0,0 +1,7 @@
+export default {
+  extraDependencies: ['html', 'language'],
+
+  generate: ({html, language}) =>
+    html.tag('p', {class: 'quick-info'},
+      language.$('albumGalleryPage.noTrackArtworksLine')),
+};
diff --git a/src/content/dependencies/generateAlbumGalleryPage.js b/src/content/dependencies/generateAlbumGalleryPage.js
index 68b56bd..f61b198 100644
--- a/src/content/dependencies/generateAlbumGalleryPage.js
+++ b/src/content/dependencies/generateAlbumGalleryPage.js
@@ -3,8 +3,10 @@ import {compareArrays, stitchArrays} from '#sugar';
 export default {
   contentDependencies: [
     'generateAlbumGalleryCoverArtistsLine',
+    'generateAlbumGalleryNoTrackArtworksLine',
     'generateAlbumGalleryStatsLine',
     'generateAlbumNavAccent',
+    'generateAlbumSecondaryNav',
     'generateAlbumStyleRules',
     'generateCoverGrid',
     'generatePageLayout',
@@ -51,7 +53,7 @@ export default {
       relation('generatePageLayout');
 
     relations.albumStyleRules =
-      relation('generateAlbumStyleRules', album);
+      relation('generateAlbumStyleRules', album, null);
 
     relations.albumLink =
       relation('linkAlbum', album);
@@ -59,9 +61,17 @@ export default {
     relations.albumNavAccent =
       relation('generateAlbumNavAccent', album, null);
 
+    relations.secondaryNav =
+      relation('generateAlbumSecondaryNav', album);
+
     relations.statsLine =
       relation('generateAlbumGalleryStatsLine', album);
 
+    if (album.tracks.every(track => !track.hasUniqueCoverArt)) {
+      relations.noTrackArtworksLine =
+        relation('generateAlbumGalleryNoTrackArtworksLine');
+    }
+
     if (query.coverArtistsForAllTracks) {
       relations.coverArtistsLine =
         relation('generateAlbumGalleryCoverArtistsLine', query.coverArtistsForAllTracks);
@@ -70,15 +80,25 @@ export default {
     relations.coverGrid =
       relation('generateCoverGrid');
 
-    relations.links =
-      album.tracks.map(track =>
-        relation('linkTrack', track));
+    relations.links = [
+      relation('linkAlbum', album),
 
-    relations.images =
-      album.tracks.map(track =>
-        (track.hasUniqueCoverArt
-          ? relation('image', track.artTags)
-          : relation('image')));
+      ...
+        album.tracks
+          .map(track => relation('linkTrack', track)),
+    ];
+
+    relations.images = [
+      (album.hasCoverArt
+        ? relation('image', album.artTags)
+        : relation('image')),
+
+      ...
+        album.tracks.map(track =>
+          (track.hasUniqueCoverArt
+            ? relation('image', track.artTags)
+            : relation('image'))),
+    ];
 
     return relations;
   },
@@ -89,27 +109,41 @@ export default {
     data.name = album.name;
     data.color = album.color;
 
-    data.names =
-      album.tracks.map(track => track.name);
+    data.names = [
+      album.name,
+      ...album.tracks.map(track => track.name),
+    ];
 
-    data.coverArtists =
-      album.tracks.map(track => {
-        if (query.coverArtistsForAllTracks) {
-          return null;
-        }
+    data.coverArtists = [
+      (album.hasCoverArt
+        ? album.coverArtistContribs.map(({who: artist}) => artist.name)
+        : null),
 
-        if (track.hasUniqueCoverArt) {
-          return track.coverArtistContribs.map(({who: artist}) => artist.name);
-        }
+      ...
+        album.tracks.map(track => {
+          if (query.coverArtistsForAllTracks) {
+            return null;
+          }
 
-        return null;
-      });
+          if (track.hasUniqueCoverArt) {
+            return track.coverArtistContribs.map(({who: artist}) => artist.name);
+          }
+
+          return null;
+        }),
+    ];
 
-    data.paths =
-      album.tracks.map(track =>
-        (track.hasUniqueCoverArt
-          ? ['media.trackCover', track.album.directory, track.directory, track.coverArtFileExtension]
-          : null));
+    data.paths = [
+      (album.hasCoverArt
+        ? ['media.albumCover', album.directory, album.coverArtFileExtension]
+        : null),
+
+      ...
+        album.tracks.map(track =>
+          (track.hasUniqueCoverArt
+            ? ['media.trackCover', track.album.directory, track.directory, track.coverArtFileExtension]
+            : null)),
+    ];
 
     return data;
   },
@@ -131,6 +165,7 @@ export default {
         mainContent: [
           relations.statsLine,
           relations.coverArtistsLine,
+          relations.noTrackArtworksLine,
 
           relations.coverGrid
             .slots({
@@ -172,6 +207,8 @@ export default {
               }),
           },
         ],
+
+        secondaryNav: relations.secondaryNav,
       });
   },
 };
diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js
index ce17ab2..5fe27ca 100644
--- a/src/content/dependencies/generateAlbumInfoPage.js
+++ b/src/content/dependencies/generateAlbumInfoPage.js
@@ -37,14 +37,14 @@ export default {
       relation('generatePageLayout');
 
     relations.albumStyleRules =
-      relation('generateAlbumStyleRules', album);
+      relation('generateAlbumStyleRules', album, null);
 
     relations.socialEmbed =
       relation('generateAlbumSocialEmbed', album);
 
     relations.coverArtistChronologyContributions =
       getChronologyRelations(album, {
-        contributions: album.coverArtistContribs,
+        contributions: album.coverArtistContribs ?? [],
 
         linkArtist: artist => relation('linkArtist', artist),
 
diff --git a/src/content/dependencies/generateAlbumNavAccent.js b/src/content/dependencies/generateAlbumNavAccent.js
index c79219b..7eb1dac 100644
--- a/src/content/dependencies/generateAlbumNavAccent.js
+++ b/src/content/dependencies/generateAlbumNavAccent.js
@@ -33,10 +33,8 @@ export default {
       }
     }
 
-    if (album.tracks.some(t => t.hasUniqueCoverArt)) {
-      relations.albumGalleryLink =
-        relation('linkAlbumGallery', album);
-    }
+    relations.albumGalleryLink =
+      relation('linkAlbumGallery', album);
 
     if (album.commentary || album.tracks.some(t => t.commentary)) {
       relations.albumCommentaryLink =
@@ -49,6 +47,7 @@ export default {
   data(album, track) {
     return {
       hasMultipleTracks: album.tracks.length > 1,
+      galleryIsStub: album.tracks.every(t => !t.hasUniqueCoverArt),
       isTrackPage: !!track,
     };
   },
@@ -66,10 +65,11 @@ export default {
     const {content: extraLinks = []} =
       slots.showExtraLinks &&
         {content: [
-          relations.albumGalleryLink?.slots({
-            attributes: {class: slots.currentExtra === 'gallery' && 'current'},
-            content: language.$('albumPage.nav.gallery'),
-          }),
+          (!data.galleryIsStub || slots.currentExtra === 'gallery') &&
+            relations.albumGalleryLink?.slots({
+              attributes: {class: slots.currentExtra === 'gallery' && 'current'},
+              content: language.$('albumPage.nav.gallery'),
+            }),
 
           relations.albumCommentaryLink?.slots({
             attributes: {class: slots.currentExtra === 'commentary' && 'current'},
diff --git a/src/content/dependencies/generateAlbumSecondaryNav.js b/src/content/dependencies/generateAlbumSecondaryNav.js
index 705dec5..8cf36fa 100644
--- a/src/content/dependencies/generateAlbumSecondaryNav.js
+++ b/src/content/dependencies/generateAlbumSecondaryNav.js
@@ -5,7 +5,7 @@ export default {
     'generateColorStyleVariables',
     'generatePreviousNextLinks',
     'generateSecondaryNav',
-    'linkAlbum',
+    'linkAlbumDynamically',
     'linkGroup',
     'linkTrack',
   ],
@@ -64,14 +64,14 @@ export default {
         query.adjacentGroupInfo
           .map(({previousAlbum}) =>
             (previousAlbum
-              ? relation('linkAlbum', previousAlbum)
+              ? relation('linkAlbumDynamically', previousAlbum)
               : null));
 
       relations.nextAlbumLinks =
         query.adjacentGroupInfo
           .map(({nextAlbum}) =>
             (nextAlbum
-              ? relation('linkAlbum', nextAlbum)
+              ? relation('linkAlbumDynamically', nextAlbum)
               : null));
     }
 
diff --git a/src/content/dependencies/generateAlbumSidebarTrackSection.js b/src/content/dependencies/generateAlbumSidebarTrackSection.js
index 2aca6da..d3cd37f 100644
--- a/src/content/dependencies/generateAlbumSidebarTrackSection.js
+++ b/src/content/dependencies/generateAlbumSidebarTrackSection.js
@@ -33,10 +33,28 @@ export default {
       }
     }
 
+    data.trackDirectories =
+      trackSection.tracks
+        .map(track => track.directory);
+
+    data.tracksAreMissingCommentary =
+      trackSection.tracks
+        .map(track => !track.commentary);
+
     return data;
   },
 
-  generate(data, relations, {getColors, html, language}) {
+  slots: {
+    anchor: {type: 'boolean'},
+    open: {type: 'boolean'},
+
+    mode: {
+      validate: v => v.is('info', 'commentary'),
+      default: 'info',
+    },
+  },
+
+  generate(data, relations, slots, {getColors, html, language}) {
     const sectionName =
       html.tag('span', {class: 'group-name'},
         (data.isDefaultTrackSection
@@ -53,13 +71,28 @@ export default {
       relations.trackLinks.map((trackLink, index) =>
         html.tag('li',
           {
-            class:
+            class: [
               data.includesCurrentTrack &&
               index === data.currentTrackIndex &&
-              'current',
+                'current',
+
+              slots.mode === 'commentary' &&
+              data.tracksAreMissingCommentary[index] &&
+                'no-commentary',
+            ],
           },
           language.$('albumSidebar.trackList.item', {
-            track: trackLink,
+            track:
+              (slots.mode === 'commentary' && data.tracksAreMissingCommentary[index]
+                ? trackLink.slots({
+                    linkless: true,
+                  })
+             : slots.anchor
+                ? trackLink.slots({
+                    anchor: true,
+                    hash: data.trackDirectories[index],
+                  })
+                : trackLink),
           })));
 
     return html.tag('details',
@@ -67,6 +100,11 @@ export default {
         class: data.includesCurrentTrack && 'current',
 
         open: (
+          // Allow forcing open via a template slot.
+          // This isn't exactly janky, but the rest of this function
+          // kind of is when you contextualize it in a template...
+          slots.open ||
+
           // Leave sidebar track sections collapsed on album info page,
           // since there's already a view of the full track listing
           // in the main content area.
@@ -82,7 +120,7 @@ export default {
             (data.hasTrackNumbers
               ? language.$('albumSidebar.trackList.group.withRange', {
                   group: sectionName,
-                  range: `${data.firstTrackNumber}&ndash;${data.lastTrackNumber}`
+                  range: `${data.firstTrackNumber}–${data.lastTrackNumber}`
                 })
               : language.$('albumSidebar.trackList.group', {
                   group: sectionName,
diff --git a/src/content/dependencies/generateAlbumStyleRules.js b/src/content/dependencies/generateAlbumStyleRules.js
index 1acaea1..c5acf37 100644
--- a/src/content/dependencies/generateAlbumStyleRules.js
+++ b/src/content/dependencies/generateAlbumStyleRules.js
@@ -3,14 +3,13 @@ import {empty} from '#sugar';
 export default {
   extraDependencies: ['to'],
 
-  data(album) {
+  data(album, track) {
     const data = {};
 
     data.hasWallpaper = !empty(album.wallpaperArtistContribs);
     data.hasBanner = !empty(album.bannerArtistContribs);
 
     if (data.hasWallpaper) {
-      data.hasWallpaperStyle = !!album.wallpaperStyle;
       data.wallpaperPath = ['media.albumWallpaper', album.directory, album.wallpaperFileExtension];
       data.wallpaperStyle = album.wallpaperStyle;
     }
@@ -20,40 +19,54 @@ export default {
       data.bannerStyle = album.bannerStyle;
     }
 
+    data.albumDirectory = album.directory;
+
+    if (track) {
+      data.trackDirectory = track.directory;
+    }
+
     return data;
   },
 
   generate(data, {to}) {
-    const wallpaperPart =
-      (data.hasWallpaper
-        ? [
-            `body::before {`,
-            `    background-image: url("${to(...data.wallpaperPath)}");`,
-            ...(data.hasWallpaperStyle
-              ? data.wallpaperStyle
-                  .split('\n')
-                  .map(line => `    ${line}`)
-              : []),
-            `}`,
-          ]
-        : []);
+    const indent = parts =>
+      (parts ?? [])
+        .filter(Boolean)
+        .join('\n')
+        .split('\n')
+        .map(line => ' '.repeat(4) + line)
+        .join('\n');
 
-    const bannerPart =
-      (data.hasBannerStyle
-        ? [
-            `#banner img {`,
-            ...data.bannerStyle
-              .split('\n')
-              .map(line => `    ${line}`),
-            `}`,
-          ]
+    const rule = (selector, parts) =>
+      (!empty(parts.filter(Boolean))
+        ? [`${selector} {`, indent(parts), `}`]
         : []);
 
-    return [
-      ...wallpaperPart,
-      ...bannerPart,
-    ]
-      .filter(Boolean)
-      .join('\n');
+    const wallpaperRule =
+      data.hasWallpaper &&
+        rule(`body::before`, [
+          `background-image: url("${to(...data.wallpaperPath)}");`,
+          data.wallpaperStyle,
+        ]);
+
+    const bannerRule =
+      data.hasBanner &&
+        rule(`#banner img`, [
+          data.bannerStyle,
+        ]);
+
+    const dataRule =
+      rule(`:root`, [
+        data.albumDirectory &&
+          `--album-directory: ${data.albumDirectory};`,
+        data.trackDirectory &&
+          `--track-directory: ${data.trackDirectory};`,
+      ]);
+
+    return (
+      [wallpaperRule, bannerRule, dataRule]
+        .filter(Boolean)
+        .flat()
+        .join('\n'));
   },
 };
diff --git a/src/content/dependencies/generateAlbumTrackListItem.js b/src/content/dependencies/generateAlbumTrackListItem.js
index f65b47c..f92712f 100644
--- a/src/content/dependencies/generateAlbumTrackListItem.js
+++ b/src/content/dependencies/generateAlbumTrackListItem.js
@@ -1,4 +1,4 @@
-import {compareArrays} from '#sugar';
+import {compareArrays, empty} from '#sugar';
 
 export default {
   contentDependencies: [
@@ -11,9 +11,11 @@ export default {
   relations(relation, track) {
     const relations = {};
 
-    relations.contributionLinks =
-      track.artistContribs
-        .map(contrib => relation('linkContribution', contrib));
+    if (!empty(track.artistContribs)) {
+      relations.contributionLinks =
+        track.artistContribs
+          .map(contrib => relation('linkContribution', contrib));
+    }
 
     relations.trackLink =
       relation('linkTrack', track);
@@ -31,10 +33,12 @@ export default {
     }
 
     data.showArtists =
-      !compareArrays(
-        track.artistContribs.map(c => c.who),
-        album.artistContribs.map(c => c.who),
-        {checkOrder: false});
+      !empty(track.artistContribs) &&
+       (empty(album.artistContribs) ||
+        !compareArrays(
+          track.artistContribs.map(c => c.who),
+          album.artistContribs.map(c => c.who),
+          {checkOrder: false}));
 
     return data;
   },
diff --git a/src/content/dependencies/generateArtistInfoPageChunkItem.js b/src/content/dependencies/generateArtistInfoPageChunkItem.js
index 36f0ebc..9f99513 100644
--- a/src/content/dependencies/generateArtistInfoPageChunkItem.js
+++ b/src/content/dependencies/generateArtistInfoPageChunkItem.js
@@ -5,7 +5,7 @@ export default {
     content: {type: 'html'},
 
     otherArtistLinks: {validate: v => v.strictArrayOf(v.isHTML)},
-    contribution: {type: 'string'},
+    contribution: {type: 'html'},
     rerelease: {type: 'boolean'},
   },
 
@@ -30,7 +30,7 @@ export default {
         options.artists = language.formatConjunctionList(slots.otherArtistLinks);
       }
 
-      if (slots.contribution) {
+      if (!html.isBlank(slots.contribution)) {
         parts.push('withContribution');
         options.contribution = slots.contribution;
       }
diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js
index 0566f71..654f759 100644
--- a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js
+++ b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js
@@ -1,4 +1,4 @@
-import {accumulateSum, stitchArrays} from '#sugar';
+import {accumulateSum, empty, stitchArrays} from '#sugar';
 
 import {
   chunkByProperties,
@@ -16,7 +16,7 @@ export default {
     'linkTrack',
   ],
 
-  extraDependencies: ['language'],
+  extraDependencies: ['html', 'language'],
 
   query(artist) {
     const tracksAsArtistAndContributor =
@@ -122,11 +122,16 @@ export default {
 
       trackContributions:
         query.chunks.map(({chunk}) =>
-          chunk.map(({contribs}) =>
-            contribs
-              .filter(({who}) => who === artist)
-              .filter(({what}) => what)
-              .map(({what}) => what))),
+          chunk
+            .map(({contribs}) =>
+              contribs
+                .filter(({who}) => who === artist)
+                .filter(({what}) => what)
+                .map(({what}) => what))
+            .map(contributions =>
+              (empty(contributions)
+                ? null
+                : contributions))),
 
       trackRereleases:
         query.chunks.map(({chunk}) =>
@@ -134,7 +139,7 @@ export default {
     };
   },
 
-  generate(data, relations, {language}) {
+  generate(data, relations, {html, language}) {
     return relations.chunkedList.slots({
       chunks:
         stitchArrays({
@@ -192,7 +197,9 @@ export default {
                       rerelease,
 
                       contribution:
-                        language.formatUnitList(contribution),
+                        (contribution
+                          ? language.formatUnitList(contribution)
+                          : html.blank()),
 
                       content:
                         (duration
diff --git a/src/content/dependencies/generateCoverArtwork.js b/src/content/dependencies/generateCoverArtwork.js
index 4060c6b..aeba97d 100644
--- a/src/content/dependencies/generateCoverArtwork.js
+++ b/src/content/dependencies/generateCoverArtwork.js
@@ -32,7 +32,7 @@ export default {
     },
 
     mode: {
-      validate: v => v.is('primary', 'thumbnail'),
+      validate: v => v.is('primary', 'thumbnail', 'commentary'),
       default: 'primary',
     },
   },
@@ -73,6 +73,19 @@ export default {
             square: true,
           });
 
+      case 'commentary':
+        return relations.image
+          .slots({
+            path: slots.path,
+            alt: slots.alt,
+            thumb: 'medium',
+            class: 'commentary-art',
+            reveal: true,
+            link: true,
+            square: true,
+            lazy: true,
+          });
+
       default:
         return html.blank();
     }
diff --git a/src/content/dependencies/generateCoverGrid.js b/src/content/dependencies/generateCoverGrid.js
index 9822e1a..5636e4f 100644
--- a/src/content/dependencies/generateCoverGrid.js
+++ b/src/content/dependencies/generateCoverGrid.js
@@ -2,7 +2,7 @@ import {stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: ['generateGridActionLinks'],
-  extraDependencies: ['html'],
+  extraDependencies: ['html', 'language'],
 
   relations(relation) {
     return {
@@ -20,7 +20,7 @@ export default {
     actionLinks: {validate: v => v.sparseArrayOf(v.isHTML)},
   },
 
-  generate(relations, slots, {html}) {
+  generate(relations, slots, {html, language}) {
     return (
       html.tag('div', {class: 'grid-listing'}, [
         stitchArrays({
@@ -42,8 +42,12 @@ export default {
                       ? slots.lazy
                       : false),
                 }),
-                html.tag('span', {[html.onlyIfContent]: true}, name),
-                html.tag('span', {[html.onlyIfContent]: true}, info),
+
+                html.tag('span', {[html.onlyIfContent]: true},
+                  language.sanitize(name)),
+
+                html.tag('span', {[html.onlyIfContent]: true},
+                  language.sanitize(info)),
               ],
             })),
 
diff --git a/src/content/dependencies/generateFlashActGalleryPage.js b/src/content/dependencies/generateFlashActGalleryPage.js
new file mode 100644
index 0000000..8eea58b
--- /dev/null
+++ b/src/content/dependencies/generateFlashActGalleryPage.js
@@ -0,0 +1,91 @@
+import {stitchArrays} from '#sugar';
+
+export default {
+  contentDependencies: [
+    'generateCoverGrid',
+    'generateFlashActNavAccent',
+    'generateFlashActSidebar',
+    'generatePageLayout',
+    'image',
+    'linkFlash',
+    'linkFlashIndex',
+  ],
+
+  extraDependencies: ['html', 'language'],
+
+  relations: (relation, act) => ({
+    layout:
+      relation('generatePageLayout'),
+
+    flashIndexLink:
+      relation('linkFlashIndex'),
+
+    flashActNavAccent:
+      relation('generateFlashActNavAccent', act),
+
+    sidebar:
+      relation('generateFlashActSidebar', act, null),
+
+    coverGrid:
+      relation('generateCoverGrid'),
+
+    coverGridImages:
+      act.flashes
+        .map(_flash => relation('image')),
+
+    flashLinks:
+      act.flashes
+        .map(flash => relation('linkFlash', flash)),
+  }),
+
+  data: (act) => ({
+    name: act.name,
+    color: act.color,
+
+    flashNames:
+      act.flashes.map(flash => flash.name),
+
+    flashCoverPaths:
+      act.flashes.map(flash =>
+        ['media.flashArt', flash.directory, flash.coverArtFileExtension])
+  }),
+
+  generate(data, relations, {html, language}) {
+    return relations.layout.slots({
+      title:
+        language.$('flashPage.title', {
+          flash: new html.Tag(null, null, data.name),
+        }),
+
+      color: data.color,
+      headingMode: 'static',
+
+      mainClasses: ['flash-index'],
+      mainContent: [
+        relations.coverGrid.slots({
+          links: relations.flashLinks,
+          names: data.flashNames,
+          lazy: 6,
+
+          images:
+            stitchArrays({
+              image: relations.coverGridImages,
+              path: data.flashCoverPaths,
+            }).map(({image, path}) =>
+                image.slot('path', path)),
+        }),
+      ],
+
+      navLinkStyle: 'hierarchical',
+      navLinks: [
+        {auto: 'home'},
+        {html: relations.flashIndexLink},
+        {auto: 'current'},
+      ],
+
+      navBottomRowContent: relations.flashActNavAccent,
+
+      ...relations.sidebar,
+    });
+  },
+};
diff --git a/src/content/dependencies/generateFlashActNavAccent.js b/src/content/dependencies/generateFlashActNavAccent.js
new file mode 100644
index 0000000..9850438
--- /dev/null
+++ b/src/content/dependencies/generateFlashActNavAccent.js
@@ -0,0 +1,74 @@
+import {empty} from '#sugar';
+
+export default {
+  contentDependencies: [
+    'generatePreviousNextLinks',
+    'linkFlashAct',
+  ],
+
+  extraDependencies: ['html', 'language', 'wikiData'],
+
+  sprawl({flashActData}) {
+    return {flashActData};
+  },
+
+  query(sprawl, flashAct) {
+    // Like with generateFlashNavAccent, don't sort chronologically here.
+    const flashActs =
+      sprawl.flashActData;
+
+    const index = flashActs.indexOf(flashAct);
+
+    const previousFlashAct =
+      (index > 0
+        ? flashActs[index - 1]
+        : null);
+
+    const nextFlashAct =
+      (index < flashActs.length - 1
+        ? flashActs[index + 1]
+        : null);
+
+    return {previousFlashAct, nextFlashAct};
+  },
+
+  relations(relation, query) {
+    const relations = {};
+
+    if (query.previousFlashAct || query.nextFlashAct) {
+      relations.previousNextLinks =
+        relation('generatePreviousNextLinks');
+
+      relations.previousFlashActLink =
+        (query.previousFlashAct
+          ? relation('linkFlashAct', query.previousFlashAct)
+          : null);
+
+      relations.nextFlashActLink =
+        (query.nextFlashAct
+          ? relation('linkFlashAct', query.nextFlashAct)
+          : null);
+    }
+
+    return relations;
+  },
+
+  generate(relations, {html, language}) {
+    const {content: previousNextLinks = []} =
+      relations.previousNextLinks &&
+        relations.previousNextLinks.slots({
+          previousLink: relations.previousFlashActLink,
+          nextLink: relations.nextFlashActLink,
+        });
+
+    const allLinks = [
+      ...previousNextLinks,
+    ].filter(Boolean);
+
+    if (empty(allLinks)) {
+      return html.blank();
+    }
+
+    return `(${language.formatUnitList(allLinks)})`;
+  },
+};
diff --git a/src/content/dependencies/generateFlashActSidebar.js b/src/content/dependencies/generateFlashActSidebar.js
new file mode 100644
index 0000000..bd6063c
--- /dev/null
+++ b/src/content/dependencies/generateFlashActSidebar.js
@@ -0,0 +1,194 @@
+import find from '#find';
+import {stitchArrays} from '#sugar';
+
+export default {
+  contentDependencies: ['linkFlash', 'linkFlashAct', 'linkFlashIndex'],
+  extraDependencies: ['getColors', 'html', 'language', 'wikiData'],
+
+  // So help me Gog, the flash sidebar is heavily hard-coded.
+
+  sprawl: ({flashActData}) => ({flashActData}),
+
+  query(sprawl, act, flash) {
+    const findFlashAct = directory =>
+      find.flashAct(directory, sprawl.flashActData, {mode: 'error'});
+
+    const sideFirstActs = [
+      findFlashAct('flash-act:a1'),
+      findFlashAct('flash-act:a6a1'),
+      findFlashAct('flash-act:hiveswap'),
+      findFlashAct('flash-act:cool-and-new-web-comic'),
+      findFlashAct('flash-act:sunday-night-strifin'),
+    ];
+
+    const sideNames = [
+      `Side 1 (Acts 1-5)`,
+      `Side 2 (Acts 6-7)`,
+      `Additional Canon`,
+      `Fan Adventures`,
+      `Fan Games & More`,
+    ];
+
+    const sideColors = [
+      '#4ac925',
+      '#3796c6',
+      '#f2a400',
+      '#c466ff',
+      '#32c7fe',
+    ];
+
+    const sideFirstActIndexes =
+      sideFirstActs
+        .map(act => sprawl.flashActData.indexOf(act));
+
+    const actSideIndexes =
+      sprawl.flashActData
+        .map((act, actIndex) => actIndex)
+        .map(actIndex =>
+          sideFirstActIndexes
+            .findIndex((firstActIndex, i) =>
+              i === sideFirstActs.length - 1 ||
+                firstActIndex <= actIndex &&
+                sideFirstActIndexes[i + 1] > actIndex));
+
+    const sideActs =
+      sideNames
+        .map((name, sideIndex) =>
+          stitchArrays({
+            act: sprawl.flashActData,
+            actSideIndex: actSideIndexes,
+          }).filter(({actSideIndex}) => actSideIndex === sideIndex)
+            .map(({act}) => act));
+
+    const currentActFlashes =
+      act.flashes;
+
+    const currentFlashIndex =
+      currentActFlashes.indexOf(flash);
+
+    const currentSideIndex =
+      actSideIndexes[sprawl.flashActData.indexOf(act)];
+
+    const currentSideActs =
+      sideActs[currentSideIndex];
+
+    const currentActIndex =
+      currentSideActs.indexOf(act);
+
+    const fallbackListTerminology =
+      (currentSideIndex <= 1
+        ? 'flashesInThisAct'
+        : 'entriesInThisSection');
+
+    return {
+      sideNames,
+      sideColors,
+      sideActs,
+
+      currentSideIndex,
+      currentSideActs,
+      currentActIndex,
+      currentActFlashes,
+      currentFlashIndex,
+
+      fallbackListTerminology,
+    };
+  },
+
+  relations: (relation, query, sprawl, act, _flash) => ({
+    currentActLink:
+      relation('linkFlashAct', act),
+
+    flashIndexLink:
+      relation('linkFlashIndex'),
+
+    sideActLinks:
+      query.sideActs
+        .map(acts => acts
+          .map(act => relation('linkFlashAct', act))),
+
+    currentActFlashLinks:
+      act.flashes
+        .map(flash => relation('linkFlash', flash)),
+  }),
+
+  data: (query, sprawl, act, flash) => ({
+    isFlashActPage: !flash,
+
+    sideColors: query.sideColors,
+    sideNames: query.sideNames,
+
+    currentSideIndex: query.currentSideIndex,
+    currentActIndex: query.currentActIndex,
+    currentFlashIndex: query.currentFlashIndex,
+
+    customListTerminology: act.listTerminology,
+    fallbackListTerminology: query.fallbackListTerminology,
+  }),
+
+  generate(data, relations, {getColors, html, language}) {
+    const currentActBox = html.tags([
+      html.tag('h1', relations.currentActLink),
+
+      html.tag('details',
+        (data.isFlashActPage
+          ? {}
+          : {class: 'current', open: true}),
+        [
+          html.tag('summary',
+            html.tag('span', {class: 'group-name'},
+              (data.customListTerminology
+                ? language.sanitize(data.customListTerminology)
+                : language.$('flashSidebar.flashList', data.fallbackListTerminology)))),
+
+          html.tag('ul',
+            relations.currentActFlashLinks
+              .map((flashLink, index) =>
+                html.tag('li',
+                  {class: index === data.currentFlashIndex && 'current'},
+                  flashLink))),
+        ]),
+    ]);
+
+    const sideMapBox = html.tags([
+      html.tag('h1', relations.flashIndexLink),
+
+      stitchArrays({
+        sideName: data.sideNames,
+        sideColor: data.sideColors,
+        actLinks: relations.sideActLinks,
+      }).map(({sideName, sideColor, actLinks}, sideIndex) =>
+          html.tag('details', {
+            class: sideIndex === data.currentSideIndex && 'current',
+            open: data.isFlashActPage && sideIndex === data.currentSideIndex,
+            style: sideColor && `--primary-color: ${getColors(sideColor).primary}`
+          }, [
+            html.tag('summary',
+              html.tag('span', {class: 'group-name'},
+                sideName)),
+
+            html.tag('ul',
+              actLinks.map((actLink, actIndex) =>
+                html.tag('li',
+                  {class:
+                    sideIndex === data.currentSideIndex &&
+                    actIndex === data.currentActIndex &&
+                      'current'},
+                  actLink))),
+          ])),
+    ]);
+
+    return {
+      leftSidebarMultiple:
+        (data.isFlashActPage
+          ? [
+              {content: sideMapBox},
+              {content: currentActBox},
+            ]
+          : [
+              {content: currentActBox},
+              {content: sideMapBox},
+            ]),
+    };
+  },
+};
diff --git a/src/content/dependencies/generateFlashIndexPage.js b/src/content/dependencies/generateFlashIndexPage.js
index 66588fd..ad1dab9 100644
--- a/src/content/dependencies/generateFlashIndexPage.js
+++ b/src/content/dependencies/generateFlashIndexPage.js
@@ -7,6 +7,7 @@ export default {
     'generatePageLayout',
     'image',
     'linkFlash',
+    'linkFlashAct',
   ],
 
   extraDependencies: ['html', 'language', 'wikiData'],
@@ -36,9 +37,9 @@ export default {
       query.flashActs
         .map(() => relation('generateColorStyleVariables')),
 
-    actFirstFlashLinks:
+    actLinks:
       query.flashActs
-        .map(act => relation('linkFlash', act.flashes[0])),
+        .map(act => relation('linkFlashAct', act)),
 
     actCoverGrids:
       query.flashActs
@@ -58,7 +59,7 @@ export default {
   data: (query) => ({
     jumpLinkAnchors:
       query.jumpActs
-        .map(act => act.anchor),
+        .map(act => act.directory),
 
     jumpLinkColors:
       query.jumpActs
@@ -70,16 +71,12 @@ export default {
 
     actAnchors:
       query.flashActs
-        .map(act => act.anchor),
+        .map(act => act.directory),
 
     actColors:
       query.flashActs
         .map(act => act.color),
 
-    actNames:
-      query.flashActs
-        .map(act => act.name),
-
     actCoverGridNames:
       query.flashActs
         .map(act => act.flashes
@@ -118,10 +115,9 @@ export default {
 
         stitchArrays({
           colorVariables: relations.actColorVariables,
-          firstFlashLink: relations.actFirstFlashLinks,
+          actLink: relations.actLinks,
           anchor: data.actAnchors,
           color: data.actColors,
-          name: data.actNames,
 
           coverGrid: relations.actCoverGrids,
           coverGridImages: relations.actCoverGridImages,
@@ -132,8 +128,7 @@ export default {
             colorVariables,
             anchor,
             color,
-            name,
-            firstFlashLink,
+            actLink,
 
             coverGrid,
             coverGridImages,
@@ -146,7 +141,7 @@ export default {
                 id: anchor,
                 style: colorVariables.slot('color', color).content,
               },
-              firstFlashLink.slot('content', name)),
+              actLink),
 
             coverGrid.slots({
               links: coverGridLinks,
diff --git a/src/content/dependencies/generateFlashInfoPage.js b/src/content/dependencies/generateFlashInfoPage.js
index 553d2f5..09c6b37 100644
--- a/src/content/dependencies/generateFlashInfoPage.js
+++ b/src/content/dependencies/generateFlashInfoPage.js
@@ -4,13 +4,13 @@ export default {
   contentDependencies: [
     'generateContentHeading',
     'generateContributionList',
+    'generateFlashActSidebar',
     'generateFlashCoverArtwork',
     'generateFlashNavAccent',
-    'generateFlashSidebar',
     'generatePageLayout',
     'generateTrackList',
     'linkExternal',
-    'linkFlashIndex',
+    'linkFlashAct',
   ],
 
   extraDependencies: ['html', 'language'],
@@ -41,7 +41,7 @@ export default {
       relation('generatePageLayout');
 
     relations.sidebar =
-      relation('generateFlashSidebar', flash);
+      relation('generateFlashActSidebar', flash.act, flash);
 
     if (query.urls) {
       relations.externalLinks =
@@ -59,8 +59,8 @@ export default {
 
     const nav = sections.nav = {};
 
-    nav.flashIndexLink =
-      relation('linkFlashIndex');
+    nav.flashActLink =
+      relation('linkFlashAct', flash.act);
 
     nav.flashNavAccent =
       relation('generateFlashNavAccent', flash);
@@ -163,14 +163,11 @@ export default {
       navLinkStyle: 'hierarchical',
       navLinks: [
         {auto: 'home'},
-        {html: sec.nav.flashIndexLink},
+        {html: sec.nav.flashActLink.slot('color', false)},
         {auto: 'current'},
       ],
 
-      navBottomRowContent:
-        sec.nav.flashNavAccent.slots({
-          showFlashNavigation: true,
-        }),
+      navBottomRowContent: sec.nav.flashNavAccent,
 
       ...relations.sidebar,
     });
diff --git a/src/content/dependencies/generateFlashNavAccent.js b/src/content/dependencies/generateFlashNavAccent.js
index 2c8205d..57196d0 100644
--- a/src/content/dependencies/generateFlashNavAccent.js
+++ b/src/content/dependencies/generateFlashNavAccent.js
@@ -55,13 +55,8 @@ export default {
     return relations;
   },
 
-  slots: {
-    showFlashNavigation: {type: 'boolean', default: false},
-  },
-
-  generate(relations, slots, {html, language}) {
+  generate(relations, {html, language}) {
     const {content: previousNextLinks = []} =
-      slots.showFlashNavigation &&
       relations.previousNextLinks &&
         relations.previousNextLinks.slots({
           previousLink: relations.previousFlashLink,
diff --git a/src/content/dependencies/generateFlashSidebar.js b/src/content/dependencies/generateFlashSidebar.js
deleted file mode 100644
index ba76192..0000000
--- a/src/content/dependencies/generateFlashSidebar.js
+++ /dev/null
@@ -1,236 +0,0 @@
-import {stitchArrays} from '#sugar';
-
-export default {
-  contentDependencies: ['linkFlash', 'linkFlashIndex'],
-  extraDependencies: ['html', 'wikiData'],
-
-  // So help me Gog, the flash sidebar is heavily hard-coded.
-
-  sprawl: ({flashActData}) => ({flashActData}),
-
-  query(sprawl, flash) {
-    const flashActs =
-      sprawl.flashActData.slice();
-
-    const act6 =
-      flashActs
-        .findIndex(act => act.name.startsWith('Act 6'));
-
-    const postCanon =
-      flashActs
-        .findIndex(act => act.name.includes('Post Canon'));
-
-    const outsideCanon =
-      postCanon +
-      flashActs
-        .slice(postCanon)
-        .findIndex(act => !act.name.includes('Post Canon'));
-
-    const currentAct = flash.act;
-
-    const actIndex =
-      flashActs
-        .indexOf(currentAct);
-
-    const side =
-      (actIndex < 0
-        ? 0
-     : actIndex < act6
-        ? 1
-     : actIndex < outsideCanon
-        ? 2
-        : 3);
-
-    const sideActs =
-      flashActs
-        .filter((act, index) =>
-          act.name.startsWith('Act 1') ||
-          act.name.startsWith('Act 6 Act 1') ||
-          act.name.startsWith('Hiveswap') ||
-          index >= outsideCanon);
-
-    const currentSideIndex =
-      sideActs
-        .findIndex(act => {
-          if (act.name.startsWith('Act 1')) {
-            return side === 1;
-          } else if (act.name.startsWith('Act 6 Act 1')) {
-            return side === 2;
-          } else if (act.name.startsWith('Hiveswap Act 1')) {
-            return side === 3;
-          } else {
-            return act === currentAct;
-          }
-        })
-
-    const sideNames =
-      sideActs
-        .map(act => {
-          if (act.name.startsWith('Act 1')) {
-            return `Side 1 (Acts 1-5)`;
-          } else if (act.name.startsWith('Act 6 Act 1')) {
-            return `Side 2 (Acts 6-7)`;
-          } else if (act.name.startsWith('Hiveswap Act 1')) {
-            return `Outside Canon (Misc. Games)`;
-          } else {
-            return act.name;
-          }
-        });
-
-    const sideColors =
-      sideActs
-        .map(act => {
-          if (act.name.startsWith('Act 1')) {
-            return '#4ac925';
-          } else if (act.name.startsWith('Act 6 Act 1')) {
-            return '#1076a2';
-          } else if (act.name.startsWith('Hiveswap Act 1')) {
-            return '#008282';
-          } else {
-            return act.color;
-          }
-        });
-
-    const sideFirstFlashes =
-      sideActs
-        .map(act => act.flashes[0]);
-
-    const scopeActs =
-      flashActs
-        .filter((act, index) => {
-          if (index < act6) {
-            return side === 1;
-          } else if (index < outsideCanon) {
-            return side === 2;
-          } else {
-            return false;
-          }
-        });
-
-    const currentScopeActIndex =
-      scopeActs.indexOf(currentAct);
-
-    const scopeActNames =
-      scopeActs
-        .map(act => act.name);
-
-    const scopeActFirstFlashes =
-      scopeActs
-        .map(act => act.flashes[0]);
-
-    const currentActFlashes =
-      currentAct.flashes;
-
-    const currentFlashIndex =
-      currentActFlashes
-        .indexOf(flash);
-
-    return {
-      currentSideIndex,
-      sideNames,
-      sideColors,
-      sideFirstFlashes,
-
-      currentScopeActIndex,
-      scopeActNames,
-      scopeActFirstFlashes,
-
-      currentActFlashes,
-      currentFlashIndex,
-    };
-  },
-
-  relations: (relation, query) => ({
-    flashIndexLink:
-      relation('linkFlashIndex'),
-
-    sideFirstFlashLinks:
-      query.sideFirstFlashes
-        .map(flash => relation('linkFlash', flash)),
-
-    scopeActFirstFlashLinks:
-      query.scopeActFirstFlashes
-        .map(flash => relation('linkFlash', flash)),
-
-    currentActFlashLinks:
-      query.currentActFlashes
-        .map(flash => relation('linkFlash', flash)),
-  }),
-
-  data: (query) => ({
-    currentSideIndex: query.currentSideIndex,
-    sideColors: query.sideColors,
-    sideNames: query.sideNames,
-
-    currentScopeActIndex: query.currentScopeActIndex,
-    scopeActNames: query.scopeActNames,
-
-    currentFlashIndex: query.currentFlashIndex,
-  }),
-
-  generate(data, relations, {html}) {
-    const currentActFlashList =
-      html.tag('ul',
-        relations.currentActFlashLinks
-          .map((flashLink, index) =>
-            html.tag('li',
-              {class: index === data.currentFlashIndex && 'current'},
-              flashLink)));
-
-    return {
-      leftSidebarContent: html.tags([
-        html.tag('h1', relations.flashIndexLink),
-
-        html.tag('dl',
-          stitchArrays({
-            sideFirstFlashLink: relations.sideFirstFlashLinks,
-            sideColor: data.sideColors,
-            sideName: data.sideNames,
-          }).map(({sideFirstFlashLink, sideColor, sideName}, index) => [
-              // Side acts are displayed whether part of Homestuck proper or
-              // not, and they're always the same regardless the current flash
-              // page. Scope acts, if applicable, and the list of flashes
-              // belonging to the current act, will be inserted after the
-              // heading of the current side.
-              html.tag('dt',
-                {class: [
-                  'side',
-                  index === data.currentSideIndex && 'current',
-                ]},
-                sideFirstFlashLink.slots({
-                  color: sideColor,
-                  content: sideName,
-                })),
-
-              // Scope acts are only applicable when inside Homestuck proper.
-              // Hiveswap and all acts beyond are each considered to be its
-              // own "side".
-              index === data.currentSideIndex &&
-              data.currentScopeActIndex !== -1 &&
-                stitchArrays({
-                  scopeActFirstFlashLink: relations.scopeActFirstFlashLinks,
-                  scopeActName: data.scopeActNames,
-                }).map(({scopeActFirstFlashLink, scopeActName}, index) => [
-                    html.tag('dt',
-                      {class: index === data.currentScopeActIndex && 'current'},
-                      scopeActFirstFlashLink.slot('content', scopeActName)),
-
-                    // Inside Homestuck proper, the flash list of the current
-                    // act should show after the heading for the relevant
-                    // scope act.
-                    index === data.currentScopeActIndex &&
-                      html.tag('dd', currentActFlashList),
-                  ]),
-
-              // Outside of Homestuck proper, the current act is represented
-              // by a side instead of a scope act, so place its flash list
-              // after the heading for the relevant side.
-              index === data.currentSideIndex &&
-              data.currentScopeActIndex === -1 &&
-                html.tag('dd', currentActFlashList),
-            ])),
-
-      ]),
-    };
-  },
-};
diff --git a/src/content/dependencies/generateFooterLocalizationLinks.js b/src/content/dependencies/generateFooterLocalizationLinks.js
index b4970b1..5df8356 100644
--- a/src/content/dependencies/generateFooterLocalizationLinks.js
+++ b/src/content/dependencies/generateFooterLocalizationLinks.js
@@ -38,7 +38,7 @@ export default {
 
     return html.tag('div', {class: 'footer-localization-links'},
       language.$('misc.uiLanguage', {
-        languages: links.join('\n'),
+        languages: language.formatListWithoutSeparator(links),
       }));
   },
 };
diff --git a/src/content/dependencies/generateGroupGalleryPage.js b/src/content/dependencies/generateGroupGalleryPage.js
index 47239f5..259f5dc 100644
--- a/src/content/dependencies/generateGroupGalleryPage.js
+++ b/src/content/dependencies/generateGroupGalleryPage.js
@@ -11,6 +11,7 @@ export default {
     'generateCoverCarousel',
     'generateCoverGrid',
     'generateGroupNavLinks',
+    'generateGroupSecondaryNav',
     'generateGroupSidebar',
     'generatePageLayout',
     'image',
@@ -20,18 +21,8 @@ export default {
 
   extraDependencies: ['html', 'language', 'wikiData'],
 
-  sprawl({listingSpec, wikiInfo}) {
-    const sprawl = {};
-    sprawl.enableGroupUI = wikiInfo.enableGroupUI;
-
-    if (wikiInfo.enableListings && wikiInfo.enableGroupUI) {
-      sprawl.groupsByCategoryListing =
-        listingSpec
-          .find(l => l.directory === 'groups/by-category');
-    }
-
-    return sprawl;
-  },
+  sprawl: ({wikiInfo}) =>
+    ({enableGroupUI: wikiInfo.enableGroupUI}),
 
   relations(relation, sprawl, group) {
     const relations = {};
@@ -46,15 +37,13 @@ export default {
       relation('generateGroupNavLinks', group);
 
     if (sprawl.enableGroupUI) {
+      relations.secondaryNav =
+        relation('generateGroupSecondaryNav', group);
+
       relations.sidebar =
         relation('generateGroupSidebar', group);
     }
 
-    if (sprawl.groupsByCategoryListing) {
-      relations.groupListingLink =
-        relation('linkListing', sprawl.groupsByCategoryListing);
-    }
-
     const carouselAlbums = filterItemsForCarousel(group.featuredAlbums);
 
     if (!empty(carouselAlbums)) {
@@ -160,15 +149,6 @@ export default {
                 })),
             })),
 
-          relations.groupListingLink &&
-            html.tag('p',
-              {class: 'quick-info'},
-              language.$('groupGalleryPage.anotherGroupLine', {
-                link:
-                  relations.groupListingLink
-                    .slot('content', language.$('groupGalleryPage.anotherGroupLine.link')),
-              })),
-
           relations.coverGrid
             .slots({
               links: relations.gridLinks,
@@ -208,6 +188,9 @@ export default {
           relations.navLinks
             .slot('currentExtra', 'gallery')
             .content,
+
+        secondaryNav:
+          relations.secondaryNav ?? null,
       });
   },
 };
diff --git a/src/content/dependencies/generateGroupInfoPage.js b/src/content/dependencies/generateGroupInfoPage.js
index e162a26..0583755 100644
--- a/src/content/dependencies/generateGroupInfoPage.js
+++ b/src/content/dependencies/generateGroupInfoPage.js
@@ -4,6 +4,7 @@ export default {
   contentDependencies: [
     'generateContentHeading',
     'generateGroupNavLinks',
+    'generateGroupSecondaryNav',
     'generateGroupSidebar',
     'generatePageLayout',
     'linkAlbum',
@@ -32,6 +33,9 @@ export default {
       relation('generateGroupNavLinks', group);
 
     if (sprawl.enableGroupUI) {
+      relations.secondaryNav =
+        relation('generateGroupSecondaryNav', group);
+
       relations.sidebar =
         relation('generateGroupSidebar', group);
     }
@@ -161,6 +165,8 @@ export default {
 
         navLinkStyle: 'hierarchical',
         navLinks: relations.navLinks.content,
+
+        secondaryNav: relations.secondaryNav ?? null,
       });
   },
 };
diff --git a/src/content/dependencies/generateGroupNavLinks.js b/src/content/dependencies/generateGroupNavLinks.js
index 68341e0..5cde2ab 100644
--- a/src/content/dependencies/generateGroupNavLinks.js
+++ b/src/content/dependencies/generateGroupNavLinks.js
@@ -2,10 +2,8 @@ import {empty} from '#sugar';
 
 export default {
   contentDependencies: [
-    'generatePreviousNextLinks',
     'linkGroup',
     'linkGroupGallery',
-    'linkGroupExtra',
   ],
 
   extraDependencies: ['html', 'language', 'wikiData'],
@@ -28,24 +26,6 @@ export default {
     relations.mainLink =
       relation('linkGroup', group);
 
-    relations.previousNextLinks =
-      relation('generatePreviousNextLinks');
-
-    const groups = sprawl.groupCategoryData
-      .flatMap(category => category.groups);
-
-    const index = groups.indexOf(group);
-
-    if (index > 0) {
-      relations.previousLink =
-        relation('linkGroupExtra', groups[index - 1]);
-    }
-
-    if (index < groups.length - 1) {
-      relations.nextLink =
-        relation('linkGroupExtra', groups[index + 1]);
-    }
-
     relations.infoLink =
       relation('linkGroup', group);
 
@@ -80,26 +60,6 @@ export default {
       ];
     }
 
-    const previousNextLinks =
-      (relations.previousLink || relations.nextLink) &&
-        relations.previousNextLinks.slots({
-          previousLink:
-            relations.previousLink
-              ?.slot('extra', slots.currentExtra)
-              ?.content
-            ?? null,
-          nextLink:
-            relations.nextLink
-              ?.slot('extra', slots.currentExtra)
-              ?.content
-            ?? null,
-        });
-
-    const previousNextPart =
-      previousNextLinks &&
-        language.formatUnitList(
-          previousNextLinks.content.filter(Boolean));
-
     const infoLink =
       relations.infoLink.slots({
         attributes: {class: slots.currentExtra === null && 'current'},
@@ -119,7 +79,9 @@ export default {
         : language.formatUnitList([infoLink, ...extraLinks]));
 
     const accent =
-      `(${[extrasPart, previousNextPart].filter(Boolean).join('; ')})`;
+      (extrasPart
+        ? `(${extrasPart})`
+        : null);
 
     return [
       {auto: 'home'},
diff --git a/src/content/dependencies/generateGroupSecondaryNav.js b/src/content/dependencies/generateGroupSecondaryNav.js
new file mode 100644
index 0000000..e3b2809
--- /dev/null
+++ b/src/content/dependencies/generateGroupSecondaryNav.js
@@ -0,0 +1,99 @@
+export default {
+  contentDependencies: [
+    'generateColorStyleVariables',
+    'generatePreviousNextLinks',
+    'generateSecondaryNav',
+    'linkGroupDynamically',
+    'linkListing',
+  ],
+
+  extraDependencies: ['html', 'language', 'wikiData'],
+
+  sprawl: ({listingSpec, wikiInfo}) => ({
+    groupsByCategoryListing:
+      (wikiInfo.enableListings
+        ? listingSpec
+            .find(l => l.directory === 'groups/by-category')
+        : null),
+  }),
+
+  query(sprawl, group) {
+    const groups = group.category.groups;
+    const index = groups.indexOf(group);
+
+    return {
+      previousGroup:
+        (index > 0
+          ? groups[index - 1]
+          : null),
+
+      nextGroup:
+        (index < groups.length - 1
+          ? groups[index + 1]
+          : null),
+    };
+  },
+
+  relations(relation, query, sprawl, _group) {
+    const relations = {};
+
+    relations.secondaryNav =
+      relation('generateSecondaryNav');
+
+    if (sprawl.groupsByCategoryListing) {
+      relations.categoryLink =
+        relation('linkListing', sprawl.groupsByCategoryListing);
+    }
+
+    relations.colorVariables =
+      relation('generateColorStyleVariables');
+
+    if (query.previousGroup || query.nextGroup) {
+      relations.previousNextLinks =
+        relation('generatePreviousNextLinks');
+    }
+
+    relations.previousGroupLink =
+      (query.previousGroup
+        ? relation('linkGroupDynamically', query.previousGroup)
+        : null);
+
+    relations.nextGroupLink =
+      (query.nextGroup
+        ? relation('linkGroupDynamically', query.nextGroup)
+        : null);
+
+    return relations;
+  },
+
+  data: (query, sprawl, group) => ({
+    categoryName: group.category.name,
+    categoryColor: group.category.color,
+  }),
+
+  generate(data, relations, {html, language}) {
+    const {content: previousNextPart} =
+      relations.previousNextLinks.slots({
+        previousLink: relations.previousGroupLink,
+        nextLink: relations.nextGroupLink,
+        id: true,
+      });
+
+    const {categoryLink} = relations;
+
+    categoryLink?.setSlot('content', data.categoryName);
+
+    return relations.secondaryNav.slots({
+      class: 'nav-links-groups',
+      content:
+        (!relations.previousGroupLink && !relations.nextGroupLink
+          ? categoryLink
+          : html.tag('span',
+              {style: relations.colorVariables.slot('color', data.categoryColor).content},
+              [
+                categoryLink.slot('color', false),
+                `(${language.formatUnitList(previousNextPart)})`,
+              ])),
+    });
+  },
+};
diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js
index 95a5dbe..cd831ba 100644
--- a/src/content/dependencies/generatePageLayout.js
+++ b/src/content/dependencies/generatePageLayout.js
@@ -105,7 +105,7 @@ export default {
     color: {validate: v => v.isColor},
 
     styleRules: {
-      validate: v => v.sparseArrayOf(v.isString),
+      validate: v => v.sparseArrayOf(v.isHTML),
       default: [],
     },
 
@@ -183,7 +183,7 @@ export default {
           } else {
             aggregate.call(v.validateProperties({
               path: v.strictArrayOf(v.isString),
-              title: v.isString,
+              title: v.isHTML,
             }), {
               path: object.path,
               title: object.title,
@@ -394,6 +394,10 @@ export default {
 
     const sidebarLeftHTML = generateSidebarHTML('leftSidebar', 'sidebar-left');
     const sidebarRightHTML = generateSidebarHTML('rightSidebar', 'sidebar-right');
+
+    const hasSidebarLeft = !html.isBlank(sidebarLeftHTML);
+    const hasSidebarRight = !html.isBlank(sidebarRightHTML);
+
     const collapseSidebars = slots.leftSidebarCollapse && slots.rightSidebarCollapse;
 
     const hasID = (() => {
@@ -422,20 +426,20 @@ export default {
             processSkippers([
               {condition: true, id: 'content', string: 'content'},
               {
-                condition: !html.isBlank(sidebarLeftHTML),
+                condition: hasSidebarLeft,
                 id: 'sidebar-left',
                 string:
-                  (html.isBlank(sidebarRightHTML)
-                    ? 'sidebar'
-                    : 'sidebar.left'),
+                  (hasSidebarRight
+                    ? 'sidebar.left'
+                    : 'sidebar'),
               },
               {
-                condition: !html.isBlank(sidebarRightHTML),
+                condition: hasSidebarRight,
                 id: 'sidebar-right',
                 string:
-                  (html.isBlank(sidebarLeftHTML)
-                    ? 'sidebar'
-                    : 'sidebar.right'),
+                  (hasSidebarLeft
+                    ? 'sidebar.right'
+                    : 'sidebar'),
               },
               {condition: navHTML, id: 'header', string: 'header'},
               {condition: footerHTML, id: 'footer', string: 'footer'},
@@ -507,11 +511,6 @@ export default {
           class: [
             'layout-columns',
             !collapseSidebars && 'vertical-when-thin',
-            (sidebarLeftHTML || sidebarRightHTML) && 'has-one-sidebar',
-            (sidebarLeftHTML && sidebarRightHTML) && 'has-two-sidebars',
-            !(sidebarLeftHTML || sidebarRightHTML) && 'has-zero-sidebars',
-            sidebarLeftHTML && 'has-sidebar-left',
-            sidebarRightHTML && 'has-sidebar-right',
           ],
         },
         [
@@ -521,7 +520,7 @@ export default {
         ]),
       slots.bannerPosition === 'bottom' && slots.banner,
       footerHTML,
-    ].filter(Boolean).join('\n');
+    ];
 
     const pageHTML = html.tags([
       `<!DOCTYPE html>`,
@@ -609,7 +608,7 @@ export default {
 
             html.tag('link', {
               rel: 'stylesheet',
-              href: to('shared.staticFile', 'site4.css', cachebust),
+              href: to('shared.staticFile', 'site5.css', cachebust),
             }),
 
             html.tag('style', [
@@ -624,12 +623,22 @@ export default {
           ]),
 
           html.tag('body',
-            // {style: body.style || ''},
             [
-              html.tag('div', {id: 'page-container'}, [
-                skippersHTML,
-                layoutHTML,
-              ]),
+              html.tag('div',
+                {
+                  id: 'page-container',
+                  class: [
+                    (hasSidebarLeft || hasSidebarRight) && 'has-one-sidebar',
+                    (hasSidebarLeft && hasSidebarRight) && 'has-two-sidebars',
+                    !(hasSidebarLeft || hasSidebarRight) && 'has-zero-sidebars',
+                    hasSidebarLeft && 'has-sidebar-left',
+                    hasSidebarRight && 'has-sidebar-right',
+                  ],
+                },
+                [
+                  skippersHTML,
+                  layoutHTML,
+                ]),
 
               // infoCardHTML,
               imageOverlayHTML,
diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js
index 334c542..1083d86 100644
--- a/src/content/dependencies/generateTrackInfoPage.js
+++ b/src/content/dependencies/generateTrackInfoPage.js
@@ -44,14 +44,17 @@ export default {
       relation('generatePageLayout');
 
     relations.albumStyleRules =
-      relation('generateAlbumStyleRules', track.album);
+      relation('generateAlbumStyleRules', track.album, track);
 
     relations.socialEmbed =
       relation('generateTrackSocialEmbed', track);
 
     relations.artistChronologyContributions =
       getChronologyRelations(track, {
-        contributions: [...track.artistContribs, ...track.contributorContribs],
+        contributions: [
+          ...track.artistContribs ?? [],
+          ...track.contributorContribs ?? [],
+        ],
 
         linkArtist: artist => relation('linkArtist', artist),
         linkThing: track => relation('linkTrack', track),
@@ -65,7 +68,7 @@ export default {
 
     relations.coverArtistChronologyContributions =
       getChronologyRelations(track, {
-        contributions: track.coverArtistContribs,
+        contributions: track.coverArtistContribs ?? [],
 
         linkArtist: artist => relation('linkArtist', artist),
 
diff --git a/src/content/dependencies/generateTrackList.js b/src/content/dependencies/generateTrackList.js
index f001c3b..65f5552 100644
--- a/src/content/dependencies/generateTrackList.js
+++ b/src/content/dependencies/generateTrackList.js
@@ -1,4 +1,4 @@
-import {empty} from '#sugar';
+import {empty, stitchArrays} from '#sugar';
 
 export default {
   contentDependencies: ['linkTrack', 'linkContribution'],
@@ -11,14 +11,17 @@ export default {
     }
 
     return {
-      items: tracks.map(track => ({
-        trackLink:
-          relation('linkTrack', track),
+      trackLinks:
+        tracks
+          .map(track => relation('linkTrack', track)),
 
-        contributionLinks:
-          track.artistContribs
-            .map(contrib => relation('linkContribution', contrib)),
-      })),
+      contributionLinks:
+        tracks
+          .map(track =>
+            (empty(track.artistContribs)
+              ? null
+              : track.artistContribs
+                  .map(contrib => relation('linkContribution', contrib)))),
     };
   },
 
@@ -28,22 +31,28 @@ export default {
   },
 
   generate(relations, slots, {html, language}) {
-    return html.tag('ul',
-      relations.items.map(({trackLink, contributionLinks}) =>
-        html.tag('li',
-          language.$('trackList.item.withArtists', {
-            track: trackLink,
-            by:
-              html.tag('span', {class: 'by'},
-                language.$('trackList.item.withArtists.by', {
-                  artists:
-                    language.formatConjunctionList(
-                      contributionLinks.map(link =>
-                        link.slots({
-                          showContribution: slots.showContribution,
-                          showIcons: slots.showIcons,
-                        }))),
-                })),
-          }))));
+    return (
+      html.tag('ul',
+        stitchArrays({
+          trackLink: relations.trackLinks,
+          contributionLinks: relations.contributionLinks,
+        }).map(({trackLink, contributionLinks}) =>
+            html.tag('li',
+              (empty(contributionLinks)
+                ? trackLink
+                : language.$('trackList.item.withArtists', {
+                    track: trackLink,
+                    by:
+                      html.tag('span', {class: 'by'},
+                        language.$('trackList.item.withArtists.by', {
+                          artists:
+                            language.formatConjunctionList(
+                              contributionLinks.map(link =>
+                                link.slots({
+                                  showContribution: slots.showContribution,
+                                  showIcons: slots.showIcons,
+                                }))),
+                        })),
+                  }))))));
   },
 };
diff --git a/src/content/dependencies/generateWikiHomeAlbumsRow.js b/src/content/dependencies/generateWikiHomeAlbumsRow.js
index 99c1be5..cb0860f 100644
--- a/src/content/dependencies/generateWikiHomeAlbumsRow.js
+++ b/src/content/dependencies/generateWikiHomeAlbumsRow.js
@@ -16,7 +16,7 @@ export default {
   sprawl({albumData}, row) {
     const sprawl = {};
 
-    switch (row.sourceGroupByRef) {
+    switch (row.sourceGroup) {
       case 'new-releases':
         sprawl.albums = getNewReleases(row.countAlbumsFromGroup, {albumData});
         break;
diff --git a/src/content/dependencies/image.js b/src/content/dependencies/image.js
index 71b905f..6c0aeec 100644
--- a/src/content/dependencies/image.js
+++ b/src/content/dependencies/image.js
@@ -1,11 +1,16 @@
+import {logInfo, logWarn} from '#cli';
 import {empty} from '#sugar';
 
 export default {
   extraDependencies: [
-    'getSizeOfImageFile',
+    'checkIfImagePathHasCachedThumbnails',
+    'getDimensionsOfImagePath',
+    'getSizeOfImagePath',
+    'getThumbnailEqualOrSmaller',
+    'getThumbnailsAvailableForDimensions',
     'html',
     'language',
-    'thumb',
+    'missingImagePaths',
     'to',
   ],
 
@@ -52,10 +57,14 @@ export default {
   },
 
   generate(data, slots, {
-    getSizeOfImageFile,
+    checkIfImagePathHasCachedThumbnails,
+    getDimensionsOfImagePath,
+    getSizeOfImagePath,
+    getThumbnailEqualOrSmaller,
+    getThumbnailsAvailableForDimensions,
     html,
     language,
-    thumb,
+    missingImagePaths,
     to,
   }) {
     let originalSrc;
@@ -68,43 +77,48 @@ export default {
       originalSrc = '';
     }
 
-    const thumbSrc =
-      originalSrc &&
-        (slots.thumb
-          ? thumb[slots.thumb](originalSrc)
-          : originalSrc);
+    let mediaSrc = null;
+    if (originalSrc.startsWith(to('media.root'))) {
+      mediaSrc =
+        originalSrc
+          .slice(to('media.root').length)
+          .replace(/^\//, '');
+    }
 
-    const willLink = typeof slots.link === 'string' || slots.link;
-    const customLink = typeof slots.link === 'string';
+    const isMissingImageFile =
+      missingImagePaths.includes(mediaSrc);
+
+    if (isMissingImageFile) {
+      logInfo`No image file for ${mediaSrc} - build again for list of missing images.`;
+    }
+
+    const willLink =
+      !isMissingImageFile &&
+      (typeof slots.link === 'string' || slots.link);
+
+    const customLink =
+      typeof slots.link === 'string';
 
     const willReveal =
       slots.reveal &&
       originalSrc &&
+      !isMissingImageFile &&
       !empty(data.contentWarnings);
 
     const willSquare = slots.square;
 
     const idOnImg = willLink ? null : slots.id;
     const idOnLink = willLink ? slots.id : null;
+
     const classOnImg = willLink ? null : slots.class;
     const classOnLink = willLink ? slots.class : null;
 
-    if (!originalSrc) {
+    if (!originalSrc || isMissingImageFile) {
       return prepare(
         html.tag('div', {class: 'image-text-area'},
-          slots.missingSourceContent));
-    }
-
-    let fileSize = null;
-    if (willLink) {
-      const mediaRoot = to('media.root');
-      if (originalSrc.startsWith(mediaRoot)) {
-        fileSize =
-          getSizeOfImageFile(
-            originalSrc
-              .slice(mediaRoot.length)
-              .replace(/^\//, ''));
-      }
+          (html.isBlank(slots.missingSourceContent)
+            ? language.$(`misc.missingImage`)
+            : slots.missingSourceContent)));
     }
 
     let reveal = null;
@@ -119,22 +133,84 @@ export default {
       ];
     }
 
+    const hasThumbnails =
+      mediaSrc &&
+      checkIfImagePathHasCachedThumbnails(mediaSrc);
+
+    // Warn for images that *should* have cached thumbnail information but are
+    // missing from the thumbs cache.
+    if (
+      slots.thumb &&
+      !hasThumbnails &&
+      !mediaSrc.endsWith('.gif')
+    ) {
+      logWarn`No thumbnail info cached: ${mediaSrc} - displaying original image here (instead of ${slots.thumb})`;
+    }
+
+    // Important to note that these might not be set at all, even if
+    // slots.thumb was provided.
+    let thumbSrc = null;
+    let availableThumbs = null;
+    let originalLength = null;
+
+    if (hasThumbnails && slots.thumb) {
+      // Note: This provides mediaSrc to getThumbnailEqualOrSmaller, since
+      // it's the identifier which thumbnail utilities use to query from the
+      // thumbnail cache. But we use the result to operate on originalSrc,
+      // which is the HTML output-appropriate path including `../../` or
+      // another alternate base path.
+      const selectedSize = getThumbnailEqualOrSmaller(slots.thumb, mediaSrc);
+      thumbSrc = originalSrc.replace(/\.(jpg|png)$/, `.${selectedSize}.jpg`);
+
+      const dimensions = getDimensionsOfImagePath(mediaSrc);
+      availableThumbs = getThumbnailsAvailableForDimensions(dimensions);
+
+      const [width, height] = dimensions;
+      originalLength = Math.max(width, height)
+    }
+
+    let fileSize = null;
+    if (willLink && mediaSrc) {
+      fileSize = getSizeOfImagePath(mediaSrc);
+    }
+
     const imgAttributes = {
       id: idOnImg,
       class: classOnImg,
       alt: slots.alt,
       width: slots.width,
       height: slots.height,
-      'data-original-size': fileSize,
-      'data-no-image-preview': customLink,
     };
 
+    if (customLink) {
+      imgAttributes['data-no-image-preview'] = true;
+    }
+
+    // These attributes are only relevant when a thumbnail are available *and*
+    // being used.
+    if (hasThumbnails && slots.thumb) {
+      if (fileSize) {
+        imgAttributes['data-original-size'] = fileSize;
+      }
+
+      if (originalLength) {
+        imgAttributes['data-original-length'] = originalLength;
+      }
+
+      if (!empty(availableThumbs)) {
+        imgAttributes['data-thumbs'] =
+          availableThumbs
+            .map(([name, size]) => `${name}:${size}`)
+            .join(' ');
+      }
+    }
+
     const nonlazyHTML =
       originalSrc &&
         prepare(
           html.tag('img', {
             ...imgAttributes,
-            src: thumbSrc,
+            src: thumbSrc ?? originalSrc,
           }));
 
     if (slots.lazy) {
@@ -145,7 +221,7 @@ export default {
             {
               ...imgAttributes,
               class: 'lazy',
-              'data-original': thumbSrc,
+              'data-original': thumbSrc ?? originalSrc,
             }),
           true),
       ]);
diff --git a/src/content/dependencies/index.js b/src/content/dependencies/index.js
index 3bc3484..58bac0d 100644
--- a/src/content/dependencies/index.js
+++ b/src/content/dependencies/index.js
@@ -6,7 +6,7 @@ import {fileURLToPath} from 'node:url';
 import chokidar from 'chokidar';
 import {ESLint} from 'eslint';
 
-import {color, logWarn} from '#cli';
+import {colors, logWarn} from '#cli';
 import contentFunction, {ContentFunctionSpecError} from '#content-function';
 import {annotateFunction} from '#sugar';
 
@@ -30,7 +30,6 @@ export function watchContentDependencies({
   const contentDependencies = {};
 
   let emittedReady = false;
-  let allDependenciesFulfilled = false;
   let closed = false;
 
   let _close = () => {};
@@ -77,12 +76,12 @@ export function watchContentDependencies({
   // prematurely find out there aren't any nulls - before the nulls have
   // been entered at all!).
 
-  readdir(metaDirname).then(files => {
+  readdir(watchPath).then(files => {
     if (closed) {
       return;
     }
 
-    const filePaths = files.map(file => path.join(metaDirname, file));
+    const filePaths = files.map(file => path.join(watchPath, file));
     for (const filePath of filePaths) {
       if (filePath === metaPath) continue;
       const functionName = getFunctionName(filePath);
@@ -91,7 +90,7 @@ export function watchContentDependencies({
       }
     }
 
-    const watcher = chokidar.watch(metaDirname);
+    const watcher = chokidar.watch(watchPath);
 
     watcher.on('all', (event, filePath) => {
       if (!['add', 'change'].includes(event)) return;
@@ -178,7 +177,14 @@ export function watchContentDependencies({
       // Just skip newly created files. They'll be processed again when
       // written.
       if (spec === undefined) {
-        contentDependencies[functionName] = null;
+        // For practical purposes the file is treated as though it doesn't
+        // even exist (undefined), rather than not being ready yet (null).
+        // Apart from if existing contents of the file were erased (but not
+        // the file itself), this value might already be set (to null!) by
+        // the readdir performed at the beginning to evaluate which files
+        // should be read and processed at least once before reporting all
+        // dependencies as ready.
+        delete contentDependencies[functionName];
         return;
       }
 
@@ -192,7 +198,7 @@ export function watchContentDependencies({
 
       if (logging && emittedReady) {
         const timestamp = new Date().toLocaleString('en-US', {timeStyle: 'medium'});
-        console.log(color.green(`[${timestamp}] Updated ${functionName}`));
+        console.log(colors.green(`[${timestamp}] Updated ${functionName}`));
       }
 
       contentDependencies[functionName] = fn;
@@ -219,9 +225,9 @@ export function watchContentDependencies({
       }
 
       if (typeof error === 'string') {
-        console.error(color.yellow(error));
+        console.error(colors.yellow(error));
       } else if (error instanceof ContentFunctionSpecError) {
-        console.error(color.yellow(error.message));
+        console.error(colors.yellow(error.message));
       } else {
         console.error(error);
       }
diff --git a/src/content/dependencies/linkAlbumDynamically.js b/src/content/dependencies/linkAlbumDynamically.js
new file mode 100644
index 0000000..3adc64d
--- /dev/null
+++ b/src/content/dependencies/linkAlbumDynamically.js
@@ -0,0 +1,14 @@
+export default {
+  contentDependencies: ['linkAlbumGallery', 'linkAlbum'],
+  extraDependencies: ['pagePath'],
+
+  relations: (relation, album) => ({
+    galleryLink: relation('linkAlbumGallery', album),
+    infoLink: relation('linkAlbum', album),
+  }),
+
+  generate: (relations, {pagePath}) =>
+    (pagePath[0] === 'albumGallery'
+      ? relations.galleryLink
+      : relations.infoLink),
+};
diff --git a/src/content/dependencies/linkFlashAct.js b/src/content/dependencies/linkFlashAct.js
new file mode 100644
index 0000000..fbb819e
--- /dev/null
+++ b/src/content/dependencies/linkFlashAct.js
@@ -0,0 +1,14 @@
+export default {
+  contentDependencies: ['linkThing'],
+  extraDependencies: ['html'],
+
+  relations: (relation, flashAct) =>
+    ({link: relation('linkThing', 'localized.flashActGallery', flashAct)}),
+
+  data: (flashAct) =>
+    ({name: flashAct.name}),
+
+  generate: (data, relations, {html}) =>
+    relations.link
+      .slot('content', new html.Tag(null, null, data.name)),
+};
diff --git a/src/content/dependencies/linkGroupDynamically.js b/src/content/dependencies/linkGroupDynamically.js
new file mode 100644
index 0000000..90303ed
--- /dev/null
+++ b/src/content/dependencies/linkGroupDynamically.js
@@ -0,0 +1,14 @@
+export default {
+  contentDependencies: ['linkGroupGallery', 'linkGroup'],
+  extraDependencies: ['pagePath'],
+
+  relations: (relation, group) => ({
+    galleryLink: relation('linkGroupGallery', group),
+    infoLink: relation('linkGroup', group),
+  }),
+
+  generate: (relations, {pagePath}) =>
+    (pagePath[0] === 'groupGallery'
+      ? relations.galleryLink
+      : relations.infoLink),
+};
diff --git a/src/content/dependencies/linkTemplate.js b/src/content/dependencies/linkTemplate.js
index 1cf64c5..d9af726 100644
--- a/src/content/dependencies/linkTemplate.js
+++ b/src/content/dependencies/linkTemplate.js
@@ -15,8 +15,9 @@ export default {
     href: {type: 'string'},
     path: {validate: v => v.validateArrayItems(v.isString)},
     hash: {type: 'string'},
+    linkless: {type: 'boolean', default: false},
 
-    tooltip: {validate: v => v.isString},
+    tooltip: {type: 'string'},
     attributes: {validate: v => v.isAttributes},
     color: {validate: v => v.isColor},
     content: {type: 'html'},
@@ -29,27 +30,33 @@ export default {
     language,
     to,
   }) {
-    let href = slots.href;
+    let href;
     let style;
     let title;
 
-    if (href) {
-      href = encodeURI(href);
-    } else if (!empty(slots.path)) {
-      href = to(...slots.path);
-    }
+    if (slots.linkless) {
+      href = null;
+    } else {
+      if (slots.href) {
+        href = encodeURI(slots.href);
+      } else if (!empty(slots.path)) {
+        href = to(...slots.path);
+      } else {
+        href = '';
+      }
 
-    if (appendIndexHTML) {
-      if (
-        /^(?!https?:\/\/).+\/$/.test(href) &&
-        href.endsWith('/')
-      ) {
-        href += 'index.html';
+      if (appendIndexHTML) {
+        if (
+          /^(?!https?:\/\/).+\/$/.test(href) &&
+          href.endsWith('/')
+        ) {
+          href += 'index.html';
+        }
       }
-    }
 
-    if (slots.hash) {
-      href += (slots.hash.startsWith('#') ? '' : '#') + slots.hash;
+      if (slots.hash) {
+        href += (slots.hash.startsWith('#') ? '' : '#') + slots.hash;
+      }
     }
 
     if (slots.color) {
diff --git a/src/content/dependencies/linkThing.js b/src/content/dependencies/linkThing.js
index e3e2608..b20b132 100644
--- a/src/content/dependencies/linkThing.js
+++ b/src/content/dependencies/linkThing.js
@@ -1,6 +1,6 @@
 export default {
   contentDependencies: ['linkTemplate'],
-  extraDependencies: ['html'],
+  extraDependencies: ['html', 'language'],
 
   relations(relation) {
     return {
@@ -26,7 +26,7 @@ export default {
     preferShortName: {type: 'boolean', default: false},
 
     tooltip: {
-      validate: v => v.oneOf(v.isBoolean, v.isString),
+      validate: v => v.oneOf(v.isBoolean, v.isHTML),
       default: false,
     },
 
@@ -36,12 +36,13 @@ export default {
     },
 
     anchor: {type: 'boolean', default: false},
+    linkless: {type: 'boolean', default: false},
 
     attributes: {validate: v => v.isAttributes},
     hash: {type: 'string'},
   },
 
-  generate(data, relations, slots, {html}) {
+  generate(data, relations, slots, {html, language}) {
     const path = [data.pathKey, data.directory];
 
     const name =
@@ -51,7 +52,7 @@ export default {
 
     const content =
       (html.isBlank(slots.content)
-        ? name
+        ? language.sanitize(name)
         : slots.content);
 
     let color = null;
@@ -78,6 +79,7 @@ export default {
 
         attributes: slots.attributes,
         hash: slots.hash,
+        linkless: slots.linkless,
       });
   },
 }
diff --git a/src/content/dependencies/listArtTagNetwork.js b/src/content/dependencies/listArtTagNetwork.js
new file mode 100644
index 0000000..b3a5474
--- /dev/null
+++ b/src/content/dependencies/listArtTagNetwork.js
@@ -0,0 +1 @@
+export default {generate() {}};
diff --git a/src/content/dependencies/listTracksWithExtra.js b/src/content/dependencies/listTracksWithExtra.js
index 73d25e3..c9f80f3 100644
--- a/src/content/dependencies/listTracksWithExtra.js
+++ b/src/content/dependencies/listTracksWithExtra.js
@@ -65,10 +65,14 @@ export default {
         stitchArrays({
           albumLink: relations.albumLinks,
           date: data.dates,
-        }).map(({albumLink, date}) => ({
-            album: albumLink,
-            date: language.formatDate(date),
-          })),
+        }).map(({albumLink, date}) =>
+            (date
+              ? {
+                  stringsKey: 'withDate',
+                  album: albumLink,
+                  date: language.formatDate(date),
+                }
+              : {album: albumLink})),
 
       chunkRows:
         relations.trackLinks
diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js
index 9a5ac45..3c2c352 100644
--- a/src/content/dependencies/transformContent.js
+++ b/src/content/dependencies/transformContent.js
@@ -53,6 +53,10 @@ export const replacerSpec = {
       }
     },
   },
+  'flash-act': {
+    find: 'flashAct',
+    link: 'flashAct',
+  },
   group: {
     find: 'group',
     link: 'groupInfo',
@@ -119,6 +123,7 @@ const linkThingRelationMap = {
   artist: 'linkArtist',
   artistGallery: 'linkArtistGallery',
   flash: 'linkFlash',
+  flashAct: 'linkFlashAct',
   groupInfo: 'linkGroup',
   groupGallery: 'linkGroupGallery',
   listing: 'linkListing',
diff --git a/src/data/composite/control-flow/exitWithoutDependency.js b/src/data/composite/control-flow/exitWithoutDependency.js
new file mode 100644
index 0000000..c660a7e
--- /dev/null
+++ b/src/data/composite/control-flow/exitWithoutDependency.js
@@ -0,0 +1,35 @@
+// Early exits if a dependency isn't available.
+// See withResultOfAvailabilityCheck for {mode} options.
+
+import {input, templateCompositeFrom} from '#composite';
+
+import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js';
+import withResultOfAvailabilityCheck from './withResultOfAvailabilityCheck.js';
+
+export default templateCompositeFrom({
+  annotation: `exitWithoutDependency`,
+
+  inputs: {
+    dependency: input({acceptsNull: true}),
+    mode: inputAvailabilityCheckMode(),
+    value: input({defaultValue: null}),
+  },
+
+  steps: () => [
+    withResultOfAvailabilityCheck({
+      from: input('dependency'),
+      mode: input('mode'),
+    }),
+
+    {
+      dependencies: ['#availability', input('value')],
+      compute: (continuation, {
+        ['#availability']: availability,
+        [input('value')]: value,
+      }) =>
+        (availability
+          ? continuation()
+          : continuation.exit(value)),
+    },
+  ],
+});
diff --git a/src/data/composite/control-flow/exitWithoutUpdateValue.js b/src/data/composite/control-flow/exitWithoutUpdateValue.js
new file mode 100644
index 0000000..244b323
--- /dev/null
+++ b/src/data/composite/control-flow/exitWithoutUpdateValue.js
@@ -0,0 +1,24 @@
+// Early exits if this property's update value isn't available.
+// See withResultOfAvailabilityCheck for {mode} options.
+
+import {input, templateCompositeFrom} from '#composite';
+
+import exitWithoutDependency from './exitWithoutDependency.js';
+import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js';
+
+export default templateCompositeFrom({
+  annotation: `exitWithoutUpdateValue`,
+
+  inputs: {
+    mode: inputAvailabilityCheckMode(),
+    value: input({defaultValue: null}),
+  },
+
+  steps: () => [
+    exitWithoutDependency({
+      dependency: input.updateValue(),
+      mode: input('mode'),
+      value: input('value'),
+    }),
+  ],
+});
diff --git a/src/data/composite/control-flow/exposeConstant.js b/src/data/composite/control-flow/exposeConstant.js
new file mode 100644
index 0000000..e043547
--- /dev/null
+++ b/src/data/composite/control-flow/exposeConstant.js
@@ -0,0 +1,26 @@
+// Exposes a constant value exactly as it is; like exposeDependency, this
+// is typically the base of a composition serving as a particular property
+// descriptor. It generally follows steps which will conditionally early
+// exit with some other value, with the exposeConstant base serving as the
+// fallback default value.
+
+import {input, templateCompositeFrom} from '#composite';
+
+export default templateCompositeFrom({
+  annotation: `exposeConstant`,
+
+  compose: false,
+
+  inputs: {
+    value: input.staticValue(),
+  },
+
+  steps: () => [
+    {
+      dependencies: [input('value')],
+      compute: ({
+        [input('value')]: value,
+      }) => value,
+    },
+  ],
+});
diff --git a/src/data/composite/control-flow/exposeDependency.js b/src/data/composite/control-flow/exposeDependency.js
new file mode 100644
index 0000000..3aa3d03
--- /dev/null
+++ b/src/data/composite/control-flow/exposeDependency.js
@@ -0,0 +1,28 @@
+// Exposes a dependency exactly as it is; this is typically the base of a
+// composition which was created to serve as one property's descriptor.
+//
+// Please note that this *doesn't* verify that the dependency exists, so
+// if you provide the wrong name or it hasn't been set by a previous
+// compositional step, the property will be exposed as undefined instead
+// of null.
+
+import {input, templateCompositeFrom} from '#composite';
+
+export default templateCompositeFrom({
+  annotation: `exposeDependency`,
+
+  compose: false,
+
+  inputs: {
+    dependency: input.staticDependency({acceptsNull: true}),
+  },
+
+  steps: () => [
+    {
+      dependencies: [input('dependency')],
+      compute: ({
+        [input('dependency')]: dependency
+      }) => dependency,
+    },
+  ],
+});
diff --git a/src/data/composite/control-flow/exposeDependencyOrContinue.js b/src/data/composite/control-flow/exposeDependencyOrContinue.js
new file mode 100644
index 0000000..0f7f223
--- /dev/null
+++ b/src/data/composite/control-flow/exposeDependencyOrContinue.js
@@ -0,0 +1,34 @@
+// Exposes a dependency as it is, or continues if it's unavailable.
+// See withResultOfAvailabilityCheck for {mode} options.
+
+import {input, templateCompositeFrom} from '#composite';
+
+import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js';
+import withResultOfAvailabilityCheck from './withResultOfAvailabilityCheck.js';
+
+export default templateCompositeFrom({
+  annotation: `exposeDependencyOrContinue`,
+
+  inputs: {
+    dependency: input({acceptsNull: true}),
+    mode: inputAvailabilityCheckMode(),
+  },
+
+  steps: () => [
+    withResultOfAvailabilityCheck({
+      from: input('dependency'),
+      mode: input('mode'),
+    }),
+
+    {
+      dependencies: ['#availability', input('dependency')],
+      compute: (continuation, {
+        ['#availability']: availability,
+        [input('dependency')]: dependency,
+      }) =>
+        (availability
+          ? continuation.exit(dependency)
+          : continuation()),
+    },
+  ],
+});
diff --git a/src/data/composite/control-flow/exposeUpdateValueOrContinue.js b/src/data/composite/control-flow/exposeUpdateValueOrContinue.js
new file mode 100644
index 0000000..1f94b33
--- /dev/null
+++ b/src/data/composite/control-flow/exposeUpdateValueOrContinue.js
@@ -0,0 +1,40 @@
+// Exposes the update value of an {update: true} property as it is,
+// or continues if it's unavailable.
+//
+// See withResultOfAvailabilityCheck for {mode} options.
+//
+// Provide {validate} here to conveniently set a custom validation check
+// for this property's update value.
+//
+
+import {input, templateCompositeFrom} from '#composite';
+
+import exposeDependencyOrContinue from './exposeDependencyOrContinue.js';
+import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js';
+
+export default templateCompositeFrom({
+  annotation: `exposeUpdateValueOrContinue`,
+
+  inputs: {
+    mode: inputAvailabilityCheckMode(),
+
+    validate: input({
+      type: 'function',
+      defaultValue: null,
+    }),
+  },
+
+  update: ({
+    [input.staticValue('validate')]: validate,
+  }) =>
+    (validate
+      ? {validate}
+      : {}),
+
+  steps: () => [
+    exposeDependencyOrContinue({
+      dependency: input.updateValue(),
+      mode: input('mode'),
+    }),
+  ],
+});
diff --git a/src/data/composite/control-flow/index.js b/src/data/composite/control-flow/index.js
new file mode 100644
index 0000000..dfc53db
--- /dev/null
+++ b/src/data/composite/control-flow/index.js
@@ -0,0 +1,9 @@
+export {default as exitWithoutDependency} from './exitWithoutDependency.js';
+export {default as exitWithoutUpdateValue} from './exitWithoutUpdateValue.js';
+export {default as exposeConstant} from './exposeConstant.js';
+export {default as exposeDependency} from './exposeDependency.js';
+export {default as exposeDependencyOrContinue} from './exposeDependencyOrContinue.js';
+export {default as exposeUpdateValueOrContinue} from './exposeUpdateValueOrContinue.js';
+export {default as raiseOutputWithoutDependency} from './raiseOutputWithoutDependency.js';
+export {default as raiseOutputWithoutUpdateValue} from './raiseOutputWithoutUpdateValue.js';
+export {default as withResultOfAvailabilityCheck} from './withResultOfAvailabilityCheck.js';
diff --git a/src/data/composite/control-flow/inputAvailabilityCheckMode.js b/src/data/composite/control-flow/inputAvailabilityCheckMode.js
new file mode 100644
index 0000000..8008fde
--- /dev/null
+++ b/src/data/composite/control-flow/inputAvailabilityCheckMode.js
@@ -0,0 +1,9 @@
+import {input} from '#composite';
+import {is} from '#validators';
+
+export default function inputAvailabilityCheckMode() {
+  return input({
+    validate: is('null', 'empty', 'falsy', 'index'),
+    defaultValue: 'null',
+  });
+}
diff --git a/src/data/composite/control-flow/raiseOutputWithoutDependency.js b/src/data/composite/control-flow/raiseOutputWithoutDependency.js
new file mode 100644
index 0000000..03d8036
--- /dev/null
+++ b/src/data/composite/control-flow/raiseOutputWithoutDependency.js
@@ -0,0 +1,39 @@
+// Raises if a dependency isn't available.
+// See withResultOfAvailabilityCheck for {mode} options.
+
+import {input, templateCompositeFrom} from '#composite';
+
+import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js';
+import withResultOfAvailabilityCheck from './withResultOfAvailabilityCheck.js';
+
+export default templateCompositeFrom({
+  annotation: `raiseOutputWithoutDependency`,
+
+  inputs: {
+    dependency: input({acceptsNull: true}),
+    mode: inputAvailabilityCheckMode(),
+    output: input.staticValue({defaultValue: {}}),
+  },
+
+  outputs: ({
+    [input.staticValue('output')]: output,
+  }) => Object.keys(output),
+
+  steps: () => [
+    withResultOfAvailabilityCheck({
+      from: input('dependency'),
+      mode: input('mode'),
+    }),
+
+    {
+      dependencies: ['#availability', input('output')],
+      compute: (continuation, {
+        ['#availability']: availability,
+        [input('output')]: output,
+      }) =>
+        (availability
+          ? continuation()
+          : continuation.raiseOutputAbove(output)),
+    },
+  ],
+});
diff --git a/src/data/composite/control-flow/raiseOutputWithoutUpdateValue.js b/src/data/composite/control-flow/raiseOutputWithoutUpdateValue.js
new file mode 100644
index 0000000..3c39f5b
--- /dev/null
+++ b/src/data/composite/control-flow/raiseOutputWithoutUpdateValue.js
@@ -0,0 +1,47 @@
+// Raises if this property's update value isn't available.
+// See withResultOfAvailabilityCheck for {mode} options!
+
+import {input, templateCompositeFrom} from '#composite';
+
+import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js';
+import withResultOfAvailabilityCheck from './withResultOfAvailabilityCheck.js';
+
+export default templateCompositeFrom({
+  annotation: `raiseOutputWithoutUpdateValue`,
+
+  inputs: {
+    mode: inputAvailabilityCheckMode(),
+    output: input.staticValue({defaultValue: {}}),
+  },
+
+  outputs: ({
+    [input.staticValue('output')]: output,
+  }) => Object.keys(output),
+
+  steps: () => [
+    withResultOfAvailabilityCheck({
+      from: input.updateValue(),
+      mode: input('mode'),
+    }),
+
+    // TODO: A bit of a kludge, below. Other "do something with the update
+    // value" type functions can get by pretty much just passing that value
+    // as an input (input.updateValue()) into the corresponding "do something
+    // with a dependency/arbitrary value" function. But we can't do that here,
+    // because the special behavior, raiseOutputAbove(), only works to raise
+    // output above the composition it's *directly* nested in. Other languages
+    // have a throw/catch system that might serve as inspiration for something
+    // better here.
+
+    {
+      dependencies: ['#availability', input('output')],
+      compute: (continuation, {
+        ['#availability']: availability,
+        [input('output')]: output,
+      }) =>
+        (availability
+          ? continuation()
+          : continuation.raiseOutputAbove(output)),
+    },
+  ],
+});
diff --git a/src/data/composite/control-flow/withResultOfAvailabilityCheck.js b/src/data/composite/control-flow/withResultOfAvailabilityCheck.js
new file mode 100644
index 0000000..a694201
--- /dev/null
+++ b/src/data/composite/control-flow/withResultOfAvailabilityCheck.js
@@ -0,0 +1,71 @@
+// Checks the availability of a dependency and provides the result to later
+// steps under '#availability' (by default). This is mainly intended for use
+// by the more specific utilities, which you should consider using instead.
+//
+// Customize {mode} to select one of these modes, or default to 'null':
+//
+// * 'null':  Check that the value isn't null (and not undefined either).
+// * 'empty': Check that the value is neither null, undefined, nor an empty
+//            array.
+// * 'falsy': Check that the value isn't false when treated as a boolean
+//            (nor an empty array). Keep in mind this will also be false
+//            for values like zero and the empty string!
+// * 'index': Check that the value is a number, and is at least zero.
+//
+// See also:
+//  - exitWithoutDependency
+//  - exitWithoutUpdateValue
+//  - exposeDependencyOrContinue
+//  - exposeUpdateValueOrContinue
+//  - raiseOutputWithoutDependency
+//  - raiseOutputWithoutUpdateValue
+//
+
+import {input, templateCompositeFrom} from '#composite';
+import {empty} from '#sugar';
+
+import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js';
+
+export default templateCompositeFrom({
+  annotation: `withResultOfAvailabilityCheck`,
+
+  inputs: {
+    from: input({acceptsNull: true}),
+    mode: inputAvailabilityCheckMode(),
+  },
+
+  outputs: ['#availability'],
+
+  steps: () => [
+    {
+      dependencies: [input('from'), input('mode')],
+
+      compute: (continuation, {
+        [input('from')]: value,
+        [input('mode')]: mode,
+      }) => {
+        let availability;
+
+        switch (mode) {
+          case 'null':
+            availability = value !== undefined && value !== null;
+            break;
+
+          case 'empty':
+            availability = value !== undefined && !empty(value);
+            break;
+
+          case 'falsy':
+            availability = !!value && (!Array.isArray(value) || !empty(value));
+            break;
+
+          case 'index':
+            availability = typeof value === 'number' && value >= 0;
+            break;
+        }
+
+        return continuation({'#availability': availability});
+      },
+    },
+  ],
+});
diff --git a/src/data/composite/data/excludeFromList.js b/src/data/composite/data/excludeFromList.js
new file mode 100644
index 0000000..718f229
--- /dev/null
+++ b/src/data/composite/data/excludeFromList.js
@@ -0,0 +1,56 @@
+// Filters particular values out of a list. Note that this will always
+// completely skip over null, but can be used to filter out any other
+// primitive or object value.
+//
+// See also:
+//  - fillMissingListItems
+//
+// More list utilities:
+//  - withFlattenedList
+//  - withPropertyFromList
+//  - withPropertiesFromList
+//  - withUnflattenedList
+//
+
+import {input, templateCompositeFrom} from '#composite';
+import {empty} from '#sugar';
+
+export default templateCompositeFrom({
+  annotation: `excludeFromList`,
+
+  inputs: {
+    list: input(),
+
+    item: input({defaultValue: null}),
+    items: input({type: 'array', defaultValue: null}),
+  },
+
+  outputs: ({
+    [input.staticDependency('list')]: list,
+  }) => [list ?? '#list'],
+
+  steps: () => [
+    {
+      dependencies: [
+        input.staticDependency('list'),
+        input('list'),
+        input('item'),
+        input('items'),
+      ],
+
+      compute: (continuation, {
+        [input.staticDependency('list')]: listName,
+        [input('list')]: listContents,
+        [input('item')]: excludeItem,
+        [input('items')]: excludeItems,
+      }) => continuation({
+        [listName ?? '#list']:
+          listContents.filter(item => {
+            if (excludeItem !== null && item === excludeItem) return false;
+            if (!empty(excludeItems) && excludeItems.includes(item)) return false;
+            return true;
+          }),
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/data/fillMissingListItems.js b/src/data/composite/data/fillMissingListItems.js
new file mode 100644
index 0000000..c06eced
--- /dev/null
+++ b/src/data/composite/data/fillMissingListItems.js
@@ -0,0 +1,51 @@
+// Replaces items of a list, which are null or undefined, with some fallback
+// value. By default, this replaces the passed dependency.
+//
+// See also:
+//  - excludeFromList
+//
+// More list utilities:
+//  - withFlattenedList
+//  - withPropertyFromList
+//  - withPropertiesFromList
+//  - withUnflattenedList
+//
+
+import {input, templateCompositeFrom} from '#composite';
+
+export default templateCompositeFrom({
+  annotation: `fillMissingListItems`,
+
+  inputs: {
+    list: input({type: 'array'}),
+    fill: input({acceptsNull: true}),
+  },
+
+  outputs: ({
+    [input.staticDependency('list')]: list,
+  }) => [list ?? '#list'],
+
+  steps: () => [
+    {
+      dependencies: [input('list'), input('fill')],
+      compute: (continuation, {
+        [input('list')]: list,
+        [input('fill')]: fill,
+      }) => continuation({
+        ['#filled']:
+          list.map(item => item ?? fill),
+      }),
+    },
+
+    {
+      dependencies: [input.staticDependency('list'), '#filled'],
+      compute: (continuation, {
+        [input.staticDependency('list')]: list,
+        ['#filled']: filled,
+      }) => continuation({
+        [list ?? '#list']:
+          filled,
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/data/index.js b/src/data/composite/data/index.js
new file mode 100644
index 0000000..ecd0512
--- /dev/null
+++ b/src/data/composite/data/index.js
@@ -0,0 +1,8 @@
+export {default as excludeFromList} from './excludeFromList.js';
+export {default as fillMissingListItems} from './fillMissingListItems.js';
+export {default as withFlattenedList} from './withFlattenedList.js';
+export {default as withPropertiesFromList} from './withPropertiesFromList.js';
+export {default as withPropertiesFromObject} from './withPropertiesFromObject.js';
+export {default as withPropertyFromList} from './withPropertyFromList.js';
+export {default as withPropertyFromObject} from './withPropertyFromObject.js';
+export {default as withUnflattenedList} from './withUnflattenedList.js';
diff --git a/src/data/composite/data/withFlattenedList.js b/src/data/composite/data/withFlattenedList.js
new file mode 100644
index 0000000..b08edb4
--- /dev/null
+++ b/src/data/composite/data/withFlattenedList.js
@@ -0,0 +1,47 @@
+// Flattens an array with one level of nested arrays, providing as dependencies
+// both the flattened array as well as the original starting indices of each
+// successive source array.
+//
+// See also:
+//  - withFlattenedList
+//
+// More list utilities:
+//  - excludeFromList
+//  - fillMissingListItems
+//  - withPropertyFromList
+//  - withPropertiesFromList
+//
+
+import {input, templateCompositeFrom} from '#composite';
+
+export default templateCompositeFrom({
+  annotation: `withFlattenedList`,
+
+  inputs: {
+    list: input({type: 'array'}),
+  },
+
+  outputs: ['#flattenedList', '#flattenedIndices'],
+
+  steps: () => [
+    {
+      dependencies: [input('list')],
+      compute(continuation, {
+        [input('list')]: sourceList,
+      }) {
+        const flattenedList = sourceList.flat();
+        const indices = [];
+        let lastEndIndex = 0;
+        for (const {length} of sourceList) {
+          indices.push(lastEndIndex);
+          lastEndIndex += length;
+        }
+
+        return continuation({
+          ['#flattenedList']: flattenedList,
+          ['#flattenedIndices']: indices,
+        });
+      },
+    },
+  ],
+});
diff --git a/src/data/composite/data/withPropertiesFromList.js b/src/data/composite/data/withPropertiesFromList.js
new file mode 100644
index 0000000..76ba696
--- /dev/null
+++ b/src/data/composite/data/withPropertiesFromList.js
@@ -0,0 +1,92 @@
+// Gets the listed properties from each of a list of objects, providing lists
+// of property values each into a dependency prefixed with the same name as the
+// list (by default).
+//
+// Like withPropertyFromList, this doesn't alter indices.
+//
+// See also:
+//  - withPropertiesFromObject
+//  - withPropertyFromList
+//
+// More list utilities:
+//  - excludeFromList
+//  - fillMissingListItems
+//  - withFlattenedList
+//  - withUnflattenedList
+//
+
+import {input, templateCompositeFrom} from '#composite';
+import {isString, validateArrayItems} from '#validators';
+
+export default templateCompositeFrom({
+  annotation: `withPropertiesFromList`,
+
+  inputs: {
+    list: input({type: 'array'}),
+
+    properties: input({
+      validate: validateArrayItems(isString),
+    }),
+
+    prefix: input.staticValue({type: 'string', defaultValue: null}),
+  },
+
+  outputs: ({
+    [input.staticDependency('list')]: list,
+    [input.staticValue('properties')]: properties,
+    [input.staticValue('prefix')]: prefix,
+  }) =>
+    (properties
+      ? properties.map(property =>
+          (prefix
+            ? `${prefix}.${property}`
+         : list
+            ? `${list}.${property}`
+            : `#list.${property}`))
+      : ['#lists']),
+
+  steps: () => [
+    {
+      dependencies: [input('list'), input('properties')],
+      compute: (continuation, {
+        [input('list')]: list,
+        [input('properties')]: properties,
+      }) => continuation({
+        ['#lists']:
+          Object.fromEntries(
+            properties.map(property => [
+              property,
+              list.map(item => item[property] ?? null),
+            ])),
+      }),
+    },
+
+    {
+      dependencies: [
+        input.staticDependency('list'),
+        input.staticValue('properties'),
+        input.staticValue('prefix'),
+        '#lists',
+      ],
+
+      compute: (continuation, {
+        [input.staticDependency('list')]: list,
+        [input.staticValue('properties')]: properties,
+        [input.staticValue('prefix')]: prefix,
+        ['#lists']: lists,
+      }) =>
+        (properties
+          ? continuation(
+              Object.fromEntries(
+                properties.map(property => [
+                  (prefix
+                    ? `${prefix}.${property}`
+                 : list
+                    ? `${list}.${property}`
+                    : `#list.${property}`),
+                  lists[property],
+                ])))
+          : continuation({'#lists': lists})),
+    },
+  ],
+});
diff --git a/src/data/composite/data/withPropertiesFromObject.js b/src/data/composite/data/withPropertiesFromObject.js
new file mode 100644
index 0000000..21726b5
--- /dev/null
+++ b/src/data/composite/data/withPropertiesFromObject.js
@@ -0,0 +1,87 @@
+// Gets the listed properties from some object, providing each property's value
+// as a dependency prefixed with the same name as the object (by default).
+// If the object itself is null, all provided dependencies will be null;
+// if it's missing only select properties, those will be provided as null.
+//
+// See also:
+//  - withPropertiesFromList
+//  - withPropertyFromObject
+//
+
+import {input, templateCompositeFrom} from '#composite';
+import {isString, validateArrayItems} from '#validators';
+
+export default templateCompositeFrom({
+  annotation: `withPropertiesFromObject`,
+
+  inputs: {
+    object: input({type: 'object', acceptsNull: true}),
+
+    properties: input({
+      type: 'array',
+      validate: validateArrayItems(isString),
+    }),
+
+    prefix: input.staticValue({type: 'string', defaultValue: null}),
+  },
+
+  outputs: ({
+    [input.staticDependency('object')]: object,
+    [input.staticValue('properties')]: properties,
+    [input.staticValue('prefix')]: prefix,
+  }) =>
+    (properties
+      ? properties.map(property =>
+          (prefix
+            ? `${prefix}.${property}`
+         : object
+            ? `${object}.${property}`
+            : `#object.${property}`))
+      : ['#object']),
+
+  steps: () => [
+    {
+      dependencies: [input('object'), input('properties')],
+      compute: (continuation, {
+        [input('object')]: object,
+        [input('properties')]: properties,
+      }) => continuation({
+        ['#entries']:
+          (object === null
+            ? properties.map(property => [property, null])
+            : properties.map(property => [property, object[property]])),
+      }),
+    },
+
+    {
+      dependencies: [
+        input.staticDependency('object'),
+        input.staticValue('properties'),
+        input.staticValue('prefix'),
+        '#entries',
+      ],
+
+      compute: (continuation, {
+        [input.staticDependency('object')]: object,
+        [input.staticValue('properties')]: properties,
+        [input.staticValue('prefix')]: prefix,
+        ['#entries']: entries,
+      }) =>
+        (properties
+          ? continuation(
+              Object.fromEntries(
+                entries.map(([property, value]) => [
+                  (prefix
+                    ? `${prefix}.${property}`
+                 : object
+                    ? `${object}.${property}`
+                    : `#object.${property}`),
+                  value ?? null,
+                ])))
+          : continuation({
+              ['#object']:
+                Object.fromEntries(entries),
+            })),
+    },
+  ],
+});
diff --git a/src/data/composite/data/withPropertyFromList.js b/src/data/composite/data/withPropertyFromList.js
new file mode 100644
index 0000000..1983ebb
--- /dev/null
+++ b/src/data/composite/data/withPropertyFromList.js
@@ -0,0 +1,82 @@
+// Gets a property from each of a list of objects (in a dependency) and
+// provides the results.
+//
+// This doesn't alter any list indices, so positions which were null in the
+// original list are kept null here. Objects which don't have the specified
+// property are retained in-place as null.
+//
+// See also:
+//  - withPropertiesFromList
+//  - withPropertyFromObject
+//
+// More list utilities:
+//  - excludeFromList
+//  - fillMissingListItems
+//  - withFlattenedList
+//  - withUnflattenedList
+//
+
+import {input, templateCompositeFrom} from '#composite';
+
+function getOutputName({list, property, prefix}) {
+  if (!property) return `#values`;
+  if (prefix) return `${prefix}.${property}`;
+  if (list) return `${list}.${property}`;
+  return `#list.${property}`;
+}
+
+export default templateCompositeFrom({
+  annotation: `withPropertyFromList`,
+
+  inputs: {
+    list: input({type: 'array'}),
+    property: input({type: 'string'}),
+    prefix: input.staticValue({type: 'string', defaultValue: null}),
+  },
+
+  outputs: ({
+    [input.staticDependency('list')]: list,
+    [input.staticValue('property')]: property,
+    [input.staticValue('prefix')]: prefix,
+  }) =>
+    [getOutputName({list, property, prefix})],
+
+  steps: () => [
+    {
+      dependencies: [input('list'), input('property')],
+      compute: (continuation, {
+        [input('list')]: list,
+        [input('property')]: property,
+      }) => continuation({
+        ['#values']:
+          list.map(item => item[property] ?? null),
+      }),
+    },
+
+    {
+      dependencies: [
+        input.staticDependency('list'),
+        input.staticValue('property'),
+        input.staticValue('prefix'),
+      ],
+
+      compute: (continuation, {
+        [input.staticDependency('list')]: list,
+        [input.staticValue('property')]: property,
+        [input.staticValue('prefix')]: prefix,
+      }) => continuation({
+        ['#outputName']:
+          getOutputName({list, property, prefix}),
+      }),
+    },
+
+    {
+      dependencies: ['#values', '#outputName'],
+      compute: (continuation, {
+        ['#values']: values,
+        ['#outputName']: outputName,
+      }) =>
+        continuation.raiseOutput({[outputName]: values}),
+    },
+  ],
+});
diff --git a/src/data/composite/data/withPropertyFromObject.js b/src/data/composite/data/withPropertyFromObject.js
new file mode 100644
index 0000000..b31bab1
--- /dev/null
+++ b/src/data/composite/data/withPropertyFromObject.js
@@ -0,0 +1,69 @@
+// Gets a property of some object (in a dependency) and provides that value.
+// If the object itself is null, or the object doesn't have the listed property,
+// the provided dependency will also be null.
+//
+// See also:
+//  - withPropertiesFromObject
+//  - withPropertyFromList
+//
+
+import {input, templateCompositeFrom} from '#composite';
+
+export default templateCompositeFrom({
+  annotation: `withPropertyFromObject`,
+
+  inputs: {
+    object: input({type: 'object', acceptsNull: true}),
+    property: input({type: 'string'}),
+  },
+
+  outputs: ({
+    [input.staticDependency('object')]: object,
+    [input.staticValue('property')]: property,
+  }) =>
+    (object && property
+      ? (object.startsWith('#')
+          ? [`${object}.${property}`]
+          : [`#${object}.${property}`])
+      : ['#value']),
+
+  steps: () => [
+    {
+      dependencies: [
+        input.staticDependency('object'),
+        input.staticValue('property'),
+      ],
+
+      compute: (continuation, {
+        [input.staticDependency('object')]: object,
+        [input.staticValue('property')]: property,
+      }) => continuation({
+        '#output':
+          (object && property
+            ? (object.startsWith('#')
+                ? `${object}.${property}`
+                : `#${object}.${property}`)
+            : '#value'),
+      }),
+    },
+
+    {
+      dependencies: [
+        '#output',
+        input('object'),
+        input('property'),
+      ],
+
+      compute: (continuation, {
+        ['#output']: output,
+        [input('object')]: object,
+        [input('property')]: property,
+      }) => continuation({
+        [output]:
+          (object === null
+            ? null
+            : object[property] ?? null),
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/data/withUnflattenedList.js b/src/data/composite/data/withUnflattenedList.js
new file mode 100644
index 0000000..3cfc247
--- /dev/null
+++ b/src/data/composite/data/withUnflattenedList.js
@@ -0,0 +1,62 @@
+// After mapping the contents of a flattened array in-place (being careful to
+// retain the original indices by replacing unmatched results with null instead
+// of filtering them out), this function allows for recombining them. It will
+// filter out null and undefined items by default (pass {filter: false} to
+// disable this).
+
+import {input, templateCompositeFrom} from '#composite';
+import {isWholeNumber, validateArrayItems} from '#validators';
+
+export default templateCompositeFrom({
+  annotation: `withUnflattenedList`,
+
+  inputs: {
+    list: input({
+      type: 'array',
+      defaultDependency: '#flattenedList',
+    }),
+
+    indices: input({
+      validate: validateArrayItems(isWholeNumber),
+      defaultDependency: '#flattenedIndices',
+    }),
+
+    filter: input({
+      type: 'boolean',
+      defaultValue: true,
+    }),
+  },
+
+  outputs: ['#unflattenedList'],
+
+  steps: () => [
+    {
+      dependencies: [input('list'), input('indices'), input('filter')],
+      compute(continuation, {
+        [input('list')]: list,
+        [input('indices')]: indices,
+        [input('filter')]: filter,
+      }) {
+        const unflattenedList = [];
+
+        for (let i = 0; i < indices.length; i++) {
+          const startIndex = indices[i];
+          const endIndex =
+            (i === indices.length - 1
+              ? list.length
+              : indices[i + 1]);
+
+          const values = list.slice(startIndex, endIndex);
+          unflattenedList.push(
+            (filter
+              ? values.filter(value => value !== null && value !== undefined)
+              : values));
+        }
+
+        return continuation({
+          ['#unflattenedList']: unflattenedList,
+        });
+      },
+    },
+  ],
+});
diff --git a/src/data/composite/things/album/index.js b/src/data/composite/things/album/index.js
new file mode 100644
index 0000000..8139f10
--- /dev/null
+++ b/src/data/composite/things/album/index.js
@@ -0,0 +1,2 @@
+export {default as withTracks} from './withTracks.js';
+export {default as withTrackSections} from './withTrackSections.js';
diff --git a/src/data/composite/things/album/withTrackSections.js b/src/data/composite/things/album/withTrackSections.js
new file mode 100644
index 0000000..baa3cb4
--- /dev/null
+++ b/src/data/composite/things/album/withTrackSections.js
@@ -0,0 +1,128 @@
+import {input, templateCompositeFrom} from '#composite';
+import find from '#find';
+import {empty, stitchArrays} from '#sugar';
+import {isTrackSectionList} from '#validators';
+import {filterMultipleArrays} from '#wiki-data';
+
+import {exitWithoutDependency, exitWithoutUpdateValue}
+  from '#composite/control-flow';
+import {withResolvedReferenceList} from '#composite/wiki-data';
+
+import {
+  fillMissingListItems,
+  withFlattenedList,
+  withPropertiesFromList,
+  withUnflattenedList,
+} from '#composite/data';
+
+export default templateCompositeFrom({
+  annotation: `withTrackSections`,
+
+  outputs: ['#trackSections'],
+
+  steps: () => [
+    exitWithoutDependency({
+      dependency: 'trackData',
+      value: input.value([]),
+    }),
+
+    exitWithoutUpdateValue({
+      mode: input.value('empty'),
+      value: input.value([]),
+    }),
+
+    // TODO: input.updateValue description down here is a kludge.
+    withPropertiesFromList({
+      list: input.updateValue({
+        validate: isTrackSectionList,
+      }),
+      prefix: input.value('#sections'),
+      properties: input.value([
+        'tracks',
+        'dateOriginallyReleased',
+        'isDefaultTrackSection',
+        'name',
+        'color',
+      ]),
+    }),
+
+    fillMissingListItems({
+      list: '#sections.tracks',
+      fill: input.value([]),
+    }),
+
+    fillMissingListItems({
+      list: '#sections.isDefaultTrackSection',
+      fill: input.value(false),
+    }),
+
+    fillMissingListItems({
+      list: '#sections.name',
+      fill: input.value('Unnamed Track Section'),
+    }),
+
+    fillMissingListItems({
+      list: '#sections.color',
+      fill: input.dependency('color'),
+    }),
+
+    withFlattenedList({
+      list: '#sections.tracks',
+    }).outputs({
+      ['#flattenedList']: '#trackRefs',
+      ['#flattenedIndices']: '#sections.startIndex',
+    }),
+
+    withResolvedReferenceList({
+      list: '#trackRefs',
+      data: 'trackData',
+      notFoundMode: input.value('null'),
+      find: input.value(find.track),
+    }).outputs({
+      ['#resolvedReferenceList']: '#tracks',
+    }),
+
+    withUnflattenedList({
+      list: '#tracks',
+      indices: '#sections.startIndex',
+    }).outputs({
+      ['#unflattenedList']: '#sections.tracks',
+    }),
+
+    {
+      dependencies: [
+        '#sections.tracks',
+        '#sections.name',
+        '#sections.color',
+        '#sections.dateOriginallyReleased',
+        '#sections.isDefaultTrackSection',
+        '#sections.startIndex',
+      ],
+
+      compute: (continuation, {
+        '#sections.tracks': tracks,
+        '#sections.name': name,
+        '#sections.color': color,
+        '#sections.dateOriginallyReleased': dateOriginallyReleased,
+        '#sections.isDefaultTrackSection': isDefaultTrackSection,
+        '#sections.startIndex': startIndex,
+      }) => {
+        filterMultipleArrays(
+          tracks, name, color, dateOriginallyReleased, isDefaultTrackSection, startIndex,
+          tracks => !empty(tracks));
+
+        return continuation({
+          ['#trackSections']:
+            stitchArrays({
+              tracks,
+              name,
+              color,
+              dateOriginallyReleased,
+              isDefaultTrackSection,
+              startIndex,
+            }),
+        });
+      },
+    },
+  ],
+});
diff --git a/src/data/composite/things/album/withTracks.js b/src/data/composite/things/album/withTracks.js
new file mode 100644
index 0000000..dcea659
--- /dev/null
+++ b/src/data/composite/things/album/withTracks.js
@@ -0,0 +1,51 @@
+import {input, templateCompositeFrom} from '#composite';
+import find from '#find';
+
+import {exitWithoutDependency, raiseOutputWithoutDependency}
+  from '#composite/control-flow';
+import {withResolvedReferenceList} from '#composite/wiki-data';
+
+export default templateCompositeFrom({
+  annotation: `withTracks`,
+
+  outputs: ['#tracks'],
+
+  steps: () => [
+    exitWithoutDependency({
+      dependency: 'trackData',
+      value: input.value([]),
+    }),
+
+    raiseOutputWithoutDependency({
+      dependency: 'trackSections',
+      mode: input.value('empty'),
+      output: input.value({
+        ['#tracks']: [],
+      }),
+    }),
+
+    {
+      dependencies: ['trackSections'],
+      compute: (continuation, {trackSections}) =>
+        continuation({
+          '#trackRefs': trackSections
+            .flatMap(section => section.tracks ?? []),
+        }),
+    },
+
+    withResolvedReferenceList({
+      list: '#trackRefs',
+      data: 'trackData',
+      find: input.value(find.track),
+    }),
+
+    {
+      dependencies: ['#resolvedReferenceList'],
+      compute: (continuation, {
+        ['#resolvedReferenceList']: resolvedReferenceList,
+      }) => continuation({
+        ['#tracks']: resolvedReferenceList,
+      })
+    },
+  ],
+});
diff --git a/src/data/composite/things/flash/index.js b/src/data/composite/things/flash/index.js
new file mode 100644
index 0000000..63ac13d
--- /dev/null
+++ b/src/data/composite/things/flash/index.js
@@ -0,0 +1 @@
+export {default as withFlashAct} from './withFlashAct.js';
diff --git a/src/data/composite/things/flash/withFlashAct.js b/src/data/composite/things/flash/withFlashAct.js
new file mode 100644
index 0000000..ada2dcf
--- /dev/null
+++ b/src/data/composite/things/flash/withFlashAct.js
@@ -0,0 +1,108 @@
+// Gets the flash's act. This will early exit if flashActData is missing.
+// By default, if there's no flash whose list of flashes includes this flash,
+// the output dependency will be null; set {notFoundMode: 'exit'} to early
+// exit instead.
+//
+// This step models with Flash.withAlbum.
+
+import {input, templateCompositeFrom} from '#composite';
+import {is} from '#validators';
+
+import {exitWithoutDependency, withResultOfAvailabilityCheck}
+  from '#composite/control-flow';
+import {withPropertyFromList} from '#composite/data';
+
+export default templateCompositeFrom({
+  annotation: `withFlashAct`,
+
+  inputs: {
+    notFoundMode: input({
+      validate: is('exit', 'null'),
+      defaultValue: 'null',
+    }),
+  },
+
+  outputs: ['#flashAct'],
+
+  steps: () => [
+    // null flashActData is always an early exit.
+
+    exitWithoutDependency({
+      dependency: 'flashActData',
+      mode: input.value('null'),
+    }),
+
+    // empty flashActData conditionally exits early or outputs null.
+
+    withResultOfAvailabilityCheck({
+      from: 'flashActData',
+      mode: input.value('empty'),
+    }).outputs({
+      '#availability': '#flashActDataAvailability',
+    }),
+
+    {
+      dependencies: [input('notFoundMode'), '#flashActDataAvailability'],
+      compute(continuation, {
+        [input('notFoundMode')]: notFoundMode,
+        ['#flashActDataAvailability']: flashActDataIsAvailable,
+      }) {
+        if (flashActDataIsAvailable) return continuation();
+        switch (notFoundMode) {
+          case 'exit': return continuation.exit(null);
+          case 'null': return continuation.raiseOutput({'#flashAct': null});
+        }
+      },
+    },
+
+    withPropertyFromList({
+      list: 'flashActData',
+      property: input.value('flashes'),
+    }),
+
+    {
+      dependencies: [input.myself(), '#flashActData.flashes'],
+      compute: (continuation, {
+        [input.myself()]: track,
+        ['#flashActData.flashes']: flashLists,
+      }) => continuation({
+        ['#flashActIndex']:
+          flashLists.findIndex(flashes => flashes.includes(track)),
+      }),
+    },
+
+    // album not found conditionally exits or outputs null.
+
+    withResultOfAvailabilityCheck({
+      from: '#flashActIndex',
+      mode: input.value('index'),
+    }).outputs({
+      '#availability': '#flashActAvailability',
+    }),
+
+    {
+      dependencies: [input('notFoundMode'), '#flashActAvailability'],
+      compute(continuation, {
+        [input('notFoundMode')]: notFoundMode,
+        ['#flashActAvailability']: flashActIsAvailable,
+      }) {
+        if (flashActIsAvailable) return continuation();
+        switch (notFoundMode) {
+          case 'exit': return continuation.exit(null);
+          case 'null': return continuation.raiseOutput({'#flashAct': null});
+        }
+      },
+    },
+
+    {
+      dependencies: ['flashActData', '#flashActIndex'],
+      compute: (continuation, {
+        ['flashActData']: flashActData,
+        ['#flashActIndex']: flashActIndex,
+      }) => continuation.raiseOutput({
+        ['#flashAct']:
+          flashActData[flashActIndex],
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/track/exitWithoutUniqueCoverArt.js b/src/data/composite/things/track/exitWithoutUniqueCoverArt.js
new file mode 100644
index 0000000..f47086d
--- /dev/null
+++ b/src/data/composite/things/track/exitWithoutUniqueCoverArt.js
@@ -0,0 +1,26 @@
+// Shorthand for checking if the track has unique cover art and exposing a
+// fallback value if it isn't.
+
+import {input, templateCompositeFrom} from '#composite';
+
+import {exitWithoutDependency} from '#composite/control-flow';
+
+import withHasUniqueCoverArt from './withHasUniqueCoverArt.js';
+
+export default templateCompositeFrom({
+  annotation: `exitWithoutUniqueCoverArt`,
+
+  inputs: {
+    value: input({defaultValue: null}),
+  },
+
+  steps: () => [
+    withHasUniqueCoverArt(),
+
+    exitWithoutDependency({
+      dependency: '#hasUniqueCoverArt',
+      mode: input.value('falsy'),
+      value: input('value'),
+    }),
+  ],
+});
diff --git a/src/data/composite/things/track/index.js b/src/data/composite/things/track/index.js
new file mode 100644
index 0000000..3354b1c
--- /dev/null
+++ b/src/data/composite/things/track/index.js
@@ -0,0 +1,9 @@
+export {default as exitWithoutUniqueCoverArt} from './exitWithoutUniqueCoverArt.js';
+export {default as inheritFromOriginalRelease} from './inheritFromOriginalRelease.js';
+export {default as trackReverseReferenceList} from './trackReverseReferenceList.js';
+export {default as withAlbum} from './withAlbum.js';
+export {default as withAlwaysReferenceByDirectory} from './withAlwaysReferenceByDirectory.js';
+export {default as withContainingTrackSection} from './withContainingTrackSection.js';
+export {default as withHasUniqueCoverArt} from './withHasUniqueCoverArt.js';
+export {default as withOtherReleases} from './withOtherReleases.js';
+export {default as withPropertyFromAlbum} from './withPropertyFromAlbum.js';
diff --git a/src/data/composite/things/track/inheritFromOriginalRelease.js b/src/data/composite/things/track/inheritFromOriginalRelease.js
new file mode 100644
index 0000000..a9d57f8
--- /dev/null
+++ b/src/data/composite/things/track/inheritFromOriginalRelease.js
@@ -0,0 +1,43 @@
+// Early exits with a value inherited from the original release, if
+// this track is a rerelease, and otherwise continues with no further
+// dependencies provided. If allowOverride is true, then the continuation
+// will also be called if the original release exposed the requested
+// property as null.
+
+import {input, templateCompositeFrom} from '#composite';
+
+import withOriginalRelease from './withOriginalRelease.js';
+
+export default templateCompositeFrom({
+  annotation: `inheritFromOriginalRelease`,
+
+  inputs: {
+    property: input({type: 'string'}),
+    allowOverride: input({type: 'boolean', defaultValue: false}),
+  },
+
+  steps: () => [
+    withOriginalRelease(),
+
+    {
+      dependencies: [
+        '#originalRelease',
+        input('property'),
+        input('allowOverride'),
+      ],
+
+      compute: (continuation, {
+        ['#originalRelease']: originalRelease,
+        [input('property')]: originalProperty,
+        [input('allowOverride')]: allowOverride,
+      }) => {
+        if (!originalRelease) return continuation();
+
+        const value = originalRelease[originalProperty];
+        if (allowOverride && value === null) return continuation();
+
+        return continuation.exit(value);
+      },
+    },
+  ],
+});
diff --git a/src/data/composite/things/track/trackReverseReferenceList.js b/src/data/composite/things/track/trackReverseReferenceList.js
new file mode 100644
index 0000000..e7bfedf
--- /dev/null
+++ b/src/data/composite/things/track/trackReverseReferenceList.js
@@ -0,0 +1,38 @@
+// Like a normal reverse reference list ("objects which reference this object
+// under a specified property"), only excluding re-releases from the possible
+// outputs. While it's useful to travel from a re-release to the tracks it
+// references, re-releases aren't generally relevant from the perspective of
+// the tracks *being* referenced. Apart from hiding re-releases from lists on
+// the site, it also excludes keeps them from relational data processing, such
+// as on the "Tracks - by Times Referenced" listing page.
+
+import {input, templateCompositeFrom} from '#composite';
+import {withReverseReferenceList} from '#composite/wiki-data';
+
+export default templateCompositeFrom({
+  annotation: `trackReverseReferenceList`,
+
+  compose: false,
+
+  inputs: {
+    list: input({type: 'string'}),
+  },
+
+  steps: () => [
+    withReverseReferenceList({
+      data: 'trackData',
+      list: input('list'),
+    }),
+
+    {
+      flags: {expose: true},
+      expose: {
+        dependencies: ['#reverseReferenceList'],
+        compute: ({
+          ['#reverseReferenceList']: reverseReferenceList,
+        }) =>
+          reverseReferenceList.filter(track => !track.originalReleaseTrack),
+      },
+    },
+  ],
+});
diff --git a/src/data/composite/things/track/withAlbum.js b/src/data/composite/things/track/withAlbum.js
new file mode 100644
index 0000000..cbd16dc
--- /dev/null
+++ b/src/data/composite/things/track/withAlbum.js
@@ -0,0 +1,108 @@
+// Gets the track's album. This will early exit if albumData is missing.
+// By default, if there's no album whose list of tracks includes this track,
+// the output dependency will be null; set {notFoundMode: 'exit'} to early
+// exit instead.
+//
+// This step models with Flash.withFlashAct.
+
+import {input, templateCompositeFrom} from '#composite';
+import {is} from '#validators';
+
+import {exitWithoutDependency, withResultOfAvailabilityCheck}
+  from '#composite/control-flow';
+import {withPropertyFromList} from '#composite/data';
+
+export default templateCompositeFrom({
+  annotation: `withAlbum`,
+
+  inputs: {
+    notFoundMode: input({
+      validate: is('exit', 'null'),
+      defaultValue: 'null',
+    }),
+  },
+
+  outputs: ['#album'],
+
+  steps: () => [
+    // null albumData is always an early exit.
+
+    exitWithoutDependency({
+      dependency: 'albumData',
+      mode: input.value('null'),
+    }),
+
+    // empty albumData conditionally exits early or outputs null.
+
+    withResultOfAvailabilityCheck({
+      from: 'albumData',
+      mode: input.value('empty'),
+    }).outputs({
+      '#availability': '#albumDataAvailability',
+    }),
+
+    {
+      dependencies: [input('notFoundMode'), '#albumDataAvailability'],
+      compute(continuation, {
+        [input('notFoundMode')]: notFoundMode,
+        ['#albumDataAvailability']: albumDataIsAvailable,
+      }) {
+        if (albumDataIsAvailable) return continuation();
+        switch (notFoundMode) {
+          case 'exit': return continuation.exit(null);
+          case 'null': return continuation.raiseOutput({'#album': null});
+        }
+      },
+    },
+
+    withPropertyFromList({
+      list: 'albumData',
+      property: input.value('tracks'),
+    }),
+
+    {
+      dependencies: [input.myself(), '#albumData.tracks'],
+      compute: (continuation, {
+        [input.myself()]: track,
+        ['#albumData.tracks']: trackLists,
+      }) => continuation({
+        ['#albumIndex']:
+          trackLists.findIndex(tracks => tracks.includes(track)),
+      }),
+    },
+
+    // album not found conditionally exits or outputs null.
+
+    withResultOfAvailabilityCheck({
+      from: '#albumIndex',
+      mode: input.value('index'),
+    }).outputs({
+      '#availability': '#albumAvailability',
+    }),
+
+    {
+      dependencies: [input('notFoundMode'), '#albumAvailability'],
+      compute(continuation, {
+        [input('notFoundMode')]: notFoundMode,
+        ['#albumAvailability']: albumIsAvailable,
+      }) {
+        if (albumIsAvailable) return continuation();
+        switch (notFoundMode) {
+          case 'exit': return continuation.exit(null);
+          case 'null': return continuation.raiseOutput({'#album': null});
+        }
+      },
+    },
+
+    {
+      dependencies: ['albumData', '#albumIndex'],
+      compute: (continuation, {
+        ['albumData']: albumData,
+        ['#albumIndex']: albumIndex,
+      }) => continuation.raiseOutput({
+        ['#album']:
+          albumData[albumIndex],
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js
new file mode 100644
index 0000000..d27f7b2
--- /dev/null
+++ b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js
@@ -0,0 +1,91 @@
+// Controls how find.track works - it'll never be matched by a reference
+// just to the track's name, which means you don't have to always reference
+// some *other* (much more commonly referenced) track by directory instead
+// of more naturally by name.
+//
+// See the implementation for an important caveat about matching the original
+// track against other tracks, which uses a custom implementation pulling (and
+// duplicating) details from #find instead of using withOriginalRelease and the
+// usual withResolvedReference / find.track() utilities.
+//
+
+import {input, templateCompositeFrom} from '#composite';
+import {isBoolean} from '#validators';
+
+import {exitWithoutDependency, exposeUpdateValueOrContinue}
+  from '#composite/control-flow';
+import {withPropertyFromObject} from '#composite/data';
+
+// TODO: Kludge. (The usage of this, not so much the import.)
+import CacheableObject from '../../../things/cacheable-object.js';
+
+export default templateCompositeFrom({
+  annotation: `withAlwaysReferenceByDirectory`,
+
+  outputs: ['#alwaysReferenceByDirectory'],
+
+  steps: () => [
+    exposeUpdateValueOrContinue({
+      validate: input.value(isBoolean),
+    }),
+
+    // Remaining code is for defaulting to true if this track is a rerelease of
+    // another with the same name, so everything further depends on access to
+    // trackData as well as originalReleaseTrack.
+
+    exitWithoutDependency({
+      dependency: 'trackData',
+      mode: input.value('empty'),
+      value: input.value(false),
+    }),
+
+    exitWithoutDependency({
+      dependency: 'originalReleaseTrack',
+      value: input.value(false),
+    }),
+
+    // "Slow" / uncached, manual search from trackData (with this track
+    // excluded). Otherwise there end up being pretty bad recursion issues
+    // (track1.alwaysReferencedByDirectory depends on searching through data
+    // including track2, which depends on evaluating track2.alwaysReferenced-
+    // ByDirectory, which depends on searcing through data including track1...)
+    // That said, this is 100% a kludge, since it involves duplicating find
+    // logic on a completely unrelated context.
+    {
+      dependencies: [input.myself(), 'trackData', 'originalReleaseTrack'],
+      compute: (continuation, {
+        [input.myself()]: thisTrack,
+        ['trackData']: trackData,
+        ['originalReleaseTrack']: ref,
+      }) => continuation({
+        ['#originalRelease']:
+          (ref.startsWith('track:')
+            ? trackData.find(track => track.directory === ref.slice('track:'.length))
+            : trackData.find(track =>
+                track !== thisTrack &&
+                !CacheableObject.getUpdateValue(track, 'originalReleaseTrack') &&
+                track.name.toLowerCase() === ref.toLowerCase())),
+      })
+    },
+
+    exitWithoutDependency({
+      dependency: '#originalRelease',
+      value: input.value(false),
+    }),
+
+    withPropertyFromObject({
+      object: '#originalRelease',
+      property: input.value('name'),
+    }),
+
+    {
+      dependencies: ['name', '#originalRelease.name'],
+      compute: (continuation, {
+        name,
+        ['#originalRelease.name']: originalName,
+      }) => continuation({
+        ['#alwaysReferenceByDirectory']: name === originalName,
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/track/withContainingTrackSection.js b/src/data/composite/things/track/withContainingTrackSection.js
new file mode 100644
index 0000000..b2e5f2b
--- /dev/null
+++ b/src/data/composite/things/track/withContainingTrackSection.js
@@ -0,0 +1,63 @@
+// Gets the track section containing this track from its album's track list.
+// If notFoundMode is set to 'exit', this will early exit if the album can't be
+// found or if none of its trackSections includes the track for some reason.
+
+import {input, templateCompositeFrom} from '#composite';
+import {is} from '#validators';
+
+import withPropertyFromAlbum from './withPropertyFromAlbum.js';
+
+export default templateCompositeFrom({
+  annotation: `withContainingTrackSection`,
+
+  inputs: {
+    notFoundMode: input({
+      validate: is('exit', 'null'),
+      defaultValue: 'null',
+    }),
+  },
+
+  outputs: ['#trackSection'],
+
+  steps: () => [
+    withPropertyFromAlbum({
+      property: input.value('trackSections'),
+      notFoundMode: input('notFoundMode'),
+    }),
+
+    {
+      dependencies: [
+        input.myself(),
+        input('notFoundMode'),
+        '#album.trackSections',
+      ],
+
+      compute(continuation, {
+        [input.myself()]: track,
+        [input('notFoundMode')]: notFoundMode,
+        ['#album.trackSections']: trackSections,
+      }) {
+        if (!trackSections) {
+          return continuation.raiseOutput({
+            ['#trackSection']: null,
+          });
+        }
+
+        const trackSection =
+          trackSections.find(({tracks}) => tracks.includes(track));
+
+        if (trackSection) {
+          return continuation.raiseOutput({
+            ['#trackSection']: trackSection,
+          });
+        } else if (notFoundMode === 'exit') {
+          return continuation.exit(null);
+        } else {
+          return continuation.raiseOutput({
+            ['#trackSection']: null,
+          });
+        }
+      },
+    },
+  ],
+});
diff --git a/src/data/composite/things/track/withHasUniqueCoverArt.js b/src/data/composite/things/track/withHasUniqueCoverArt.js
new file mode 100644
index 0000000..96078d5
--- /dev/null
+++ b/src/data/composite/things/track/withHasUniqueCoverArt.js
@@ -0,0 +1,61 @@
+// Whether or not the track has "unique" cover artwork - a cover which is
+// specifically associated with this track in particular, rather than with
+// the track's album as a whole. This is typically used to select between
+// displaying the track artwork and a fallback, such as the album artwork
+// or a placeholder. (This property is named hasUniqueCoverArt instead of
+// the usual hasCoverArt to emphasize that it does not inherit from the
+// album.)
+
+import {input, templateCompositeFrom} from '#composite';
+import {empty} from '#sugar';
+
+import {withResolvedContribs} from '#composite/wiki-data';
+
+import withPropertyFromAlbum from './withPropertyFromAlbum.js';
+
+export default templateCompositeFrom({
+  annotation: 'withHasUniqueCoverArt',
+
+  outputs: ['#hasUniqueCoverArt'],
+
+  steps: () => [
+    {
+      dependencies: ['disableUniqueCoverArt'],
+      compute: (continuation, {disableUniqueCoverArt}) =>
+        (disableUniqueCoverArt
+          ? continuation.raiseOutput({
+              ['#hasUniqueCoverArt']: false,
+            })
+          : continuation()),
+    },
+
+    withResolvedContribs({from: 'coverArtistContribs'}),
+
+    {
+      dependencies: ['#resolvedContribs'],
+      compute: (continuation, {
+        ['#resolvedContribs']: contribsFromTrack,
+      }) =>
+        (empty(contribsFromTrack)
+          ? continuation()
+          : continuation.raiseOutput({
+              ['#hasUniqueCoverArt']: true,
+            })),
+    },
+
+    withPropertyFromAlbum({
+      property: input.value('trackCoverArtistContribs'),
+    }),
+
+    {
+      dependencies: ['#album.trackCoverArtistContribs'],
+      compute: (continuation, {
+        ['#album.trackCoverArtistContribs']: contribsFromAlbum,
+      }) =>
+        continuation.raiseOutput({
+          ['#hasUniqueCoverArt']:
+            !empty(contribsFromAlbum),
+        }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/track/withOriginalRelease.js b/src/data/composite/things/track/withOriginalRelease.js
new file mode 100644
index 0000000..d2ee39d
--- /dev/null
+++ b/src/data/composite/things/track/withOriginalRelease.js
@@ -0,0 +1,59 @@
+// Just includes the original release of this track as a dependency.
+// If this track isn't a rerelease, then it'll provide null, unless the
+// {selfIfOriginal} option is set, in which case it'll provide this track
+// itself. Note that this will early exit if the original release is
+// specified by reference and that reference doesn't resolve to anything.
+// Outputs to '#originalRelease' by default.
+
+import {input, templateCompositeFrom} from '#composite';
+import find from '#find';
+import {validateWikiData} from '#validators';
+
+import {withResolvedReference} from '#composite/wiki-data';
+
+export default templateCompositeFrom({
+  annotation: `withOriginalRelease`,
+
+  inputs: {
+    selfIfOriginal: input({type: 'boolean', defaultValue: false}),
+
+    data: input({
+      validate: validateWikiData({referenceType: 'track'}),
+      defaultDependency: 'trackData',
+    }),
+  },
+
+  outputs: ['#originalRelease'],
+
+  steps: () => [
+    withResolvedReference({
+      ref: 'originalReleaseTrack',
+      data: input('data'),
+      find: input.value(find.track),
+      notFoundMode: input.value('exit'),
+    }).outputs({
+      ['#resolvedReference']: '#originalRelease',
+    }),
+
+    {
+      dependencies: [
+        input.myself(),
+        input('selfIfOriginal'),
+        '#originalRelease',
+      ],
+
+      compute: (continuation, {
+        [input.myself()]: track,
+        [input('selfIfOriginal')]: selfIfOriginal,
+        ['#originalRelease']: originalRelease,
+      }) =>
+        continuation({
+          ['#originalRelease']:
+            (originalRelease ??
+              (selfIfOriginal
+                ? track
+                : null)),
+        }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/track/withOtherReleases.js b/src/data/composite/things/track/withOtherReleases.js
new file mode 100644
index 0000000..84420cf
--- /dev/null
+++ b/src/data/composite/things/track/withOtherReleases.js
@@ -0,0 +1,40 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {exitWithoutDependency} from '#composite/control-flow';
+
+import withOriginalRelease from './withOriginalRelease.js';
+
+export default templateCompositeFrom({
+  annotation: `withOtherReleases`,
+
+  outputs: ['#otherReleases'],
+
+  steps: () => [
+    exitWithoutDependency({
+      dependency: 'trackData',
+      mode: input.value('empty'),
+    }),
+
+    withOriginalRelease({
+      selfIfOriginal: input.value(true),
+    }),
+
+    {
+      dependencies: [input.myself(), '#originalRelease', 'trackData'],
+      compute: (continuation, {
+        [input.myself()]: thisTrack,
+        ['#originalRelease']: originalRelease,
+        trackData,
+      }) => continuation({
+        ['#otherReleases']:
+          (originalRelease === thisTrack
+            ? []
+            : [originalRelease])
+            .concat(trackData.filter(track =>
+              track !== originalRelease &&
+              track !== thisTrack &&
+              track.originalReleaseTrack === originalRelease)),
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/track/withPropertyFromAlbum.js b/src/data/composite/things/track/withPropertyFromAlbum.js
new file mode 100644
index 0000000..b236a6e
--- /dev/null
+++ b/src/data/composite/things/track/withPropertyFromAlbum.js
@@ -0,0 +1,49 @@
+// Gets a single property from this track's album, providing it as the same
+// property name prefixed with '#album.' (by default). If the track's album
+// isn't available, then by default, the property will be provided as null;
+// set {notFoundMode: 'exit'} to early exit instead.
+
+import {input, templateCompositeFrom} from '#composite';
+import {is} from '#validators';
+
+import {withPropertyFromObject} from '#composite/data';
+
+import withAlbum from './withAlbum.js';
+
+export default templateCompositeFrom({
+  annotation: `withPropertyFromAlbum`,
+
+  inputs: {
+    property: input.staticValue({type: 'string'}),
+
+    notFoundMode: input({
+      validate: is('exit', 'null'),
+      defaultValue: 'null',
+    }),
+  },
+
+  outputs: ({
+    [input.staticValue('property')]: property,
+  }) => ['#album.' + property],
+
+  steps: () => [
+    withAlbum({
+      notFoundMode: input('notFoundMode'),
+    }),
+
+    withPropertyFromObject({
+      object: '#album',
+      property: input('property'),
+    }),
+
+    {
+      dependencies: ['#value', input.staticValue('property')],
+      compute: (continuation, {
+        ['#value']: value,
+        [input.staticValue('property')]: property,
+      }) => continuation({
+        ['#album.' + property]: value,
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/wiki-data/exitWithoutContribs.js b/src/data/composite/wiki-data/exitWithoutContribs.js
new file mode 100644
index 0000000..2c8219f
--- /dev/null
+++ b/src/data/composite/wiki-data/exitWithoutContribs.js
@@ -0,0 +1,47 @@
+// Shorthand for exiting if the contribution list (usually a property's update
+// value) resolves to empty - ensuring that the later computed results are only
+// returned if these contributions are present.
+
+import {input, templateCompositeFrom} from '#composite';
+import {isContributionList} from '#validators';
+
+import {withResultOfAvailabilityCheck} from '#composite/control-flow';
+
+import withResolvedContribs from './withResolvedContribs.js';
+
+export default templateCompositeFrom({
+  annotation: `exitWithoutContribs`,
+
+  inputs: {
+    contribs: input({
+      validate: isContributionList,
+      acceptsNull: true,
+    }),
+
+    value: input({defaultValue: null}),
+  },
+
+  steps: () => [
+    withResolvedContribs({
+      from: input('contribs'),
+    }),
+
+    // TODO: Fairly certain exitWithoutDependency would be sufficient here.
+
+    withResultOfAvailabilityCheck({
+      from: '#resolvedContribs',
+      mode: input.value('empty'),
+    }),
+
+    {
+      dependencies: ['#availability', input('value')],
+      compute: (continuation, {
+        ['#availability']: availability,
+        [input('value')]: value,
+      }) =>
+        (availability
+          ? continuation()
+          : continuation.exit(value)),
+    },
+  ],
+});
diff --git a/src/data/composite/wiki-data/index.js b/src/data/composite/wiki-data/index.js
new file mode 100644
index 0000000..1d0400f
--- /dev/null
+++ b/src/data/composite/wiki-data/index.js
@@ -0,0 +1,7 @@
+export {default as exitWithoutContribs} from './exitWithoutContribs.js';
+export {default as inputThingClass} from './inputThingClass.js';
+export {default as inputWikiData} from './inputWikiData.js';
+export {default as withResolvedContribs} from './withResolvedContribs.js';
+export {default as withResolvedReference} from './withResolvedReference.js';
+export {default as withResolvedReferenceList} from './withResolvedReferenceList.js';
+export {default as withReverseReferenceList} from './withReverseReferenceList.js';
diff --git a/src/data/composite/wiki-data/inputThingClass.js b/src/data/composite/wiki-data/inputThingClass.js
new file mode 100644
index 0000000..d70480e
--- /dev/null
+++ b/src/data/composite/wiki-data/inputThingClass.js
@@ -0,0 +1,23 @@
+// Please note that this input, used in a variety of #composite/wiki-data
+// utilities, is basically always a kludge. Any usage of it depends on
+// referencing Thing class values defined outside of the #composite folder.
+
+import {input} from '#composite';
+import {isType} from '#validators';
+
+// TODO: Kludge.
+import Thing from '../../things/thing.js';
+
+export default function inputThingClass() {
+  return input.staticValue({
+    validate(thingClass) {
+      isType(thingClass, 'function');
+
+      if (!Object.hasOwn(thingClass, Thing.referenceType)) {
+        throw new TypeError(`Expected a Thing constructor, missing Thing.referenceType`);
+      }
+
+      return true;
+    },
+  });
+}
diff --git a/src/data/composite/wiki-data/inputWikiData.js b/src/data/composite/wiki-data/inputWikiData.js
new file mode 100644
index 0000000..cf7a7c2
--- /dev/null
+++ b/src/data/composite/wiki-data/inputWikiData.js
@@ -0,0 +1,17 @@
+import {input} from '#composite';
+import {validateWikiData} from '#validators';
+
+// TODO: This doesn't access a class's own ThingSubclass[Thing.referenceType]
+// value because classes aren't initialized by when templateCompositeFrom gets
+// called (see: circular imports). So the reference types have to be hard-coded,
+// which somewhat defeats the point of storing them on the class in the first
+// place...
+export default function inputWikiData({
+  referenceType = '',
+  allowMixedTypes = false,
+} = {}) {
+  return input({
+    validate: validateWikiData({referenceType, allowMixedTypes}),
+    acceptsNull: true,
+  });
+}
diff --git a/src/data/composite/wiki-data/withResolvedContribs.js b/src/data/composite/wiki-data/withResolvedContribs.js
new file mode 100644
index 0000000..eda2416
--- /dev/null
+++ b/src/data/composite/wiki-data/withResolvedContribs.js
@@ -0,0 +1,77 @@
+// Resolves the contribsByRef contained in the provided dependency,
+// providing (named by the second argument) the result. "Resolving"
+// means mapping the "who" reference of each contribution to an artist
+// object, and filtering out those whose "who" doesn't match any artist.
+
+import {input, templateCompositeFrom} from '#composite';
+import find from '#find';
+import {stitchArrays} from '#sugar';
+import {is, isContributionList} from '#validators';
+import {filterMultipleArrays} from '#wiki-data';
+
+import {
+  raiseOutputWithoutDependency,
+} from '#composite/control-flow';
+
+import {
+  withPropertiesFromList,
+} from '#composite/data';
+
+import withResolvedReferenceList from './withResolvedReferenceList.js';
+
+export default templateCompositeFrom({
+  annotation: `withResolvedContribs`,
+
+  inputs: {
+    from: input({
+      validate: isContributionList,
+      acceptsNull: true,
+    }),
+
+    notFoundMode: input({
+      validate: is('exit', 'filter', 'null'),
+      defaultValue: 'null',
+    }),
+  },
+
+  outputs: ['#resolvedContribs'],
+
+  steps: () => [
+    raiseOutputWithoutDependency({
+      dependency: input('from'),
+      mode: input.value('empty'),
+      output: input.value({
+        ['#resolvedContribs']: [],
+      }),
+    }),
+
+    withPropertiesFromList({
+      list: input('from'),
+      properties: input.value(['who', 'what']),
+      prefix: input.value('#contribs'),
+    }),
+
+    withResolvedReferenceList({
+      list: '#contribs.who',
+      data: 'artistData',
+      find: input.value(find.artist),
+      notFoundMode: input('notFoundMode'),
+    }).outputs({
+      ['#resolvedReferenceList']: '#contribs.who',
+    }),
+
+    {
+      dependencies: ['#contribs.who', '#contribs.what'],
+
+      compute(continuation, {
+        ['#contribs.who']: who,
+        ['#contribs.what']: what,
+      }) {
+        filterMultipleArrays(who, what, (who, _what) => who);
+        return continuation({
+          ['#resolvedContribs']: stitchArrays({who, what}),
+        });
+      },
+    },
+  ],
+});
diff --git a/src/data/composite/wiki-data/withResolvedReference.js b/src/data/composite/wiki-data/withResolvedReference.js
new file mode 100644
index 0000000..0fa5c55
--- /dev/null
+++ b/src/data/composite/wiki-data/withResolvedReference.js
@@ -0,0 +1,73 @@
+// Resolves a reference by using the provided find function to match it
+// within the provided thingData dependency. This will early exit if the
+// data dependency is null, or, if notFoundMode is set to 'exit', if the find
+// function doesn't match anything for the reference. Otherwise, the data
+// object is provided on the output dependency; or null, if the reference
+// doesn't match anything or itself was null to begin with.
+
+import {input, templateCompositeFrom} from '#composite';
+import {is} from '#validators';
+
+import {
+  exitWithoutDependency,
+  raiseOutputWithoutDependency,
+} from '#composite/control-flow';
+
+import inputWikiData from './inputWikiData.js';
+
+export default templateCompositeFrom({
+  annotation: `withResolvedReference`,
+
+  inputs: {
+    ref: input({type: 'string', acceptsNull: true}),
+
+    data: inputWikiData({allowMixedTypes: false}),
+    find: input({type: 'function'}),
+
+    notFoundMode: input({
+      validate: is('null', 'exit'),
+      defaultValue: 'null',
+    }),
+  },
+
+  outputs: ['#resolvedReference'],
+
+  steps: () => [
+    raiseOutputWithoutDependency({
+      dependency: input('ref'),
+      output: input.value({
+        ['#resolvedReference']: null,
+      }),
+    }),
+
+    exitWithoutDependency({
+      dependency: input('data'),
+    }),
+
+    {
+      dependencies: [
+        input('ref'),
+        input('data'),
+        input('find'),
+        input('notFoundMode'),
+      ],
+
+      compute(continuation, {
+        [input('ref')]: ref,
+        [input('data')]: data,
+        [input('find')]: findFunction,
+        [input('notFoundMode')]: notFoundMode,
+      }) {
+        const match = findFunction(ref, data, {mode: 'quiet'});
+
+        if (match === null && notFoundMode === 'exit') {
+          return continuation.exit(null);
+        }
+
+        return continuation.raiseOutput({
+          ['#resolvedReference']: match ?? null,
+        });
+      },
+    },
+  ],
+});
diff --git a/src/data/composite/wiki-data/withResolvedReferenceList.js b/src/data/composite/wiki-data/withResolvedReferenceList.js
new file mode 100644
index 0000000..1d39e5b
--- /dev/null
+++ b/src/data/composite/wiki-data/withResolvedReferenceList.js
@@ -0,0 +1,101 @@
+// Resolves a list of references, with each reference matched with provided
+// data in the same way as withResolvedReference. This will early exit if the
+// data dependency is null (even if the reference list is empty). By default
+// it will filter out references which don't match, but this can be changed
+// to early exit ({notFoundMode: 'exit'}) or leave null in place ('null').
+
+import {input, templateCompositeFrom} from '#composite';
+import {is, isString, validateArrayItems} from '#validators';
+
+import {
+  exitWithoutDependency,
+  raiseOutputWithoutDependency,
+} from '#composite/control-flow';
+
+import inputWikiData from './inputWikiData.js';
+
+export default templateCompositeFrom({
+  annotation: `withResolvedReferenceList`,
+
+  inputs: {
+    list: input({
+      validate: validateArrayItems(isString),
+      acceptsNull: true,
+    }),
+
+    data: inputWikiData({allowMixedTypes: false}),
+    find: input({type: 'function'}),
+
+    notFoundMode: input({
+      validate: is('exit', 'filter', 'null'),
+      defaultValue: 'filter',
+    }),
+  },
+
+  outputs: ['#resolvedReferenceList'],
+
+  steps: () => [
+    exitWithoutDependency({
+      dependency: input('data'),
+      value: input.value([]),
+    }),
+
+    raiseOutputWithoutDependency({
+      dependency: input('list'),
+      mode: input.value('empty'),
+      output: input.value({
+        ['#resolvedReferenceList']: [],
+      }),
+    }),
+
+    {
+      dependencies: [input('list'), input('data'), input('find')],
+      compute: (continuation, {
+        [input('list')]: list,
+        [input('data')]: data,
+        [input('find')]: findFunction,
+      }) =>
+        continuation({
+          '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})),
+        }),
+    },
+
+    {
+      dependencies: ['#matches'],
+      compute: (continuation, {'#matches': matches}) =>
+        (matches.every(match => match)
+          ? continuation.raiseOutput({
+              ['#resolvedReferenceList']: matches,
+            })
+          : continuation()),
+    },
+
+    {
+      dependencies: ['#matches', input('notFoundMode')],
+      compute(continuation, {
+        ['#matches']: matches,
+        [input('notFoundMode')]: notFoundMode,
+      }) {
+        switch (notFoundMode) {
+          case 'exit':
+            return continuation.exit([]);
+
+          case 'filter':
+            return continuation.raiseOutput({
+              ['#resolvedReferenceList']:
+                matches.filter(match => match),
+            });
+
+          case 'null':
+            return continuation.raiseOutput({
+              ['#resolvedReferenceList']:
+                matches.map(match => match ?? null),
+            });
+
+          default:
+            throw new TypeError(`Expected notFoundMode to be exit, filter, or null`);
+        }
+      },
+    },
+  ],
+});
diff --git a/src/data/composite/wiki-data/withReverseReferenceList.js b/src/data/composite/wiki-data/withReverseReferenceList.js
new file mode 100644
index 0000000..a025b5e
--- /dev/null
+++ b/src/data/composite/wiki-data/withReverseReferenceList.js
@@ -0,0 +1,41 @@
+// Check out the info on reverseReferenceList!
+// This is its composable form.
+
+import {input, templateCompositeFrom} from '#composite';
+
+import {exitWithoutDependency} from '#composite/control-flow';
+
+import inputWikiData from './inputWikiData.js';
+
+export default templateCompositeFrom({
+  annotation: `withReverseReferenceList`,
+
+  inputs: {
+    data: inputWikiData({allowMixedTypes: false}),
+    list: input({type: 'string'}),
+  },
+
+  outputs: ['#reverseReferenceList'],
+
+  steps: () => [
+    exitWithoutDependency({
+      dependency: input('data'),
+      value: input.value([]),
+      mode: input.value('empty'),
+    }),
+
+    {
+      dependencies: [input.myself(), input('data'), input('list')],
+
+      compute: (continuation, {
+        [input.myself()]: thisThing,
+        [input('data')]: data,
+        [input('list')]: refListProperty,
+      }) =>
+        continuation({
+          ['#reverseReferenceList']:
+            data.filter(thing => thing[refListProperty].includes(thisThing)),
+        }),
+    },
+  ],
+});
diff --git a/src/data/composite/wiki-properties/additionalFiles.js b/src/data/composite/wiki-properties/additionalFiles.js
new file mode 100644
index 0000000..6760527
--- /dev/null
+++ b/src/data/composite/wiki-properties/additionalFiles.js
@@ -0,0 +1,30 @@
+// This is a somewhat more involved data structure - it's for additional
+// or "bonus" files associated with albums or tracks (or anything else).
+// It's got this form:
+//
+//   [
+//     {title: 'Booklet', files: ['Booklet.pdf']},
+//     {
+//       title: 'Wallpaper',
+//       description: 'Cool Wallpaper!',
+//       files: ['1440x900.png', '1920x1080.png']
+//     },
+//     {title: 'Alternate Covers', description: null, files: [...]},
+//     ...
+//   ]
+//
+
+import {isAdditionalFileList} from '#validators';
+
+// TODO: Not templateCompositeFrom.
+
+export default function() {
+  return {
+    flags: {update: true, expose: true},
+    update: {validate: isAdditionalFileList},
+    expose: {
+      transform: (additionalFiles) =>
+        additionalFiles ?? [],
+    },
+  };
+}
diff --git a/src/data/composite/wiki-properties/color.js b/src/data/composite/wiki-properties/color.js
new file mode 100644
index 0000000..1bc9888
--- /dev/null
+++ b/src/data/composite/wiki-properties/color.js
@@ -0,0 +1,12 @@
+// A color! This'll be some CSS-ready value.
+
+import {isColor} from '#validators';
+
+// TODO: Not templateCompositeFrom.
+
+export default function() {
+  return {
+    flags: {update: true, expose: true},
+    update: {validate: isColor},
+  };
+}
diff --git a/src/data/composite/wiki-properties/commentary.js b/src/data/composite/wiki-properties/commentary.js
new file mode 100644
index 0000000..fbea9d5
--- /dev/null
+++ b/src/data/composite/wiki-properties/commentary.js
@@ -0,0 +1,12 @@
+// Artist commentary! Generally present on tracks and albums.
+
+import {isCommentary} from '#validators';
+
+// TODO: Not templateCompositeFrom.
+
+export default function() {
+  return {
+    flags: {update: true, expose: true},
+    update: {validate: isCommentary},
+  };
+}
diff --git a/src/data/composite/wiki-properties/commentatorArtists.js b/src/data/composite/wiki-properties/commentatorArtists.js
new file mode 100644
index 0000000..52aeb86
--- /dev/null
+++ b/src/data/composite/wiki-properties/commentatorArtists.js
@@ -0,0 +1,55 @@
+// This one's kinda tricky: it parses artist "references" from the
+// commentary content, and finds the matching artist for each reference.
+// This is mostly useful for credits and listings on artist pages.
+
+import {input, templateCompositeFrom} from '#composite';
+import find from '#find';
+import {unique} from '#sugar';
+
+import {exitWithoutDependency} from '#composite/control-flow';
+import {withResolvedReferenceList} from '#composite/wiki-data';
+
+export default templateCompositeFrom({
+  annotation: `commentatorArtists`,
+
+  compose: false,
+
+  steps: () => [
+    exitWithoutDependency({
+      dependency: 'commentary',
+      mode: input.value('falsy'),
+      value: input.value([]),
+    }),
+
+    {
+      dependencies: ['commentary'],
+      compute: (continuation, {commentary}) =>
+        continuation({
+          '#artistRefs':
+            Array.from(
+              commentary
+                .replace(/<\/?b>/g, '')
+                .matchAll(/<i>(?<who>.*?):<\/i>/g))
+              .map(({groups: {who}}) => who),
+        }),
+    },
+
+    withResolvedReferenceList({
+      list: '#artistRefs',
+      data: 'artistData',
+      find: input.value(find.artist),
+    }).outputs({
+      '#resolvedReferenceList': '#artists',
+    }),
+
+    {
+      flags: {expose: true},
+
+      expose: {
+        dependencies: ['#artists'],
+        compute: ({'#artists': artists}) =>
+          unique(artists),
+      },
+    },
+  ],
+});
diff --git a/src/data/composite/wiki-properties/contribsPresent.js b/src/data/composite/wiki-properties/contribsPresent.js
new file mode 100644
index 0000000..24f302a
--- /dev/null
+++ b/src/data/composite/wiki-properties/contribsPresent.js
@@ -0,0 +1,30 @@
+// Nice 'n simple shorthand for an exposed-only flag which is true when any
+// contributions are present in the specified property.
+
+import {input, templateCompositeFrom} from '#composite';
+import {isContributionList} from '#validators';
+
+import {exposeDependency, withResultOfAvailabilityCheck}
+  from '#composite/control-flow';
+
+export default templateCompositeFrom({
+  annotation: `contribsPresent`,
+
+  compose: false,
+
+  inputs: {
+    contribs: input.staticDependency({
+      validate: isContributionList,
+      acceptsNull: true,
+    }),
+  },
+
+  steps: () => [
+    withResultOfAvailabilityCheck({
+      from: input('contribs'),
+      mode: input.value('empty'),
+    }),
+
+    exposeDependency({dependency: '#availability'}),
+  ],
+});
diff --git a/src/data/composite/wiki-properties/contributionList.js b/src/data/composite/wiki-properties/contributionList.js
new file mode 100644
index 0000000..8fde2ca
--- /dev/null
+++ b/src/data/composite/wiki-properties/contributionList.js
@@ -0,0 +1,35 @@
+// Strong 'n sturdy contribution list, rolling a list of references (provided
+// as this property's update value) and the resolved results (as get exposed)
+// into one property. Update value will look something like this:
+//
+//   [
+//     {who: 'Artist Name', what: 'Viola'},
+//     {who: 'artist:john-cena', what: null},
+//     ...
+//   ]
+//
+// ...typically as processed from YAML, spreadsheet, or elsewhere.
+// Exposes as the same, but with the "who" replaced with matches found in
+// artistData - which means this always depends on an `artistData` property
+// also existing on this object!
+//
+
+import {input, templateCompositeFrom} from '#composite';
+import {isContributionList} from '#validators';
+
+import {exposeConstant, exposeDependencyOrContinue} from '#composite/control-flow';
+import {withResolvedContribs} from '#composite/wiki-data';
+
+export default templateCompositeFrom({
+  annotation: `contributionList`,
+
+  compose: false,
+
+  update: {validate: isContributionList},
+
+  steps: () => [
+    withResolvedContribs({from: input.updateValue()}),
+    exposeDependencyOrContinue({dependency: '#resolvedContribs'}),
+    exposeConstant({value: input.value([])}),
+  ],
+});
diff --git a/src/data/composite/wiki-properties/dimensions.js b/src/data/composite/wiki-properties/dimensions.js
new file mode 100644
index 0000000..57a0127
--- /dev/null
+++ b/src/data/composite/wiki-properties/dimensions.js
@@ -0,0 +1,13 @@
+// Plain ol' image dimensions. This is a two-item array of positive integers,
+// corresponding to width and height respectively.
+
+import {isDimensions} from '#validators';
+
+// TODO: Not templateCompositeFrom.
+
+export default function() {
+  return {
+    flags: {update: true, expose: true},
+    update: {validate: isDimensions},
+  };
+}
diff --git a/src/data/composite/wiki-properties/directory.js b/src/data/composite/wiki-properties/directory.js
new file mode 100644
index 0000000..0b2181c
--- /dev/null
+++ b/src/data/composite/wiki-properties/directory.js
@@ -0,0 +1,23 @@
+// The all-encompassing "directory" property, used as the unique identifier for
+// almost any data object. Also corresponds to a part of the URL which pages of
+// such objects are visited at.
+
+import {isDirectory} from '#validators';
+import {getKebabCase} from '#wiki-data';
+
+// TODO: Not templateCompositeFrom.
+
+export default function() {
+  return {
+    flags: {update: true, expose: true},
+    update: {validate: isDirectory},
+    expose: {
+      dependencies: ['name'],
+      transform(directory, {name}) {
+        if (directory === null && name === null) return null;
+        else if (directory === null) return getKebabCase(name);
+        else return directory;
+      },
+    },
+  };
+}
diff --git a/src/data/composite/wiki-properties/duration.js b/src/data/composite/wiki-properties/duration.js
new file mode 100644
index 0000000..827f282
--- /dev/null
+++ b/src/data/composite/wiki-properties/duration.js
@@ -0,0 +1,13 @@
+// Duration! This is a number of seconds, possibly floating point, always
+// at minimum zero.
+
+import {isDuration} from '#validators';
+
+// TODO: Not templateCompositeFrom.
+
+export default function() {
+  return {
+    flags: {update: true, expose: true},
+    update: {validate: isDuration},
+  };
+}
diff --git a/src/data/composite/wiki-properties/externalFunction.js b/src/data/composite/wiki-properties/externalFunction.js
new file mode 100644
index 0000000..c388da6
--- /dev/null
+++ b/src/data/composite/wiki-properties/externalFunction.js
@@ -0,0 +1,11 @@
+// External function. These should only be used as dependencies for other
+// properties, so they're left unexposed.
+
+// TODO: Not templateCompositeFrom.
+
+export default function() {
+  return {
+    flags: {update: true},
+    update: {validate: (t) => typeof t === 'function'},
+  };
+}
diff --git a/src/data/composite/wiki-properties/fileExtension.js b/src/data/composite/wiki-properties/fileExtension.js
new file mode 100644
index 0000000..c926fa8
--- /dev/null
+++ b/src/data/composite/wiki-properties/fileExtension.js
@@ -0,0 +1,13 @@
+// A file extension! Or the default, if provided when calling this.
+
+import {isFileExtension} from '#validators';
+
+// TODO: Not templateCompositeFrom.
+
+export default function(defaultFileExtension = null) {
+  return {
+    flags: {update: true, expose: true},
+    update: {validate: isFileExtension},
+    expose: {transform: (value) => value ?? defaultFileExtension},
+  };
+}
diff --git a/src/data/composite/wiki-properties/flag.js b/src/data/composite/wiki-properties/flag.js
new file mode 100644
index 0000000..076e663
--- /dev/null
+++ b/src/data/composite/wiki-properties/flag.js
@@ -0,0 +1,19 @@
+// Straightforward flag descriptor for a variety of property purposes.
+// Provide a default value, true or false!
+
+import {isBoolean} from '#validators';
+
+// TODO: Not templateCompositeFrom.
+
+// TODO: The description is a lie. This defaults to false. Bad.
+
+export default function(defaultValue = false) {
+  if (typeof defaultValue !== 'boolean') {
+    throw new TypeError(`Always set explicit defaults for flags!`);
+  }
+
+  return {
+    flags: {update: true, expose: true},
+    update: {validate: isBoolean, default: defaultValue},
+  };
+}
diff --git a/src/data/composite/wiki-properties/index.js b/src/data/composite/wiki-properties/index.js
new file mode 100644
index 0000000..2462b04
--- /dev/null
+++ b/src/data/composite/wiki-properties/index.js
@@ -0,0 +1,20 @@
+export {default as additionalFiles} from './additionalFiles.js';
+export {default as color} from './color.js';
+export {default as commentary} from './commentary.js';
+export {default as commentatorArtists} from './commentatorArtists.js';
+export {default as contribsPresent} from './contribsPresent.js';
+export {default as contributionList} from './contributionList.js';
+export {default as dimensions} from './dimensions.js';
+export {default as directory} from './directory.js';
+export {default as duration} from './duration.js';
+export {default as externalFunction} from './externalFunction.js';
+export {default as fileExtension} from './fileExtension.js';
+export {default as flag} from './flag.js';
+export {default as name} from './name.js';
+export {default as referenceList} from './referenceList.js';
+export {default as reverseReferenceList} from './reverseReferenceList.js';
+export {default as simpleDate} from './simpleDate.js';
+export {default as simpleString} from './simpleString.js';
+export {default as singleReference} from './singleReference.js';
+export {default as urls} from './urls.js';
+export {default as wikiData} from './wikiData.js';
diff --git a/src/data/composite/wiki-properties/name.js b/src/data/composite/wiki-properties/name.js
new file mode 100644
index 0000000..5146488
--- /dev/null
+++ b/src/data/composite/wiki-properties/name.js
@@ -0,0 +1,11 @@
+// A wiki data object's name! Its directory (i.e. unique identifier) will be
+// computed based on this value if not otherwise specified.
+
+import {isName} from '#validators';
+
+export default function(defaultName) {
+  return {
+    flags: {update: true, expose: true},
+    update: {validate: isName, default: defaultName},
+  };
+}
diff --git a/src/data/composite/wiki-properties/referenceList.js b/src/data/composite/wiki-properties/referenceList.js
new file mode 100644
index 0000000..f5b6c58
--- /dev/null
+++ b/src/data/composite/wiki-properties/referenceList.js
@@ -0,0 +1,47 @@
+// Stores and exposes a list of references to other data objects; all items
+// must be references to the same type, which is specified on the class input.
+//
+// See also:
+//  - singleReference
+//  - withResolvedReferenceList
+//
+
+import {input, templateCompositeFrom} from '#composite';
+import {validateReferenceList} from '#validators';
+
+import {exposeDependency} from '#composite/control-flow';
+import {inputThingClass, inputWikiData, withResolvedReferenceList}
+  from '#composite/wiki-data';
+
+// TODO: Kludge.
+import Thing from '../../things/thing.js';
+
+export default templateCompositeFrom({
+  annotation: `referenceList`,
+
+  compose: false,
+
+  inputs: {
+    class: inputThingClass(),
+
+    data: inputWikiData({allowMixedTypes: false}),
+    find: input({type: 'function'}),
+  },
+
+  update: ({
+    [input.staticValue('class')]: thingClass,
+  }) => {
+    const {[Thing.referenceType]: referenceType} = thingClass;
+    return {validate: validateReferenceList(referenceType)};
+  },
+
+  steps: () => [
+    withResolvedReferenceList({
+      list: input.updateValue(),
+      data: input('data'),
+      find: input('find'),
+    }),
+
+    exposeDependency({dependency: '#resolvedReferenceList'}),
+  ],
+});
diff --git a/src/data/composite/wiki-properties/reverseReferenceList.js b/src/data/composite/wiki-properties/reverseReferenceList.js
new file mode 100644
index 0000000..84ba67d
--- /dev/null
+++ b/src/data/composite/wiki-properties/reverseReferenceList.js
@@ -0,0 +1,30 @@
+// Neat little shortcut for "reversing" the reference lists stored on other
+// things - for example, tracks specify a "referenced tracks" property, and
+// you would use this to compute a corresponding "referenced *by* tracks"
+// property. Naturally, the passed ref list property is of the things in the
+// wiki data provided, not the requesting Thing itself.
+
+import {input, templateCompositeFrom} from '#composite';
+
+import {exposeDependency} from '#composite/control-flow';
+import {inputWikiData, withReverseReferenceList} from '#composite/wiki-data';
+
+export default templateCompositeFrom({
+  annotation: `reverseReferenceList`,
+
+  compose: false,
+
+  inputs: {
+    data: inputWikiData({allowMixedTypes: false}),
+    list: input({type: 'string'}),
+  },
+
+  steps: () => [
+    withReverseReferenceList({
+      data: input('data'),
+      list: input('list'),
+    }),
+
+    exposeDependency({dependency: '#reverseReferenceList'}),
+  ],
+});
diff --git a/src/data/composite/wiki-properties/simpleDate.js b/src/data/composite/wiki-properties/simpleDate.js
new file mode 100644
index 0000000..f08d832
--- /dev/null
+++ b/src/data/composite/wiki-properties/simpleDate.js
@@ -0,0 +1,14 @@
+// General date type, used as the descriptor for a bunch of properties.
+// This isn't dynamic though - it won't inherit from a date stored on
+// another object, for example.
+
+import {isDate} from '#validators';
+
+// TODO: Not templateCompositeFrom.
+
+export default function() {
+  return {
+    flags: {update: true, expose: true},
+    update: {validate: isDate},
+  };
+}
diff --git a/src/data/composite/wiki-properties/simpleString.js b/src/data/composite/wiki-properties/simpleString.js
new file mode 100644
index 0000000..18d6514
--- /dev/null
+++ b/src/data/composite/wiki-properties/simpleString.js
@@ -0,0 +1,14 @@
+// General string type. This should probably generally be avoided in favor
+// of more specific validation, but using it makes it easy to find where we
+// might want to improve later, and it's a useful shorthand meanwhile.
+
+import {isString} from '#validators';
+
+// TODO: Not templateCompositeFrom.
+
+export default function() {
+  return {
+    flags: {update: true, expose: true},
+    update: {validate: isString},
+  };
+}
diff --git a/src/data/composite/wiki-properties/singleReference.js b/src/data/composite/wiki-properties/singleReference.js
new file mode 100644
index 0000000..34bd2e6
--- /dev/null
+++ b/src/data/composite/wiki-properties/singleReference.js
@@ -0,0 +1,47 @@
+// Stores and exposes one connection, or reference, to another data object.
+// The reference must be to a specific type, which is specified on the class
+// input.
+//
+// See also:
+//  - referenceList
+//  - withResolvedReference
+//
+
+import {input, templateCompositeFrom} from '#composite';
+import {validateReference} from '#validators';
+
+import {exposeDependency} from '#composite/control-flow';
+import {inputThingClass, inputWikiData, withResolvedReference}
+  from '#composite/wiki-data';
+
+// TODO: Kludge.
+import Thing from '../../things/thing.js';
+
+export default templateCompositeFrom({
+  annotation: `singleReference`,
+
+  compose: false,
+
+  inputs: {
+    class: inputThingClass(),
+    find: input({type: 'function'}),
+    data: inputWikiData({allowMixedTypes: false}),
+  },
+
+  update: ({
+    [input.staticValue('class')]: thingClass,
+  }) => {
+    const {[Thing.referenceType]: referenceType} = thingClass;
+    return {validate: validateReference(referenceType)};
+  },
+
+  steps: () => [
+    withResolvedReference({
+      ref: input.updateValue(),
+      data: input('data'),
+      find: input('find'),
+    }),
+
+    exposeDependency({dependency: '#resolvedReference'}),
+  ],
+});
diff --git a/src/data/composite/wiki-properties/urls.js b/src/data/composite/wiki-properties/urls.js
new file mode 100644
index 0000000..3160a0b
--- /dev/null
+++ b/src/data/composite/wiki-properties/urls.js
@@ -0,0 +1,14 @@
+// A list of URLs! This will always be present on the data object, even if set
+// to an empty array or null.
+
+import {isURL, validateArrayItems} from '#validators';
+
+// TODO: Not templateCompositeFrom.
+
+export default function() {
+  return {
+    flags: {update: true, expose: true},
+    update: {validate: validateArrayItems(isURL)},
+    expose: {transform: value => value ?? []},
+  };
+}
diff --git a/src/data/composite/wiki-properties/wikiData.js b/src/data/composite/wiki-properties/wikiData.js
new file mode 100644
index 0000000..4ea4778
--- /dev/null
+++ b/src/data/composite/wiki-properties/wikiData.js
@@ -0,0 +1,17 @@
+// General purpose wiki data constructor, for properties like artistData,
+// trackData, etc.
+
+import {validateArrayItems, validateInstanceOf} from '#validators';
+
+// TODO: Not templateCompositeFrom.
+
+// TODO: This should validate with validateWikiData.
+
+export default function(thingClass) {
+  return {
+    flags: {update: true},
+    update: {
+      validate: validateArrayItems(validateInstanceOf(thingClass)),
+    },
+  };
+}
diff --git a/src/data/things/album.js b/src/data/things/album.js
index c012c24..546fda3 100644
--- a/src/data/things/album.js
+++ b/src/data/things/album.js
@@ -1,163 +1,143 @@
-import {empty} from '#sugar';
+import {input} from '#composite';
 import find from '#find';
+import {isDate} from '#validators';
+
+import {exposeDependency, exposeUpdateValueOrContinue}
+  from '#composite/control-flow';
+import {exitWithoutContribs} from '#composite/wiki-data';
+
+import {
+  additionalFiles,
+  commentary,
+  color,
+  commentatorArtists,
+  contribsPresent,
+  contributionList,
+  dimensions,
+  directory,
+  fileExtension,
+  flag,
+  name,
+  referenceList,
+  simpleDate,
+  simpleString,
+  urls,
+  wikiData,
+} from '#composite/wiki-properties';
+
+import {
+  withTracks,
+  withTrackSections,
+} from '#composite/things/album';
 
 import Thing from './thing.js';
 
 export class Album extends Thing {
   static [Thing.referenceType] = 'album';
 
-  static [Thing.getPropertyDescriptors] = ({
-    ArtTag,
-    Artist,
-    Group,
-    Track,
-
-    validators: {
-      isDate,
-      isDimensions,
-      isTrackSectionList,
-    },
-  }) => ({
+  static [Thing.getPropertyDescriptors] = ({ArtTag, Artist, Group, Track}) => ({
     // Update & expose
 
-    name: Thing.common.name('Unnamed Album'),
-    color: Thing.common.color(),
-    directory: Thing.common.directory(),
-    urls: Thing.common.urls(),
-
-    date: Thing.common.simpleDate(),
-    trackArtDate: Thing.common.simpleDate(),
-    dateAddedToWiki: Thing.common.simpleDate(),
-
-    coverArtDate: {
-      flags: {update: true, expose: true},
-
-      update: {validate: isDate},
-
-      expose: {
-        dependencies: ['date', 'coverArtistContribsByRef'],
-        transform: (coverArtDate, {
-          coverArtistContribsByRef,
-          date,
-        }) =>
-          (!empty(coverArtistContribsByRef)
-            ? coverArtDate ?? date ?? null
-            : null),
-      },
-    },
-
-    artistContribsByRef: Thing.common.contribsByRef(),
-    coverArtistContribsByRef: Thing.common.contribsByRef(),
-    trackCoverArtistContribsByRef: Thing.common.contribsByRef(),
-    wallpaperArtistContribsByRef: Thing.common.contribsByRef(),
-    bannerArtistContribsByRef: Thing.common.contribsByRef(),
-
-    groupsByRef: Thing.common.referenceList(Group),
-    artTagsByRef: Thing.common.referenceList(ArtTag),
-
-    trackSections: {
-      flags: {update: true, expose: true},
-
-      update: {
-        validate: isTrackSectionList,
-      },
-
-      expose: {
-        dependencies: ['color', 'trackData'],
-        transform(trackSections, {
-          color: albumColor,
-          trackData,
-        }) {
-          let startIndex = 0;
-          return trackSections?.map(section => ({
-            name: section.name ?? null,
-            color: section.color ?? albumColor ?? null,
-            dateOriginallyReleased: section.dateOriginallyReleased ?? null,
-            isDefaultTrackSection: section.isDefaultTrackSection ?? false,
-
-            startIndex: (
-              startIndex += section.tracksByRef.length,
-              startIndex - section.tracksByRef.length
-            ),
-
-            tracksByRef: section.tracksByRef ?? [],
-            tracks:
-              (trackData && section.tracksByRef
-                ?.map(ref => find.track(ref, trackData, {mode: 'quiet'}))
-                .filter(Boolean)) ??
-              [],
-          }));
-        },
-      },
-    },
-
-    coverArtFileExtension: Thing.common.fileExtension('jpg'),
-    trackCoverArtFileExtension: Thing.common.fileExtension('jpg'),
-
-    wallpaperStyle: Thing.common.simpleString(),
-    wallpaperFileExtension: Thing.common.fileExtension('jpg'),
-
-    bannerStyle: Thing.common.simpleString(),
-    bannerFileExtension: Thing.common.fileExtension('jpg'),
-    bannerDimensions: {
-      flags: {update: true, expose: true},
-      update: {validate: isDimensions},
-    },
-
-    hasTrackNumbers: Thing.common.flag(true),
-    isListedOnHomepage: Thing.common.flag(true),
-    isListedInGalleries: Thing.common.flag(true),
-
-    commentary: Thing.common.commentary(),
-    additionalFiles: Thing.common.additionalFiles(),
+    name: name('Unnamed Album'),
+    color: color(),
+    directory: directory(),
+    urls: urls(),
+
+    date: simpleDate(),
+    trackArtDate: simpleDate(),
+    dateAddedToWiki: simpleDate(),
+
+    coverArtDate: [
+      exitWithoutContribs({contribs: 'coverArtistContribs'}),
+
+      exposeUpdateValueOrContinue({
+        validate: input.value(isDate),
+      }),
+
+      exposeDependency({dependency: 'date'}),
+    ],
+
+    coverArtFileExtension: [
+      exitWithoutContribs({contribs: 'coverArtistContribs'}),
+      fileExtension('jpg'),
+    ],
+
+    trackCoverArtFileExtension: fileExtension('jpg'),
+
+    wallpaperFileExtension: [
+      exitWithoutContribs({contribs: 'wallpaperArtistContribs'}),
+      fileExtension('jpg'),
+    ],
+
+    bannerFileExtension: [
+      exitWithoutContribs({contribs: 'bannerArtistContribs'}),
+      fileExtension('jpg'),
+    ],
+
+    wallpaperStyle: [
+      exitWithoutContribs({contribs: 'wallpaperArtistContribs'}),
+      simpleString(),
+    ],
+
+    bannerStyle: [
+      exitWithoutContribs({contribs: 'bannerArtistContribs'}),
+      simpleString(),
+    ],
+
+    bannerDimensions: [
+      exitWithoutContribs({contribs: 'bannerArtistContribs'}),
+      dimensions(),
+    ],
+
+    hasTrackNumbers: flag(true),
+    isListedOnHomepage: flag(true),
+    isListedInGalleries: flag(true),
+
+    commentary: commentary(),
+    additionalFiles: additionalFiles(),
+
+    trackSections: [
+      withTrackSections(),
+      exposeDependency({dependency: '#trackSections'}),
+    ],
+
+    artistContribs: contributionList(),
+    coverArtistContribs: contributionList(),
+    trackCoverArtistContribs: contributionList(),
+    wallpaperArtistContribs: contributionList(),
+    bannerArtistContribs: contributionList(),
+
+    groups: referenceList({
+      class: input.value(Group),
+      find: input.value(find.group),
+      data: 'groupData',
+    }),
+
+    artTags: referenceList({
+      class: input.value(ArtTag),
+      find: input.value(find.artTag),
+      data: 'artTagData',
+    }),
 
     // Update only
 
-    artistData: Thing.common.wikiData(Artist),
-    artTagData: Thing.common.wikiData(ArtTag),
-    groupData: Thing.common.wikiData(Group),
-    trackData: Thing.common.wikiData(Track),
+    artistData: wikiData(Artist),
+    artTagData: wikiData(ArtTag),
+    groupData: wikiData(Group),
+    trackData: wikiData(Track),
 
     // Expose only
 
-    artistContribs: Thing.common.dynamicContribs('artistContribsByRef'),
-    coverArtistContribs: Thing.common.dynamicContribs('coverArtistContribsByRef'),
-    trackCoverArtistContribs: Thing.common.dynamicContribs('trackCoverArtistContribsByRef'),
-    wallpaperArtistContribs: Thing.common.dynamicContribs('wallpaperArtistContribsByRef'),
-    bannerArtistContribs: Thing.common.dynamicContribs('bannerArtistContribsByRef'),
-
-    commentatorArtists: Thing.common.commentatorArtists(),
-
-    hasCoverArt: Thing.common.contribsPresent('coverArtistContribsByRef'),
-    hasWallpaperArt: Thing.common.contribsPresent('wallpaperArtistContribsByRef'),
-    hasBannerArt: Thing.common.contribsPresent('bannerArtistContribsByRef'),
-
-    tracks: {
-      flags: {expose: true},
-
-      expose: {
-        dependencies: ['trackSections', 'trackData'],
-        compute: ({trackSections, trackData}) =>
-          trackSections && trackData
-            ? trackSections
-                .flatMap((section) => section.tracksByRef ?? [])
-                .map((ref) => find.track(ref, trackData, {mode: 'quiet'}))
-                .filter(Boolean)
-            : [],
-      },
-    },
-
-    groups: Thing.common.dynamicThingsFromReferenceList(
-      'groupsByRef',
-      'groupData',
-      find.group
-    ),
-
-    artTags: Thing.common.dynamicThingsFromReferenceList(
-      'artTagsByRef',
-      'artTagData',
-      find.artTag
-    ),
+    commentatorArtists: commentatorArtists(),
+
+    hasCoverArt: contribsPresent({contribs: 'coverArtistContribs'}),
+    hasWallpaperArt: contribsPresent({contribs: 'wallpaperArtistContribs'}),
+    hasBannerArt: contribsPresent({contribs: 'bannerArtistContribs'}),
+
+    tracks: [
+      withTracks(),
+      exposeDependency({dependency: '#tracks'}),
+    ],
   });
 
   static [Thing.getSerializeDescriptors] = ({
@@ -201,10 +181,12 @@ export class Album extends Thing {
 }
 
 export class TrackSectionHelper extends Thing {
+  static [Thing.friendlyName] = `Track Section`;
+
   static [Thing.getPropertyDescriptors] = () => ({
-    name: Thing.common.name('Unnamed Track Group'),
-    color: Thing.common.color(),
-    dateOriginallyReleased: Thing.common.simpleDate(),
-    isDefaultTrackGroup: Thing.common.flag(false),
+    name: name('Unnamed Track Section'),
+    color: color(),
+    dateOriginallyReleased: simpleDate(),
+    isDefaultTrackGroup: flag(false),
   })
 }
diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js
index c103c4d..6503bee 100644
--- a/src/data/things/art-tag.js
+++ b/src/data/things/art-tag.js
@@ -1,35 +1,47 @@
+import {input} from '#composite';
 import {sortAlbumsTracksChronologically} from '#wiki-data';
+import {isName} from '#validators';
+
+import {exposeUpdateValueOrContinue} from '#composite/control-flow';
+
+import {
+  color,
+  directory,
+  flag,
+  name,
+  wikiData,
+} from '#composite/wiki-properties';
 
 import Thing from './thing.js';
 
 export class ArtTag extends Thing {
   static [Thing.referenceType] = 'tag';
+  static [Thing.friendlyName] = `Art Tag`;
 
-  static [Thing.getPropertyDescriptors] = ({
-    Album,
-    Track,
-  }) => ({
+  static [Thing.getPropertyDescriptors] = ({Album, Track}) => ({
     // Update & expose
 
-    name: Thing.common.name('Unnamed Art Tag'),
-    directory: Thing.common.directory(),
-    color: Thing.common.color(),
-    isContentWarning: Thing.common.flag(false),
+    name: name('Unnamed Art Tag'),
+    directory: directory(),
+    color: color(),
+    isContentWarning: flag(false),
 
-    nameShort: {
-      flags: {update: true, expose: true},
+    nameShort: [
+      exposeUpdateValueOrContinue({
+        validate: input.value(isName),
+      }),
 
-      expose: {
+      {
         dependencies: ['name'],
-        transform: (value, {name}) =>
-          value ?? name.replace(/ \(.*?\)$/, ''),
+        compute: ({name}) =>
+          name.replace(/ \([^)]*?\)$/, ''),
       },
-    },
+    ],
 
     // Update only
 
-    albumData: Thing.common.wikiData(Album),
-    trackData: Thing.common.wikiData(Track),
+    albumData: wikiData(Album),
+    trackData: wikiData(Track),
 
     // Expose only
 
@@ -37,8 +49,8 @@ export class ArtTag extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['albumData', 'trackData'],
-        compute: ({albumData, trackData, [ArtTag.instance]: artTag}) =>
+        dependencies: ['this', 'albumData', 'trackData'],
+        compute: ({this: artTag, albumData, trackData}) =>
           sortAlbumsTracksChronologically(
             [...albumData, ...trackData]
               .filter(({artTags}) => artTags.includes(artTag)),
diff --git a/src/data/things/artist.js b/src/data/things/artist.js
index 6d4f4a0..1b313db 100644
--- a/src/data/things/artist.js
+++ b/src/data/things/artist.js
@@ -1,29 +1,33 @@
+import {input} from '#composite';
 import find from '#find';
+import {isName, validateArrayItems} from '#validators';
+
+import {
+  directory,
+  fileExtension,
+  flag,
+  name,
+  simpleString,
+  singleReference,
+  urls,
+  wikiData,
+} from '#composite/wiki-properties';
 
 import Thing from './thing.js';
 
 export class Artist extends Thing {
   static [Thing.referenceType] = 'artist';
 
-  static [Thing.getPropertyDescriptors] = ({
-    Album,
-    Flash,
-    Track,
-
-    validators: {
-      isName,
-      validateArrayItems,
-    },
-  }) => ({
+  static [Thing.getPropertyDescriptors] = ({Album, Flash, Track}) => ({
     // Update & expose
 
-    name: Thing.common.name('Unnamed Artist'),
-    directory: Thing.common.directory(),
-    urls: Thing.common.urls(),
-    contextNotes: Thing.common.simpleString(),
+    name: name('Unnamed Artist'),
+    directory: directory(),
+    urls: urls(),
+    contextNotes: simpleString(),
 
-    hasAvatar: Thing.common.flag(false),
-    avatarFileExtension: Thing.common.fileExtension('jpg'),
+    hasAvatar: flag(false),
+    avatarFileExtension: fileExtension('jpg'),
 
     aliasNames: {
       flags: {update: true, expose: true},
@@ -31,30 +35,23 @@ export class Artist extends Thing {
       expose: {transform: (names) => names ?? []},
     },
 
-    isAlias: Thing.common.flag(),
-    aliasedArtistRef: Thing.common.singleReference(Artist),
+    isAlias: flag(),
+
+    aliasedArtist: singleReference({
+      class: input.value(Artist),
+      find: input.value(find.artist),
+      data: 'artistData',
+    }),
 
     // Update only
 
-    albumData: Thing.common.wikiData(Album),
-    artistData: Thing.common.wikiData(Artist),
-    flashData: Thing.common.wikiData(Flash),
-    trackData: Thing.common.wikiData(Track),
+    albumData: wikiData(Album),
+    artistData: wikiData(Artist),
+    flashData: wikiData(Flash),
+    trackData: wikiData(Track),
 
     // Expose only
 
-    aliasedArtist: {
-      flags: {expose: true},
-
-      expose: {
-        dependencies: ['artistData', 'aliasedArtistRef'],
-        compute: ({artistData, aliasedArtistRef}) =>
-          aliasedArtistRef && artistData
-            ? find.artist(aliasedArtistRef, artistData, {mode: 'quiet'})
-            : null,
-      },
-    },
-
     tracksAsArtist:
       Artist.filterByContrib('trackData', 'artistContribs'),
     tracksAsContributor:
@@ -66,14 +63,14 @@ export class Artist extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['trackData'],
+        dependencies: ['this', 'trackData'],
 
-        compute: ({trackData, [Artist.instance]: artist}) =>
+        compute: ({this: artist, trackData}) =>
           trackData?.filter((track) =>
             [
-              ...track.artistContribs,
-              ...track.contributorContribs,
-              ...track.coverArtistContribs,
+              ...track.artistContribs ?? [],
+              ...track.contributorContribs ?? [],
+              ...track.coverArtistContribs ?? [],
             ].some(({who}) => who === artist)) ?? [],
       },
     },
@@ -82,9 +79,9 @@ export class Artist extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['trackData'],
+        dependencies: ['this', 'trackData'],
 
-        compute: ({trackData, [Artist.instance]: artist}) =>
+        compute: ({this: artist, trackData}) =>
           trackData?.filter(({commentatorArtists}) =>
             commentatorArtists.includes(artist)) ?? [],
       },
@@ -120,18 +117,16 @@ export class Artist extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['albumData'],
+        dependencies: ['this', 'albumData'],
 
-        compute: ({albumData, [Artist.instance]: artist}) =>
+        compute: ({this: artist, albumData}) =>
           albumData?.filter(({commentatorArtists}) =>
             commentatorArtists.includes(artist)) ?? [],
       },
     },
 
-    flashesAsContributor: Artist.filterByContrib(
-      'flashData',
-      'contributorContribs'
-    ),
+    flashesAsContributor:
+      Artist.filterByContrib('flashData', 'contributorContribs'),
   });
 
   static [Thing.getSerializeDescriptors] = ({
@@ -165,15 +160,15 @@ export class Artist extends Thing {
     flags: {expose: true},
 
     expose: {
-      dependencies: [thingDataProperty],
+      dependencies: ['this', thingDataProperty],
 
       compute: ({
+        this: artist,
         [thingDataProperty]: thingData,
-        [Artist.instance]: artist
       }) =>
         thingData?.filter(thing =>
           thing[contribsProperty]
-            .some(contrib => contrib.who === artist)) ?? [],
+            ?.some(contrib => contrib.who === artist)) ?? [],
     },
   });
 }
diff --git a/src/data/things/cacheable-object.js b/src/data/things/cacheable-object.js
index ea705a6..9fda865 100644
--- a/src/data/things/cacheable-object.js
+++ b/src/data/things/cacheable-object.js
@@ -76,28 +76,24 @@
 
 import {inspect as nodeInspect} from 'node:util';
 
-import {color, ENABLE_COLOR} from '#cli';
+import {colors, ENABLE_COLOR} from '#cli';
 
 function inspect(value) {
   return nodeInspect(value, {colors: ENABLE_COLOR});
 }
 
 export default class CacheableObject {
-  static instance = Symbol('CacheableObject `this` instance');
-
   #propertyUpdateValues = Object.create(null);
   #propertyUpdateCacheInvalidators = Object.create(null);
 
-  /*
-    // Note the constructor doesn't take an initial data source. Due to a quirk
-    // of JavaScript, private members can't be accessed before the superclass's
-    // constructor is finished processing - so if we call the overridden
-    // update() function from inside this constructor, it will error when
-    // writing to private members. Pretty bad!
-    //
-    // That means initial data must be provided by following up with update()
-    // after constructing the new instance of the Thing (sub)class.
-    */
+  // Note the constructor doesn't take an initial data source. Due to a quirk
+  // of JavaScript, private members can't be accessed before the superclass's
+  // constructor is finished processing - so if we call the overridden
+  // update() function from inside this constructor, it will error when
+  // writing to private members. Pretty bad!
+  //
+  // That means initial data must be provided by following up with update()
+  // after constructing the new instance of the Thing (sub)class.
 
   constructor() {
     this.#defineProperties();
@@ -143,7 +139,7 @@ export default class CacheableObject {
 
       const definition = {
         configurable: false,
-        enumerable: true,
+        enumerable: flags.expose,
       };
 
       if (flags.update) {
@@ -183,13 +179,8 @@ export default class CacheableObject {
           } else if (result !== true) {
             throw new TypeError(`Validation failed for value ${newValue}`);
           }
-        } catch (error) {
-          error.message = [
-            `Property ${color.green(property)}`,
-            `(${inspect(this[property])} -> ${inspect(newValue)}):`,
-            error.message
-          ].join(' ');
-          throw error;
+        } catch (caughtError) {
+          throw new CacheableObjectPropertyValueError(property, this[property], newValue, caughtError);
         }
       }
 
@@ -250,20 +241,27 @@ export default class CacheableObject {
 
     let getAllDependencies;
 
-    const dependencyKeys = expose.dependencies;
-    if (dependencyKeys?.length > 0) {
-      const reflectionEntry = [this.constructor.instance, this];
-      const dependencyGetters = dependencyKeys
-        .map(key => () => [key, this.#propertyUpdateValues[key]]);
+    if (expose.dependencies?.length > 0) {
+      const dependencyKeys = expose.dependencies.slice();
+      const shouldReflect = dependencyKeys.includes('this');
+
+      getAllDependencies = () => {
+        const dependencies = Object.create(null);
+
+        for (const key of dependencyKeys) {
+          dependencies[key] = this.#propertyUpdateValues[key];
+        }
+
+        if (shouldReflect) {
+          dependencies.this = this;
+        }
 
-      getAllDependencies = () =>
-        Object.fromEntries(dependencyGetters
-          .map(f => f())
-          .concat([reflectionEntry]));
+        return dependencies;
+      };
     } else {
-      const allDependencies = {[this.constructor.instance]: this};
-      Object.freeze(allDependencies);
-      getAllDependencies = () => allDependencies;
+      const dependencies = Object.create(null);
+      Object.freeze(dependencies);
+      getAllDependencies = () => dependencies;
     }
 
     if (flags.update) {
@@ -347,4 +345,22 @@ export default class CacheableObject {
       console.log(` - ${line}`);
     }
   }
+
+  static getUpdateValue(object, key) {
+    if (!Object.hasOwn(object, key)) {
+      return undefined;
+    }
+
+    return object.#propertyUpdateValues[key] ?? null;
+  }
+}
+
+export class CacheableObjectPropertyValueError extends Error {
+  constructor(property, oldValue, newValue, error) {
+    super(
+      `Error setting ${colors.green(property)} (${inspect(oldValue)} -> ${inspect(newValue)})`,
+      {cause: error});
+
+    this.property = property;
+  }
 }
diff --git a/src/data/things/composite.js b/src/data/things/composite.js
new file mode 100644
index 0000000..51525bc
--- /dev/null
+++ b/src/data/things/composite.js
@@ -0,0 +1,1301 @@
+import {inspect} from 'node:util';
+
+import {colors} from '#cli';
+import {TupleMap} from '#wiki-data';
+import {a} from '#validators';
+
+import {
+  decorateErrorWithIndex,
+  empty,
+  filterProperties,
+  openAggregate,
+  stitchArrays,
+  typeAppearance,
+  unique,
+  withAggregate,
+} from '#sugar';
+
+const globalCompositeCache = {};
+
+const _valueIntoToken = shape =>
+  (value = null) =>
+    (value === null
+      ? Symbol.for(`hsmusic.composite.${shape}`)
+   : typeof value === 'string'
+      ? Symbol.for(`hsmusic.composite.${shape}:${value}`)
+      : {
+          symbol: Symbol.for(`hsmusic.composite.input`),
+          shape,
+          value,
+        });
+
+export const input = _valueIntoToken('input');
+input.symbol = Symbol.for('hsmusic.composite.input');
+
+input.value = _valueIntoToken('input.value');
+input.dependency = _valueIntoToken('input.dependency');
+
+input.myself = () => Symbol.for(`hsmusic.composite.input.myself`);
+
+input.updateValue = _valueIntoToken('input.updateValue');
+
+input.staticDependency = _valueIntoToken('input.staticDependency');
+input.staticValue = _valueIntoToken('input.staticValue');
+
+function isInputToken(token) {
+  if (token === null) {
+    return false;
+  } else if (typeof token === 'object') {
+    return token.symbol === Symbol.for('hsmusic.composite.input');
+  } else if (typeof token === 'symbol') {
+    return token.description.startsWith('hsmusic.composite.input');
+  } else {
+    return false;
+  }
+}
+
+function getInputTokenShape(token) {
+  if (!isInputToken(token)) {
+    throw new TypeError(`Expected an input token, got ${typeAppearance(token)}`);
+  }
+
+  if (typeof token === 'object') {
+    return token.shape;
+  } else {
+    return token.description.match(/hsmusic\.composite\.(input.*?)(:|$)/)[1];
+  }
+}
+
+function getInputTokenValue(token) {
+  if (!isInputToken(token)) {
+    throw new TypeError(`Expected an input token, got ${typeAppearance(token)}`);
+  }
+
+  if (typeof token === 'object') {
+    return token.value;
+  } else {
+    return token.description.match(/hsmusic\.composite\.input.*?:(.*)/)?.[1] ?? null;
+  }
+}
+
+function getStaticInputMetadata(inputOptions) {
+  const metadata = {};
+
+  for (const [name, token] of Object.entries(inputOptions)) {
+    if (typeof token === 'string') {
+      metadata[input.staticDependency(name)] = token;
+      metadata[input.staticValue(name)] = null;
+    } else if (isInputToken(token)) {
+      const tokenShape = getInputTokenShape(token);
+      const tokenValue = getInputTokenValue(token);
+
+      metadata[input.staticDependency(name)] =
+        (tokenShape === 'input.dependency'
+          ? tokenValue
+          : null);
+
+      metadata[input.staticValue(name)] =
+        (tokenShape === 'input.value'
+          ? tokenValue
+          : null);
+    } else {
+      metadata[input.staticDependency(name)] = null;
+      metadata[input.staticValue(name)] = null;
+    }
+  }
+
+  return metadata;
+}
+
+function getCompositionName(description) {
+  return (
+    (description.annotation
+      ? description.annotation
+      : `unnamed composite`));
+}
+
+function validateInputValue(value, description) {
+  const tokenValue = getInputTokenValue(description);
+
+  const {acceptsNull, defaultValue, type, validate} = tokenValue || {};
+
+  if (value === null || value === undefined) {
+    if (acceptsNull || defaultValue === null) {
+      return true;
+    } else {
+      throw new TypeError(
+        (type
+          ? `Expected ${a(type)}, got ${typeAppearance(value)}`
+          : `Expected a value, got ${typeAppearance(value)}`));
+    }
+  }
+
+  if (type) {
+    // Note: null is already handled earlier in this function, so it won't
+    // cause any trouble here.
+    const typeofValue =
+      (typeof value === 'object'
+        ? Array.isArray(value) ? 'array' : 'object'
+        : typeof value);
+
+    if (typeofValue !== type) {
+      throw new TypeError(`Expected ${a(type)}, got ${typeAppearance(value)}`);
+    }
+  }
+
+  if (validate) {
+    validate(value);
+  }
+
+  return true;
+}
+
+export function templateCompositeFrom(description) {
+  const compositionName = getCompositionName(description);
+
+  withAggregate({message: `Errors in description for ${compositionName}`}, ({map, nest, push}) => {
+    if ('steps' in description) {
+      if (Array.isArray(description.steps)) {
+        push(new TypeError(`Wrap steps array in a function`));
+      } else if (typeof description.steps !== 'function') {
+        push(new TypeError(`Expected steps to be a function (returning an array)`));
+      }
+    }
+
+    validateInputs:
+    if ('inputs' in description) {
+      if (
+        Array.isArray(description.inputs) ||
+        typeof description.inputs !== 'object'
+      ) {
+        push(new Error(`Expected inputs to be object, got ${typeAppearance(description.inputs)}`));
+        break validateInputs;
+      }
+
+      nest({message: `Errors in static input descriptions for ${compositionName}`}, ({push}) => {
+        const missingCallsToInput = [];
+        const wrongCallsToInput = [];
+
+        for (const [name, value] of Object.entries(description.inputs)) {
+          if (!isInputToken(value)) {
+            missingCallsToInput.push(name);
+            continue;
+          }
+
+          if (!['input', 'input.staticDependency', 'input.staticValue'].includes(getInputTokenShape(value))) {
+            wrongCallsToInput.push(name);
+          }
+        }
+
+        for (const name of missingCallsToInput) {
+          push(new Error(`${name}: Missing call to input()`));
+        }
+
+        for (const name of wrongCallsToInput) {
+          const shape = getInputTokenShape(description.inputs[name]);
+          push(new Error(`${name}: Expected call to input, input.staticDependency, or input.staticValue, got ${shape}`));
+        }
+      });
+    }
+
+    validateOutputs:
+    if ('outputs' in description) {
+      if (
+        !Array.isArray(description.outputs) &&
+        typeof description.outputs !== 'function'
+      ) {
+        push(new Error(`Expected outputs to be array or function, got ${typeAppearance(description.outputs)}`));
+        break validateOutputs;
+      }
+
+      if (Array.isArray(description.outputs)) {
+        map(
+          description.outputs,
+          decorateErrorWithIndex(value => {
+            if (typeof value !== 'string') {
+              throw new Error(`${value}: Expected string, got ${typeAppearance(value)}`)
+            } else if (!value.startsWith('#')) {
+              throw new Error(`${value}: Expected "#" at start`);
+            }
+          }),
+          {message: `Errors in output descriptions for ${compositionName}`});
+      }
+    }
+  });
+
+  const expectedInputNames =
+    (description.inputs
+      ? Object.keys(description.inputs)
+      : []);
+
+  const instantiate = (inputOptions = {}) => {
+    withAggregate({message: `Errors in input options passed to ${compositionName}`}, ({push}) => {
+      const providedInputNames = Object.keys(inputOptions);
+
+      const misplacedInputNames =
+        providedInputNames
+          .filter(name => !expectedInputNames.includes(name));
+
+      const missingInputNames =
+        expectedInputNames
+          .filter(name => !providedInputNames.includes(name))
+          .filter(name => {
+            const inputDescription = getInputTokenValue(description.inputs[name]);
+            if (!inputDescription) return true;
+            if ('defaultValue' in inputDescription) return false;
+            if ('defaultDependency' in inputDescription) return false;
+            return true;
+          });
+
+      const wrongTypeInputNames = [];
+
+      const expectedStaticValueInputNames = [];
+      const expectedStaticDependencyInputNames = [];
+      const expectedValueProvidingTokenInputNames = [];
+
+      const validateFailedErrors = [];
+
+      for (const [name, value] of Object.entries(inputOptions)) {
+        if (misplacedInputNames.includes(name)) {
+          continue;
+        }
+
+        if (typeof value !== 'string' && !isInputToken(value)) {
+          wrongTypeInputNames.push(name);
+          continue;
+        }
+
+        const descriptionShape = getInputTokenShape(description.inputs[name]);
+
+        const tokenShape = (isInputToken(value) ? getInputTokenShape(value) : null);
+        const tokenValue = (isInputToken(value) ? getInputTokenValue(value) : null);
+
+        switch (descriptionShape) {
+          case'input.staticValue':
+            if (tokenShape !== 'input.value') {
+              expectedStaticValueInputNames.push(name);
+              continue;
+            }
+            break;
+
+          case 'input.staticDependency':
+            if (typeof value !== 'string' && tokenShape !== 'input.dependency') {
+              expectedStaticDependencyInputNames.push(name);
+              continue;
+            }
+            break;
+
+          case 'input':
+            if (typeof value !== 'string' && ![
+              'input',
+              'input.value',
+              'input.dependency',
+              'input.myself',
+              'input.updateValue',
+            ].includes(tokenShape)) {
+              expectedValueProvidingTokenInputNames.push(name);
+              continue;
+            }
+            break;
+        }
+
+        if (tokenShape === 'input.value') {
+          try {
+            validateInputValue(tokenValue, description.inputs[name]);
+          } catch (error) {
+            error.message = `${name}: ${error.message}`;
+            validateFailedErrors.push(error);
+          }
+        }
+      }
+
+      if (!empty(misplacedInputNames)) {
+        push(new Error(`Unexpected input names: ${misplacedInputNames.join(', ')}`));
+      }
+
+      if (!empty(missingInputNames)) {
+        push(new Error(`Required these inputs: ${missingInputNames.join(', ')}`));
+      }
+
+      const inputAppearance = name =>
+        (isInputToken(inputOptions[name])
+          ? `${getInputTokenShape(inputOptions[name])}() call`
+          : `dependency name`);
+
+      for (const name of expectedStaticDependencyInputNames) {
+        const appearance = inputAppearance(name);
+        push(new Error(`${name}: Expected dependency name, got ${appearance}`));
+      }
+
+      for (const name of expectedStaticValueInputNames) {
+        const appearance = inputAppearance(name)
+        push(new Error(`${name}: Expected input.value() call, got ${appearance}`));
+      }
+
+      for (const name of expectedValueProvidingTokenInputNames) {
+        const appearance = getInputTokenShape(inputOptions[name]);
+        push(new Error(`${name}: Expected dependency name or value-providing input() call, got ${appearance}`));
+      }
+
+      for (const name of wrongTypeInputNames) {
+        const type = typeAppearance(inputOptions[name]);
+        push(new Error(`${name}: Expected dependency name or input() call, got ${type}`));
+      }
+
+      for (const error of validateFailedErrors) {
+        push(error);
+      }
+    });
+
+    const inputMetadata = getStaticInputMetadata(inputOptions);
+
+    const expectedOutputNames =
+      (Array.isArray(description.outputs)
+        ? description.outputs
+     : typeof description.outputs === 'function'
+        ? description.outputs(inputMetadata)
+            .map(name =>
+              (name.startsWith('#')
+                ? name
+                : '#' + name))
+        : []);
+
+    const ownUpdateDescription =
+      (typeof description.update === 'object'
+        ? description.update
+     : typeof description.update === 'function'
+        ? description.update(inputMetadata)
+        : null);
+
+    const outputOptions = {};
+
+    const instantiatedTemplate = {
+      symbol: templateCompositeFrom.symbol,
+
+      outputs(providedOptions) {
+        withAggregate({message: `Errors in output options passed to ${compositionName}`}, ({push}) => {
+          const misplacedOutputNames = [];
+          const wrongTypeOutputNames = [];
+
+          for (const [name, value] of Object.entries(providedOptions)) {
+            if (!expectedOutputNames.includes(name)) {
+              misplacedOutputNames.push(name);
+              continue;
+            }
+
+            if (typeof value !== 'string') {
+              wrongTypeOutputNames.push(name);
+              continue;
+            }
+          }
+
+          if (!empty(misplacedOutputNames)) {
+            push(new Error(`Unexpected output names: ${misplacedOutputNames.join(', ')}`));
+          }
+
+          for (const name of wrongTypeOutputNames) {
+            const appearance = typeAppearance(providedOptions[name]);
+            push(new Error(`${name}: Expected string, got ${appearance}`));
+          }
+        });
+
+        Object.assign(outputOptions, providedOptions);
+        return instantiatedTemplate;
+      },
+
+      toDescription() {
+        const finalDescription = {};
+
+        if ('annotation' in description) {
+          finalDescription.annotation = description.annotation;
+        }
+
+        if ('compose' in description) {
+          finalDescription.compose = description.compose;
+        }
+
+        if (ownUpdateDescription) {
+          finalDescription.update = ownUpdateDescription;
+        }
+
+        if ('inputs' in description) {
+          const inputMapping = {};
+
+          for (const [name, token] of Object.entries(description.inputs)) {
+            const tokenValue = getInputTokenValue(token);
+            if (name in inputOptions) {
+              if (typeof inputOptions[name] === 'string') {
+                inputMapping[name] = input.dependency(inputOptions[name]);
+              } else {
+                inputMapping[name] = inputOptions[name];
+              }
+            } else if (tokenValue.defaultValue) {
+              inputMapping[name] = input.value(tokenValue.defaultValue);
+            } else if (tokenValue.defaultDependency) {
+              inputMapping[name] = input.dependency(tokenValue.defaultDependency);
+            } else {
+              inputMapping[name] = input.value(null);
+            }
+          }
+
+          finalDescription.inputMapping = inputMapping;
+          finalDescription.inputDescriptions = description.inputs;
+        }
+
+        if ('outputs' in description) {
+          const finalOutputs = {};
+
+          for (const name of expectedOutputNames) {
+            if (name in outputOptions) {
+              finalOutputs[name] = outputOptions[name];
+            } else {
+              finalOutputs[name] = name;
+            }
+          }
+
+          finalDescription.outputs = finalOutputs;
+        }
+
+        if ('steps' in description) {
+          finalDescription.steps = description.steps;
+        }
+
+        return finalDescription;
+      },
+
+      toResolvedComposition() {
+        const ownDescription = instantiatedTemplate.toDescription();
+
+        const finalDescription = {...ownDescription};
+
+        const aggregate = openAggregate({message: `Errors resolving ${compositionName}`});
+
+        const steps = ownDescription.steps();
+
+        const resolvedSteps =
+          aggregate.map(
+            steps,
+            decorateErrorWithIndex(step =>
+              (step.symbol === templateCompositeFrom.symbol
+                ? compositeFrom(step.toResolvedComposition())
+                : step)),
+            {message: `Errors resolving steps`});
+
+        aggregate.close();
+
+        finalDescription.steps = resolvedSteps;
+
+        return finalDescription;
+      },
+    };
+
+    return instantiatedTemplate;
+  };
+
+  instantiate.inputs = instantiate;
+
+  return instantiate;
+}
+
+templateCompositeFrom.symbol = Symbol();
+
+export const continuationSymbol = Symbol.for('compositeFrom: continuation symbol');
+export const noTransformSymbol = Symbol.for('compositeFrom: no-transform symbol');
+
+export function compositeFrom(description) {
+  const {annotation} = description;
+  const compositionName = getCompositionName(description);
+
+  const debug = fn => {
+    if (compositeFrom.debug === true) {
+      const label =
+        (annotation
+          ? colors.dim(`[composite: ${annotation}]`)
+          : colors.dim(`[composite]`));
+      const result = fn();
+      if (Array.isArray(result)) {
+        console.log(label, ...result.map(value =>
+          (typeof value === 'object'
+            ? inspect(value, {depth: 1, colors: true, compact: true, breakLength: Infinity})
+            : value)));
+      } else {
+        console.log(label, result);
+      }
+    }
+  };
+
+  if (!Array.isArray(description.steps)) {
+    throw new TypeError(
+      `Expected steps to be array, got ${typeAppearance(description.steps)}` +
+      (annotation ? ` (${annotation})` : ''));
+  }
+
+  const composition =
+    description.steps.map(step =>
+      ('toResolvedComposition' in step
+        ? compositeFrom(step.toResolvedComposition())
+        : step));
+
+  const inputMetadata = getStaticInputMetadata(description.inputMapping ?? {});
+
+  function _mapDependenciesToOutputs(providedDependencies) {
+    if (!description.outputs) {
+      return {};
+    }
+
+    if (!providedDependencies) {
+      return {};
+    }
+
+    return (
+      Object.fromEntries(
+        Object.entries(description.outputs)
+          .map(([continuationName, outputName]) => [
+            outputName,
+            (continuationName in providedDependencies
+              ? providedDependencies[continuationName]
+              : providedDependencies[continuationName.replace(/^#/, '')]),
+          ])));
+  }
+
+  // These dependencies were all provided by the composition which this one is
+  // nested inside, so input('name')-shaped tokens are going to be evaluated
+  // in the context of the containing composition.
+  const dependenciesFromInputs =
+    Object.values(description.inputMapping ?? {})
+      .map(token => {
+        const tokenShape = getInputTokenShape(token);
+        const tokenValue = getInputTokenValue(token);
+        switch (tokenShape) {
+          case 'input.dependency':
+            return tokenValue;
+          case 'input':
+          case 'input.updateValue':
+            return token;
+          case 'input.myself':
+            return 'this';
+          default:
+            return null;
+        }
+      })
+      .filter(Boolean);
+
+  const anyInputsUseUpdateValue =
+    dependenciesFromInputs
+      .filter(dependency => isInputToken(dependency))
+      .some(token => getInputTokenShape(token) === 'input.updateValue');
+
+  const inputNames =
+    Object.keys(description.inputMapping ?? {});
+
+  const inputSymbols =
+    inputNames.map(name => input(name));
+
+  const inputsMayBeDynamicValue =
+    stitchArrays({
+      mappingToken: Object.values(description.inputMapping ?? {}),
+      descriptionToken: Object.values(description.inputDescriptions ?? {}),
+    }).map(({mappingToken, descriptionToken}) => {
+        if (getInputTokenShape(descriptionToken) === 'input.staticValue') return false;
+        if (getInputTokenShape(mappingToken) === 'input.value') return false;
+        return true;
+      });
+
+  const inputDescriptions =
+    Object.values(description.inputDescriptions ?? {});
+
+  /*
+  const inputsAcceptNull =
+    Object.values(description.inputDescriptions ?? {})
+      .map(token => {
+        const tokenValue = getInputTokenValue(token);
+        if (!tokenValue) return false;
+        if ('acceptsNull' in tokenValue) return tokenValue.acceptsNull;
+        if ('defaultValue' in tokenValue) return tokenValue.defaultValue === null;
+        return false;
+      });
+  */
+
+  // Update descriptions passed as the value in an input.updateValue() token,
+  // as provided as inputs for this composition.
+  const inputUpdateDescriptions =
+    Object.values(description.inputMapping ?? {})
+      .map(token =>
+        (getInputTokenShape(token) === 'input.updateValue'
+          ? getInputTokenValue(token)
+          : null))
+      .filter(Boolean);
+
+  const base = composition.at(-1);
+  const steps = composition.slice();
+
+  const aggregate = openAggregate({
+    message:
+      `Errors preparing composition` +
+      (annotation ? ` (${annotation})` : ''),
+  });
+
+  const compositionNests = description.compose ?? true;
+
+  // Steps default to exposing if using a shorthand syntax where flags aren't
+  // specified at all.
+  const stepsExpose =
+    steps
+      .map(step =>
+        (step.flags
+          ? step.flags.expose ?? false
+          : true));
+
+  // Steps default to composing if using a shorthand syntax where flags aren't
+  // specified at all - *and* aren't the base (final step), unless the whole
+  // composition is nestable.
+  const stepsCompose =
+    steps
+      .map((step, index, {length}) =>
+        (step.flags
+          ? step.flags.compose ?? false
+          : (index === length - 1
+              ? compositionNests
+              : true)));
+
+  // Steps update if the corresponding flag is explicitly set, if a transform
+  // function is provided, or if the dependencies include an input.updateValue
+  // token.
+  const stepsUpdate =
+    steps
+      .map(step =>
+        (step.flags
+          ? step.flags.update ?? false
+          : !!step.transform ||
+            !!step.dependencies?.some(dependency =>
+                isInputToken(dependency) &&
+                getInputTokenShape(dependency) === 'input.updateValue')));
+
+  // The expose description for a step is just the entire step object, when
+  // using the shorthand syntax where {flags: {expose: true}} is left implied.
+  const stepExposeDescriptions =
+    steps
+      .map((step, index) =>
+        (stepsExpose[index]
+          ? (step.flags
+              ? step.expose ?? null
+              : step)
+          : null));
+
+  // The update description for a step, if present at all, is always set
+  // explicitly. There may be multiple per step - namely that step's own
+  // {update} description, and any descriptions passed as the value in an
+  // input.updateValue({...}) token.
+  const stepUpdateDescriptions =
+    steps
+      .map((step, index) =>
+        (stepsUpdate[index]
+          ? [
+              step.update ?? null,
+              ...(stepExposeDescriptions[index]?.dependencies ?? [])
+                .filter(dependency => isInputToken(dependency))
+                .filter(token => getInputTokenShape(token) === 'input.updateValue')
+                .map(token => getInputTokenValue(token)),
+            ].filter(Boolean)
+          : []));
+
+  // Indicates presence of a {compute} function on the expose description.
+  const stepsCompute =
+    stepExposeDescriptions
+      .map(expose => !!expose?.compute);
+
+  // Indicates presence of a {transform} function on the expose description.
+  const stepsTransform =
+    stepExposeDescriptions
+      .map(expose => !!expose?.transform);
+
+  const dependenciesFromSteps =
+    unique(
+      stepExposeDescriptions
+        .flatMap(expose => expose?.dependencies ?? [])
+        .map(dependency => {
+          if (typeof dependency === 'string')
+            return (dependency.startsWith('#') ? null : dependency);
+
+          const tokenShape = getInputTokenShape(dependency);
+          const tokenValue = getInputTokenValue(dependency);
+          switch (tokenShape) {
+            case 'input.dependency':
+              return (tokenValue.startsWith('#') ? null : tokenValue);
+            case 'input.myself':
+              return 'this';
+            default:
+              return null;
+          }
+        })
+        .filter(Boolean));
+
+  const anyStepsUseUpdateValue =
+    stepExposeDescriptions
+      .some(expose =>
+        (expose?.dependencies
+          ? expose.dependencies.includes(input.updateValue())
+          : false));
+
+  const anyStepsExpose =
+    stepsExpose.includes(true);
+
+  const anyStepsUpdate =
+    stepsUpdate.includes(true);
+
+  const anyStepsCompute =
+    stepsCompute.includes(true);
+
+  const anyStepsTransform =
+    stepsTransform.includes(true);
+
+  const compositionExposes =
+    anyStepsExpose;
+
+  const compositionUpdates =
+    'update' in description ||
+    anyInputsUseUpdateValue ||
+    anyStepsUseUpdateValue ||
+    anyStepsUpdate;
+
+  const stepEntries = stitchArrays({
+    step: steps,
+    stepComposes: stepsCompose,
+    stepComputes: stepsCompute,
+    stepTransforms: stepsTransform,
+  });
+
+  for (let i = 0; i < stepEntries.length; i++) {
+    const {
+      step,
+      stepComposes,
+      stepComputes,
+      stepTransforms,
+    } = stepEntries[i];
+
+    const isBase = i === stepEntries.length - 1;
+    const message =
+      `Errors in step #${i + 1}` +
+      (isBase ? ` (base)` : ``) +
+      (step.annotation ? ` (${step.annotation})` : ``);
+
+    aggregate.nest({message}, ({push}) => {
+      if (isBase && stepComposes !== compositionNests) {
+        return push(new TypeError(
+          (compositionNests
+            ? `Base must compose, this composition is nestable`
+            : `Base must not compose, this composition isn't nestable`)));
+      } else if (!isBase && !stepComposes) {
+        return push(new TypeError(
+          (compositionNests
+            ? `All steps must compose`
+            : `All steps (except base) must compose`)));
+      }
+
+      if (
+        !compositionNests && !compositionUpdates &&
+        stepTransforms && !stepComputes
+      ) {
+        return push(new TypeError(
+          `Steps which only transform can't be used in a composition that doesn't update`));
+      }
+    });
+  }
+
+  if (!compositionNests && !anyStepsCompute && !anyStepsTransform) {
+    aggregate.push(new TypeError(`Expected at least one step to compute or transform`));
+  }
+
+  aggregate.close();
+
+  function _prepareContinuation(callingTransformForThisStep) {
+    const continuationStorage = {
+      returnedWith: null,
+      providedDependencies: undefined,
+      providedValue: undefined,
+    };
+
+    const continuation =
+      (callingTransformForThisStep
+        ? (providedValue, providedDependencies = null) => {
+            continuationStorage.returnedWith = 'continuation';
+            continuationStorage.providedDependencies = providedDependencies;
+            continuationStorage.providedValue = providedValue;
+            return continuationSymbol;
+          }
+        : (providedDependencies = null) => {
+            continuationStorage.returnedWith = 'continuation';
+            continuationStorage.providedDependencies = providedDependencies;
+            return continuationSymbol;
+          });
+
+    continuation.exit = (providedValue) => {
+      continuationStorage.returnedWith = 'exit';
+      continuationStorage.providedValue = providedValue;
+      return continuationSymbol;
+    };
+
+    if (compositionNests) {
+      const makeRaiseLike = returnWith =>
+        (callingTransformForThisStep
+          ? (providedValue, providedDependencies = null) => {
+              continuationStorage.returnedWith = returnWith;
+              continuationStorage.providedDependencies = providedDependencies;
+              continuationStorage.providedValue = providedValue;
+              return continuationSymbol;
+            }
+          : (providedDependencies = null) => {
+              continuationStorage.returnedWith = returnWith;
+              continuationStorage.providedDependencies = providedDependencies;
+              return continuationSymbol;
+            });
+
+      continuation.raiseOutput = makeRaiseLike('raiseOutput');
+      continuation.raiseOutputAbove = makeRaiseLike('raiseOutputAbove');
+    }
+
+    return {continuation, continuationStorage};
+  }
+
+  function _computeOrTransform(initialValue, continuationIfApplicable, initialDependencies) {
+    const expectingTransform = initialValue !== noTransformSymbol;
+
+    let valueSoFar =
+      (expectingTransform
+        ? initialValue
+        : undefined);
+
+    const availableDependencies = {...initialDependencies};
+
+    const inputValues =
+      Object.values(description.inputMapping ?? {})
+        .map(token => {
+          const tokenShape = getInputTokenShape(token);
+          const tokenValue = getInputTokenValue(token);
+          switch (tokenShape) {
+            case 'input.dependency':
+              return initialDependencies[tokenValue];
+            case 'input.value':
+              return tokenValue;
+            case 'input.updateValue':
+              if (!expectingTransform)
+                throw new Error(`Unexpected input.updateValue() accessed on non-transform call`);
+              return valueSoFar;
+            case 'input.myself':
+              return initialDependencies['this'];
+            case 'input':
+              return initialDependencies[token];
+            default:
+              throw new TypeError(`Unexpected input shape ${tokenShape}`);
+          }
+        });
+
+    withAggregate({message: `Errors in input values provided to ${compositionName}`}, ({push}) => {
+      for (const {dynamic, name, value, description} of stitchArrays({
+        dynamic: inputsMayBeDynamicValue,
+        name: inputNames,
+        value: inputValues,
+        description: inputDescriptions,
+      })) {
+        if (!dynamic) continue;
+        try {
+          validateInputValue(value, description);
+        } catch (error) {
+          error.message = `${name}: ${error.message}`;
+          push(error);
+        }
+      }
+    });
+
+    if (expectingTransform) {
+      debug(() => [colors.bright(`begin composition - transforming from:`), initialValue]);
+    } else {
+      debug(() => colors.bright(`begin composition - not transforming`));
+    }
+
+    for (let i = 0; i < steps.length; i++) {
+      const step = steps[i];
+      const isBase = i === steps.length - 1;
+
+      debug(() => [
+        `step #${i+1}` +
+        (isBase
+          ? ` (base):`
+          : ` of ${steps.length}:`),
+        step]);
+
+      const expose =
+        (step.flags
+          ? step.expose
+          : step);
+
+      if (!expose) {
+        if (!isBase) {
+          debug(() => `step #${i+1} - no expose description, nothing to do for this step`);
+          continue;
+        }
+
+        if (expectingTransform) {
+          debug(() => `step #${i+1} (base) - no expose description, returning so-far update value:`, valueSoFar);
+          if (continuationIfApplicable) {
+            debug(() => colors.bright(`end composition - raise (inferred - composing)`));
+            return continuationIfApplicable(valueSoFar);
+          } else {
+            debug(() => colors.bright(`end composition - exit (inferred - not composing)`));
+            return valueSoFar;
+          }
+        } else {
+          debug(() => `step #${i+1} (base) - no expose description, nothing to continue with`);
+          if (continuationIfApplicable) {
+            debug(() => colors.bright(`end composition - raise (inferred - composing)`));
+            return continuationIfApplicable();
+          } else {
+            debug(() => colors.bright(`end composition - exit (inferred - not composing)`));
+            return null;
+          }
+        }
+      }
+
+      const callingTransformForThisStep =
+        expectingTransform && expose.transform;
+
+      let continuationStorage;
+
+      const inputDictionary =
+        Object.fromEntries(
+          stitchArrays({symbol: inputSymbols, value: inputValues})
+            .map(({symbol, value}) => [symbol, value]));
+
+      const filterableDependencies = {
+        ...availableDependencies,
+        ...inputMetadata,
+        ...inputDictionary,
+        ...
+          (expectingTransform
+            ? {[input.updateValue()]: valueSoFar}
+            : {}),
+        [input.myself()]: initialDependencies?.['this'] ?? null,
+      };
+
+      const selectDependencies =
+        (expose.dependencies ?? []).map(dependency => {
+          if (!isInputToken(dependency)) return dependency;
+          const tokenShape = getInputTokenShape(dependency);
+          const tokenValue = getInputTokenValue(dependency);
+          switch (tokenShape) {
+            case 'input':
+            case 'input.staticDependency':
+            case 'input.staticValue':
+              return dependency;
+            case 'input.myself':
+              return input.myself();
+            case 'input.dependency':
+              return tokenValue;
+            case 'input.updateValue':
+              return input.updateValue();
+            default:
+              throw new Error(`Unexpected token ${tokenShape} as dependency`);
+          }
+        })
+
+      const filteredDependencies =
+        filterProperties(filterableDependencies, selectDependencies);
+
+      debug(() => [
+        `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`,
+        `with dependencies:`, filteredDependencies,
+        `selecting:`, selectDependencies,
+        `from available:`, filterableDependencies,
+        ...callingTransformForThisStep ? [`from value:`, valueSoFar] : []]);
+
+      let result;
+
+      const getExpectedEvaluation = () =>
+        (callingTransformForThisStep
+          ? (filteredDependencies
+              ? ['transform', valueSoFar, continuationSymbol, filteredDependencies]
+              : ['transform', valueSoFar, continuationSymbol])
+          : (filteredDependencies
+              ? ['compute', continuationSymbol, filteredDependencies]
+              : ['compute', continuationSymbol]));
+
+      const naturalEvaluate = () => {
+        const [name, ...argsLayout] = getExpectedEvaluation();
+
+        let args;
+
+        if (isBase && !compositionNests) {
+          args =
+            argsLayout.filter(arg => arg !== continuationSymbol);
+        } else {
+          let continuation;
+
+          ({continuation, continuationStorage} =
+            _prepareContinuation(callingTransformForThisStep));
+
+          args =
+            argsLayout.map(arg =>
+              (arg === continuationSymbol
+                ? continuation
+                : arg));
+        }
+
+        return expose[name](...args);
+      }
+
+      switch (step.cache) {
+        // Warning! Highly WIP!
+        case 'aggressive': {
+          const hrnow = () => {
+            const hrTime = process.hrtime();
+            return hrTime[0] * 1000000000 + hrTime[1];
+          };
+
+          const [name, ...args] = getExpectedEvaluation();
+
+          let cache = globalCompositeCache[step.annotation];
+          if (!cache) {
+            cache = globalCompositeCache[step.annotation] = {
+              transform: new TupleMap(),
+              compute: new TupleMap(),
+              times: {
+                read: [],
+                evaluate: [],
+              },
+            };
+          }
+
+          const tuplefied = args
+            .flatMap(arg => [
+              Symbol.for('compositeFrom: tuplefied arg divider'),
+              ...(typeof arg !== 'object' || Array.isArray(arg)
+                ? [arg]
+                : Object.entries(arg).flat()),
+            ]);
+
+          const readTime = hrnow();
+          const cacheContents = cache[name].get(tuplefied);
+          cache.times.read.push(hrnow() - readTime);
+
+          if (cacheContents) {
+            ({result, continuationStorage} = cacheContents);
+          } else {
+            const evaluateTime = hrnow();
+            result = naturalEvaluate();
+            cache.times.evaluate.push(hrnow() - evaluateTime);
+            cache[name].set(tuplefied, {result, continuationStorage});
+          }
+
+          break;
+        }
+
+        default: {
+          result = naturalEvaluate();
+          break;
+        }
+      }
+
+      if (result !== continuationSymbol) {
+        debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]);
+
+        if (compositionNests) {
+          throw new TypeError(`Inferred early-exit is disallowed in nested compositions`);
+        }
+
+        debug(() => colors.bright(`end composition - exit (inferred)`));
+
+        return result;
+      }
+
+      const {returnedWith} = continuationStorage;
+
+      if (returnedWith === 'exit') {
+        const {providedValue} = continuationStorage;
+
+        debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]);
+        debug(() => colors.bright(`end composition - exit (explicit)`));
+
+        if (compositionNests) {
+          return continuationIfApplicable.exit(providedValue);
+        } else {
+          return providedValue;
+        }
+      }
+
+      const {providedValue, providedDependencies} = continuationStorage;
+
+      const continuationArgs = [];
+      if (expectingTransform) {
+        continuationArgs.push(
+          (callingTransformForThisStep
+            ? providedValue ?? null
+            : valueSoFar ?? null));
+      }
+
+      debug(() => {
+        const base = `step #${i+1} - result: ` + returnedWith;
+        const parts = [];
+
+        if (callingTransformForThisStep) {
+          parts.push('value:', providedValue);
+        }
+
+        if (providedDependencies !== null) {
+          parts.push(`deps:`, providedDependencies);
+        } else {
+          parts.push(`(no deps)`);
+        }
+
+        if (empty(parts)) {
+          return base;
+        } else {
+          return [base + ' ->', ...parts];
+        }
+      });
+
+      switch (returnedWith) {
+        case 'raiseOutput':
+          debug(() =>
+            (isBase
+              ? colors.bright(`end composition - raiseOutput (base: explicit)`)
+              : colors.bright(`end composition - raiseOutput`)));
+          continuationArgs.push(_mapDependenciesToOutputs(providedDependencies));
+          return continuationIfApplicable(...continuationArgs);
+
+        case 'raiseOutputAbove':
+          debug(() => colors.bright(`end composition - raiseOutputAbove`));
+          continuationArgs.push(_mapDependenciesToOutputs(providedDependencies));
+          return continuationIfApplicable.raiseOutput(...continuationArgs);
+
+        case 'continuation':
+          if (isBase) {
+            debug(() => colors.bright(`end composition - raiseOutput (inferred)`));
+            continuationArgs.push(_mapDependenciesToOutputs(providedDependencies));
+            return continuationIfApplicable(...continuationArgs);
+          } else {
+            Object.assign(availableDependencies, providedDependencies);
+            if (callingTransformForThisStep && providedValue !== null) {
+              valueSoFar = providedValue;
+            }
+            break;
+          }
+      }
+    }
+  }
+
+  const constructedDescriptor = {};
+
+  if (annotation) {
+    constructedDescriptor.annotation = annotation;
+  }
+
+  constructedDescriptor.flags = {
+    update: compositionUpdates,
+    expose: compositionExposes,
+    compose: compositionNests,
+  };
+
+  if (compositionUpdates) {
+    // TODO: This is a dumb assign statement, and it could probably do more
+    // interesting things, like combining validation functions.
+    constructedDescriptor.update =
+      Object.assign(
+        {...description.update ?? {}},
+        ...inputUpdateDescriptions,
+        ...stepUpdateDescriptions.flat());
+  }
+
+  if (compositionExposes) {
+    const expose = constructedDescriptor.expose = {};
+
+    expose.dependencies =
+      unique([
+        ...dependenciesFromInputs,
+        ...dependenciesFromSteps,
+      ]);
+
+    const _wrapper = (...args) => {
+      try {
+        return _computeOrTransform(...args);
+      } catch (thrownError) {
+        const error = new Error(
+          `Error computing composition` +
+          (annotation ? ` ${annotation}` : ''));
+        error.cause = thrownError;
+        throw error;
+      }
+    };
+
+    if (compositionNests) {
+      if (compositionUpdates) {
+        expose.transform = (value, continuation, dependencies) =>
+          _wrapper(value, continuation, dependencies);
+      }
+
+      if (anyStepsCompute && !anyStepsUseUpdateValue && !anyInputsUseUpdateValue) {
+        expose.compute = (continuation, dependencies) =>
+          _wrapper(noTransformSymbol, continuation, dependencies);
+      }
+
+      if (base.cacheComposition) {
+        expose.cache = base.cacheComposition;
+      }
+    } else if (compositionUpdates) {
+      expose.transform = (value, dependencies) =>
+        _wrapper(value, null, dependencies);
+    } else {
+      expose.compute = (dependencies) =>
+        _wrapper(noTransformSymbol, null, dependencies);
+    }
+  }
+
+  return constructedDescriptor;
+}
+
+export function displayCompositeCacheAnalysis() {
+  const showTimes = (cache, key) => {
+    const times = cache.times[key].slice().sort();
+
+    const all = times;
+    const worst10pc = times.slice(-times.length / 10);
+    const best10pc = times.slice(0, times.length / 10);
+    const middle50pc = times.slice(times.length / 4, -times.length / 4);
+    const middle80pc = times.slice(times.length / 10, -times.length / 10);
+
+    const fmt = val => `${(val / 1000).toFixed(2)}ms`.padStart(9);
+    const avg = times => times.reduce((a, b) => a + b, 0) / times.length;
+
+    const left = ` - ${key}: `;
+    const indn = ' '.repeat(left.length);
+    console.log(left + `${fmt(avg(all))} (all ${all.length})`);
+    console.log(indn + `${fmt(avg(worst10pc))} (worst 10%)`);
+    console.log(indn + `${fmt(avg(best10pc))} (best 10%)`);
+    console.log(indn + `${fmt(avg(middle80pc))} (middle 80%)`);
+    console.log(indn + `${fmt(avg(middle50pc))} (middle 50%)`);
+  };
+
+  for (const [annotation, cache] of Object.entries(globalCompositeCache)) {
+    console.log(`Cached ${annotation}:`);
+    showTimes(cache, 'evaluate');
+    showTimes(cache, 'read');
+  }
+}
+
+// Evaluates a function with composite debugging enabled, turns debugging
+// off again, and returns the result of the function. This is mostly syntax
+// sugar, but also helps avoid unit tests avoid accidentally printing debug
+// info for a bunch of unrelated composites (due to property enumeration
+// when displaying an unexpected result). Use as so:
+//
+//   Without debugging:
+//     t.same(thing.someProp, value)
+//
+//   With debugging:
+//     t.same(debugComposite(() => thing.someProp), value)
+//
+export function debugComposite(fn) {
+  compositeFrom.debug = true;
+  const value = fn();
+  compositeFrom.debug = false;
+  return value;
+}
diff --git a/src/data/things/flash.js b/src/data/things/flash.js
index 6eb5234..e2afcef 100644
--- a/src/data/things/flash.js
+++ b/src/data/things/flash.js
@@ -1,25 +1,42 @@
+import {input} from '#composite';
 import find from '#find';
 
+import {
+  isColor,
+  isDirectory,
+  isNumber,
+  isString,
+  oneOf,
+} from '#validators';
+
+import {exposeDependency, exposeUpdateValueOrContinue}
+  from '#composite/control-flow';
+import {withPropertyFromObject} from '#composite/data';
+
+import {
+  color,
+  contributionList,
+  directory,
+  fileExtension,
+  name,
+  referenceList,
+  simpleDate,
+  simpleString,
+  urls,
+  wikiData,
+} from '#composite/wiki-properties';
+
+import {withFlashAct} from '#composite/things/flash';
+
 import Thing from './thing.js';
 
 export class Flash extends Thing {
   static [Thing.referenceType] = 'flash';
 
-  static [Thing.getPropertyDescriptors] = ({
-    Artist,
-    Track,
-    FlashAct,
-
-    validators: {
-      isDirectory,
-      isNumber,
-      isString,
-      oneOf,
-    },
-  }) => ({
+  static [Thing.getPropertyDescriptors] = ({Artist, Track, FlashAct}) => ({
     // Update & expose
 
-    name: Thing.common.name('Unnamed Flash'),
+    name: name('Unnamed Flash'),
 
     directory: {
       flags: {update: true, expose: true},
@@ -47,51 +64,51 @@ export class Flash extends Thing {
       },
     },
 
-    date: Thing.common.simpleDate(),
-
-    coverArtFileExtension: Thing.common.fileExtension('jpg'),
+    color: [
+      exposeUpdateValueOrContinue({
+        validate: input.value(isColor),
+      }),
 
-    contributorContribsByRef: Thing.common.contribsByRef(),
+      withFlashAct(),
 
-    featuredTracksByRef: Thing.common.referenceList(Track),
+      withPropertyFromObject({
+        object: '#flashAct',
+        property: input.value('color'),
+      }),
 
-    urls: Thing.common.urls(),
+      exposeDependency({dependency: '#flashAct.color'}),
+    ],
 
-    // Update only
+    date: simpleDate(),
 
-    artistData: Thing.common.wikiData(Artist),
-    trackData: Thing.common.wikiData(Track),
-    flashActData: Thing.common.wikiData(FlashAct),
+    coverArtFileExtension: fileExtension('jpg'),
 
-    // Expose only
+    contributorContribs: contributionList(),
 
-    contributorContribs: Thing.common.dynamicContribs('contributorContribsByRef'),
+    featuredTracks: referenceList({
+      class: input.value(Track),
+      find: input.value(find.track),
+      data: 'trackData',
+    }),
 
-    featuredTracks: Thing.common.dynamicThingsFromReferenceList(
-      'featuredTracksByRef',
-      'trackData',
-      find.track
-    ),
+    urls: urls(),
 
-    act: {
-      flags: {expose: true},
+    // Update only
 
-      expose: {
-        dependencies: ['flashActData'],
+    artistData: wikiData(Artist),
+    trackData: wikiData(Track),
+    flashActData: wikiData(FlashAct),
 
-        compute: ({flashActData, [Flash.instance]: flash}) =>
-          flashActData.find((act) => act.flashes.includes(flash)) ?? null,
-      },
-    },
+    // Expose only
 
-    color: {
+    act: {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['flashActData'],
+        dependencies: ['this', 'flashActData'],
 
-        compute: ({flashActData, [Flash.instance]: flash}) =>
-          flashActData.find((act) => act.flashes.includes(flash))?.color ?? null,
+        compute: ({this: flash, flashActData}) =>
+          flashActData.find((act) => act.flashes.includes(flash)) ?? null,
       },
     },
   });
@@ -111,17 +128,18 @@ export class Flash extends Thing {
 }
 
 export class FlashAct extends Thing {
-  static [Thing.getPropertyDescriptors] = ({
-    validators: {
-      isColor,
-    },
-  }) => ({
+  static [Thing.referenceType] = 'flash-act';
+  static [Thing.friendlyName] = `Flash Act`;
+
+  static [Thing.getPropertyDescriptors] = () => ({
     // Update & expose
 
-    name: Thing.common.name('Unnamed Flash Act'),
-    color: Thing.common.color(),
-    anchor: Thing.common.simpleString(),
-    jump: Thing.common.simpleString(),
+    name: name('Unnamed Flash Act'),
+    directory: directory(),
+    color: color(),
+    listTerminology: simpleString(),
+
+    jump: simpleString(),
 
     jumpColor: {
       flags: {update: true, expose: true},
@@ -133,18 +151,14 @@ export class FlashAct extends Thing {
       }
     },
 
-    flashesByRef: Thing.common.referenceList(Flash),
+    flashes: referenceList({
+      class: input.value(Flash),
+      find: input.value(find.flash),
+      data: 'flashData',
+    }),
 
     // Update only
 
-    flashData: Thing.common.wikiData(Flash),
-
-    // Expose only
-
-    flashes: Thing.common.dynamicThingsFromReferenceList(
-      'flashesByRef',
-      'flashData',
-      find.flash
-    ),
+    flashData: wikiData(Flash),
   })
 }
diff --git a/src/data/things/group.js b/src/data/things/group.js
index ba339b3..8764a9d 100644
--- a/src/data/things/group.js
+++ b/src/data/things/group.js
@@ -1,33 +1,44 @@
+import {input} from '#composite';
 import find from '#find';
 
+import {
+  color,
+  directory,
+  name,
+  referenceList,
+  simpleString,
+  urls,
+  wikiData,
+} from '#composite/wiki-properties';
+
 import Thing from './thing.js';
 
 export class Group extends Thing {
   static [Thing.referenceType] = 'group';
 
-  static [Thing.getPropertyDescriptors] = ({
-    Album,
-  }) => ({
+  static [Thing.getPropertyDescriptors] = ({Album}) => ({
     // Update & expose
 
-    name: Thing.common.name('Unnamed Group'),
-    directory: Thing.common.directory(),
+    name: name('Unnamed Group'),
+    directory: directory(),
 
-    description: Thing.common.simpleString(),
+    description: simpleString(),
 
-    urls: Thing.common.urls(),
+    urls: urls(),
 
-    featuredAlbumsByRef: Thing.common.referenceList(Album),
+    featuredAlbums: referenceList({
+      class: input.value(Album),
+      find: input.value(find.album),
+      data: 'albumData',
+    }),
 
     // Update only
 
-    albumData: Thing.common.wikiData(Album),
-    groupCategoryData: Thing.common.wikiData(GroupCategory),
+    albumData: wikiData(Album),
+    groupCategoryData: wikiData(GroupCategory),
 
     // Expose only
 
-    featuredAlbums: Thing.common.dynamicThingsFromReferenceList('featuredAlbumsByRef', 'albumData', find.album),
-
     descriptionShort: {
       flags: {expose: true},
 
@@ -41,8 +52,8 @@ export class Group extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['albumData'],
-        compute: ({albumData, [Group.instance]: group}) =>
+        dependencies: ['this', 'albumData'],
+        compute: ({this: group, albumData}) =>
           albumData?.filter((album) => album.groups.includes(group)) ?? [],
       },
     },
@@ -51,9 +62,8 @@ export class Group extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['groupCategoryData'],
-
-        compute: ({groupCategoryData, [Group.instance]: group}) =>
+        dependencies: ['this', 'groupCategoryData'],
+        compute: ({this: group, groupCategoryData}) =>
           groupCategoryData.find((category) => category.groups.includes(group))
             ?.color,
       },
@@ -63,8 +73,8 @@ export class Group extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['groupCategoryData'],
-        compute: ({groupCategoryData, [Group.instance]: group}) =>
+        dependencies: ['this', 'groupCategoryData'],
+        compute: ({this: group, groupCategoryData}) =>
           groupCategoryData.find((category) => category.groups.includes(group)) ??
           null,
       },
@@ -73,26 +83,22 @@ export class Group extends Thing {
 }
 
 export class GroupCategory extends Thing {
-  static [Thing.getPropertyDescriptors] = ({
-    Group,
-  }) => ({
+  static [Thing.friendlyName] = `Group Category`;
+
+  static [Thing.getPropertyDescriptors] = ({Group}) => ({
     // Update & expose
 
-    name: Thing.common.name('Unnamed Group Category'),
-    color: Thing.common.color(),
+    name: name('Unnamed Group Category'),
+    color: color(),
 
-    groupsByRef: Thing.common.referenceList(Group),
+    groups: referenceList({
+      class: input.value(Group),
+      find: input.value(find.group),
+      data: 'groupData',
+    }),
 
     // Update only
 
-    groupData: Thing.common.wikiData(Group),
-
-    // Expose only
-
-    groups: Thing.common.dynamicThingsFromReferenceList(
-      'groupsByRef',
-      'groupData',
-      find.group
-    ),
+    groupData: wikiData(Group),
   });
 }
diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js
index ec9e955..bfa971c 100644
--- a/src/data/things/homepage-layout.js
+++ b/src/data/things/homepage-layout.js
@@ -1,20 +1,37 @@
+import {input} from '#composite';
 import find from '#find';
 
+import {
+  is,
+  isCountingNumber,
+  isString,
+  isStringNonEmpty,
+  oneOf,
+  validateArrayItems,
+  validateInstanceOf,
+  validateReference,
+} from '#validators';
+
+import {exposeDependency} from '#composite/control-flow';
+import {withResolvedReference} from '#composite/wiki-data';
+
+import {
+  color,
+  name,
+  referenceList,
+  simpleString,
+  wikiData,
+} from '#composite/wiki-properties';
+
 import Thing from './thing.js';
 
 export class HomepageLayout extends Thing {
-  static [Thing.getPropertyDescriptors] = ({
-    HomepageLayoutRow,
+  static [Thing.friendlyName] = `Homepage Layout`;
 
-    validators: {
-      isStringNonEmpty,
-      validateArrayItems,
-      validateInstanceOf,
-    },
-  }) => ({
+  static [Thing.getPropertyDescriptors] = ({HomepageLayoutRow}) => ({
     // Update & expose
 
-    sidebarContent: Thing.common.simpleString(),
+    sidebarContent: simpleString(),
 
     navbarLinks: {
       flags: {update: true, expose: true},
@@ -32,13 +49,12 @@ export class HomepageLayout extends Thing {
 }
 
 export class HomepageLayoutRow extends Thing {
-  static [Thing.getPropertyDescriptors] = ({
-    Album,
-    Group,
-  }) => ({
+  static [Thing.friendlyName] = `Homepage Row`;
+
+  static [Thing.getPropertyDescriptors] = ({Album, Group}) => ({
     // Update & expose
 
-    name: Thing.common.name('Unnamed Homepage Row'),
+    name: name('Unnamed Homepage Row'),
 
     type: {
       flags: {update: true, expose: true},
@@ -50,30 +66,22 @@ export class HomepageLayoutRow extends Thing {
       },
     },
 
-    color: Thing.common.color(),
+    color: color(),
 
     // Update only
 
     // These aren't necessarily used by every HomepageLayoutRow subclass, but
     // for convenience of providing this data, every row accepts all wiki data
     // arrays depended upon by any subclass's behavior.
-    albumData: Thing.common.wikiData(Album),
-    groupData: Thing.common.wikiData(Group),
+    albumData: wikiData(Album),
+    groupData: wikiData(Group),
   });
 }
 
 export class HomepageLayoutAlbumsRow extends HomepageLayoutRow {
-  static [Thing.getPropertyDescriptors] = (opts, {
-    Album,
-    Group,
-
-    validators: {
-      is,
-      isCountingNumber,
-      isString,
-      validateArrayItems,
-    },
-  } = opts) => ({
+  static [Thing.friendlyName] = `Homepage Albums Row`;
+
+  static [Thing.getPropertyDescriptors] = (opts, {Album, Group} = opts) => ({
     ...HomepageLayoutRow[Thing.getPropertyDescriptors](opts),
 
     // Update & expose
@@ -104,8 +112,39 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow {
       },
     },
 
-    sourceGroupByRef: Thing.common.singleReference(Group),
-    sourceAlbumsByRef: Thing.common.referenceList(Album),
+    sourceGroup: [
+      {
+        flags: {expose: true, update: true, compose: true},
+
+        update: {
+          validate:
+            oneOf(
+              is('new-releases', 'new-additions'),
+              validateReference(Group[Thing.referenceType])),
+        },
+
+        expose: {
+          transform: (value, continuation) =>
+            (value === 'new-releases' || value === 'new-additions'
+              ? value
+              : continuation(value)),
+        },
+      },
+
+      withResolvedReference({
+        ref: input.updateValue(),
+        data: 'groupData',
+        find: input.value(find.group),
+      }),
+
+      exposeDependency({dependency: '#resolvedReference'}),
+    ],
+
+    sourceAlbums: referenceList({
+      class: input.value(Album),
+      find: input.value(find.album),
+      data: 'albumData',
+    }),
 
     countAlbumsFromGroup: {
       flags: {update: true, expose: true},
@@ -116,19 +155,5 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow {
       flags: {update: true, expose: true},
       update: {validate: validateArrayItems(isString)},
     },
-
-    // Expose only
-
-    sourceGroup: Thing.common.dynamicThingFromSingleReference(
-      'sourceGroupByRef',
-      'groupData',
-      find.group
-    ),
-
-    sourceAlbums: Thing.common.dynamicThingsFromReferenceList(
-      'sourceAlbumsByRef',
-      'albumData',
-      find.album
-    ),
   });
 }
diff --git a/src/data/things/index.js b/src/data/things/index.js
index 591cdc3..4ea1f00 100644
--- a/src/data/things/index.js
+++ b/src/data/things/index.js
@@ -2,9 +2,9 @@ import * as path from 'node:path';
 import {fileURLToPath} from 'node:url';
 
 import {logError} from '#cli';
+import {compositeFrom} from '#composite';
 import * as serialize from '#serialize';
 import {openAggregate, showAggregate} from '#sugar';
-import * as validators from '#validators';
 
 import Thing from './thing.js';
 
@@ -21,7 +21,11 @@ import * as trackClasses from './track.js';
 import * as wikiInfoClasses from './wiki-info.js';
 
 export {default as Thing} from './thing.js';
-export {default as CacheableObject} from './cacheable-object.js';
+
+export {
+  default as CacheableObject,
+  CacheableObjectPropertyValueError,
+} from './cacheable-object.js';
 
 const allClassLists = {
   'album.js': albumClasses,
@@ -82,6 +86,8 @@ function errorDuplicateClassNames() {
 function flattenClassLists() {
   for (const classes of Object.values(allClassLists)) {
     for (const [name, constructor] of Object.entries(classes)) {
+      if (typeof constructor !== 'function') continue;
+      if (!(constructor.prototype instanceof Thing)) continue;
       allClasses[name] = constructor;
     }
   }
@@ -119,7 +125,7 @@ function descriptorAggregateHelper({
 }
 
 function evaluatePropertyDescriptors() {
-  const opts = {...allClasses, validators};
+  const opts = {...allClasses};
 
   return descriptorAggregateHelper({
     message: `Errors evaluating Thing class property descriptors`,
@@ -129,8 +135,21 @@ function evaluatePropertyDescriptors() {
         throw new Error(`Missing [Thing.getPropertyDescriptors] function`);
       }
 
-      constructor.propertyDescriptors =
-        constructor[Thing.getPropertyDescriptors](opts);
+      const results = constructor[Thing.getPropertyDescriptors](opts);
+
+      for (const [key, value] of Object.entries(results)) {
+        if (Array.isArray(value)) {
+          results[key] = compositeFrom({
+            annotation: `${constructor.name}.${key}`,
+            compose: false,
+            steps: value,
+          });
+        } else if (value.toResolvedComposition) {
+          results[key] = compositeFrom(value.toResolvedComposition());
+        }
+      }
+
+      constructor.propertyDescriptors = results;
     },
 
     showFailedClasses(failedClasses) {
diff --git a/src/data/things/language.js b/src/data/things/language.js
index 7755c50..646eb6d 100644
--- a/src/data/things/language.js
+++ b/src/data/things/language.js
@@ -1,11 +1,17 @@
+import {Tag} from '#html';
+import {isLanguageCode} from '#validators';
+
+import {
+  externalFunction,
+  flag,
+  simpleString,
+} from '#composite/wiki-properties';
+
+import CacheableObject from './cacheable-object.js';
 import Thing from './thing.js';
 
 export class Language extends Thing {
-  static [Thing.getPropertyDescriptors] = ({
-    validators: {
-      isLanguageCode,
-    },
-  }) => ({
+  static [Thing.getPropertyDescriptors] = () => ({
     // Update & expose
 
     // General language code. This is used to identify the language distinctly
@@ -18,7 +24,7 @@ export class Language extends Thing {
 
     // Human-readable name. This should be the language's own native name, not
     // localized to any other language.
-    name: Thing.common.simpleString(),
+    name: simpleString(),
 
     // Language code specific to JavaScript's Internationalization (Intl) API.
     // Usually this will be the same as the language's general code, but it
@@ -40,7 +46,7 @@ export class Language extends Thing {
     // with languages that are currently in development and not ready for
     // formal release, or which are just kept hidden as "experimental zones"
     // for wiki development or content testing.
-    hidden: Thing.common.flag(false),
+    hidden: flag(false),
 
     // Mapping of translation keys to values (strings). Generally, don't
     // access this object directly - use methods instead.
@@ -68,7 +74,7 @@ export class Language extends Thing {
 
     // Update only
 
-    escapeHTML: Thing.common.externalFunction(),
+    escapeHTML: externalFunction(),
 
     // Expose only
 
@@ -95,6 +101,7 @@ export class Language extends Thing {
       },
     },
 
+    // TODO: This currently isn't used. Is it still needed?
     strings_htmlEscaped: {
       flags: {expose: true},
       expose: {
@@ -124,8 +131,8 @@ export class Language extends Thing {
     };
   }
 
-  $(key, args = {}) {
-    return this.formatString(key, args);
+  $(...args) {
+    return this.formatString(...args);
   }
 
   assertIntlAvailable(property) {
@@ -139,20 +146,22 @@ export class Language extends Thing {
     return this.intl_pluralCardinal.select(value);
   }
 
-  formatString(key, args = {}) {
-    if (this.strings && !this.strings_htmlEscaped) {
-      throw new Error(`HTML-escaped strings unavailable - please ensure escapeHTML function is provided`);
-    }
+  formatString(...args) {
+    const hasOptions =
+      typeof args.at(-1) === 'object' &&
+      args.at(-1) !== null;
 
-    return this.formatStringHelper(this.strings_htmlEscaped, key, args);
-  }
+    const key =
+      (hasOptions ? args.slice(0, -1) : args)
+        .filter(Boolean)
+        .join('.');
 
-  formatStringNoHTMLEscape(key, args = {}) {
-    return this.formatStringHelper(this.strings, key, args);
-  }
+    const options =
+      (hasOptions
+        ? args.at(-1)
+        : null);
 
-  formatStringHelper(strings, key, args = {}) {
-    if (!strings) {
+    if (!this.strings) {
       throw new Error(`Strings unavailable`);
     }
 
@@ -160,30 +169,94 @@ export class Language extends Thing {
       throw new Error(`Invalid key ${key} accessed`);
     }
 
-    const template = strings[key];
-
-    // Convert the keys on the args dict from camelCase to CONSTANT_CASE.
-    // (This isn't an OUTRAGEOUSLY versatile algorithm for doing that, 8ut
-    // like, who cares, dude?) Also, this is an array, 8ecause it's handy
-    // for the iterating we're a8out to do.
-    const processedArgs = Object.entries(args).map(([k, v]) => [
-      k.replace(/[A-Z]/g, '_$&').toUpperCase(),
-      v,
-    ]);
-
-    // Replacement time! Woot. Reduce comes in handy here!
-    const output = processedArgs.reduce(
-      (x, [k, v]) => x.replaceAll(`{${k}}`, v),
-      template
-    );
+    const template = this.strings[key];
+
+    let output;
+
+    if (hasOptions) {
+      // Convert the keys on the options dict from camelCase to CONSTANT_CASE.
+      // (This isn't an OUTRAGEOUSLY versatile algorithm for doing that, 8ut
+      // like, who cares, dude?) Also, this is an array, 8ecause it's handy
+      // for the iterating we're a8out to do. Also strip HTML from arguments
+      // that are literal strings - real HTML content should always be proper
+      // HTML objects (see html.js).
+      const processedOptions =
+        Object.entries(options).map(([k, v]) => [
+          k.replace(/[A-Z]/g, '_$&').toUpperCase(),
+          this.#sanitizeStringArg(v),
+        ]);
+
+      // Replacement time! Woot. Reduce comes in handy here!
+      output =
+        processedOptions.reduce(
+          (x, [k, v]) => x.replaceAll(`{${k}}`, v),
+          template);
+    } else {
+      // Without any options provided, just use the template as-is. This will
+      // still error if the template expected arguments, and otherwise will be
+      // the right value.
+      output = template;
+    }
 
     // Post-processing: if any expected arguments *weren't* replaced, that
     // is almost definitely an error.
-    if (output.match(/\{[A-Z_]+\}/)) {
+    if (output.match(/\{[A-Z][A-Z0-9_]*\}/)) {
       throw new Error(`Args in ${key} were missing - output: ${output}`);
     }
 
-    return output;
+    // Last caveat: Wrap the output in an HTML tag so that it doesn't get
+    // treated as unsanitized HTML if *it* gets passed as an argument to
+    // *another* formatString call.
+    return this.#wrapSanitized(output);
+  }
+
+  // Escapes HTML special characters so they're displayed as-are instead of
+  // treated by the browser as a tag. This does *not* have an effect on actual
+  // html.Tag objects, which are treated as sanitized by default (so that they
+  // can be nested inside strings at all).
+  #sanitizeStringArg(arg) {
+    const escapeHTML = CacheableObject.getUpdateValue(this, 'escapeHTML');
+
+    if (!escapeHTML) {
+      throw new Error(`escapeHTML unavailable`);
+    }
+
+    if (typeof arg !== 'string') {
+      return arg.toString();
+    }
+
+    return escapeHTML(arg);
+  }
+
+  // Wraps the output of a formatting function in a no-name-nor-attributes
+  // HTML tag, which will indicate to other calls to formatString that this
+  // content is a string *that may contain HTML* and doesn't need to
+  // sanitized any further. It'll still .toString() to just the string
+  // contents, if needed.
+  #wrapSanitized(output) {
+    return new Tag(null, null, output);
+  }
+
+  // Similar to the above internal methods, but this one is public.
+  // It should be used when embedding content that may not have previously
+  // been sanitized directly into an HTML tag or template's contents.
+  // The templating engine usually handles this on its own, as does passing
+  // a value (sanitized or not) directly as an argument to formatString,
+  // but if you used a custom validation function ({validate: v => v.isHTML}
+  // instead of {type: 'string'} / {type: 'html'}) and are embedding the
+  // contents of a slot directly, it should be manually sanitized with this
+  // function first.
+  sanitize(arg) {
+    const escapeHTML = CacheableObject.getUpdateValue(this, 'escapeHTML');
+
+    if (!escapeHTML) {
+      throw new Error(`escapeHTML unavailable`);
+    }
+
+    return (
+      (typeof arg === 'string'
+        ? new Tag(null, null, escapeHTML(arg))
+        : arg));
   }
 
   formatDate(date) {
@@ -252,19 +325,32 @@ export class Language extends Thing {
   // Conjunction list: A, B, and C
   formatConjunctionList(array) {
     this.assertIntlAvailable('intl_listConjunction');
-    return this.intl_listConjunction.format(array.map(arr => arr.toString()));
+    return this.#wrapSanitized(
+      this.intl_listConjunction.format(
+        array.map(item => this.#sanitizeStringArg(item))));
   }
 
   // Disjunction lists: A, B, or C
   formatDisjunctionList(array) {
     this.assertIntlAvailable('intl_listDisjunction');
-    return this.intl_listDisjunction.format(array.map(arr => arr.toString()));
+    return this.#wrapSanitized(
+      this.intl_listDisjunction.format(
+        array.map(item => this.#sanitizeStringArg(item))));
   }
 
   // Unit lists: A, B, C
   formatUnitList(array) {
     this.assertIntlAvailable('intl_listUnit');
-    return this.intl_listUnit.format(array.map(arr => arr.toString()));
+    return this.#wrapSanitized(
+      this.intl_listUnit.format(
+        array.map(item => this.#sanitizeStringArg(item))));
+  }
+
+  // Lists without separator: A B C
+  formatListWithoutSeparator(array) {
+    return this.#wrapSanitized(
+      array.map(item => this.#sanitizeStringArg(item))
+        .join(' '));
   }
 
   // File sizes: 42.5 kB, 127.2 MB, 4.13 GB, 998.82 TB
diff --git a/src/data/things/news-entry.js b/src/data/things/news-entry.js
index 4391141..36da029 100644
--- a/src/data/things/news-entry.js
+++ b/src/data/things/news-entry.js
@@ -1,16 +1,24 @@
+import {
+  directory,
+  name,
+  simpleDate,
+  simpleString,
+} from '#composite/wiki-properties';
+
 import Thing from './thing.js';
 
 export class NewsEntry extends Thing {
   static [Thing.referenceType] = 'news-entry';
+  static [Thing.friendlyName] = `News Entry`;
 
   static [Thing.getPropertyDescriptors] = () => ({
     // Update & expose
 
-    name: Thing.common.name('Unnamed News Entry'),
-    directory: Thing.common.directory(),
-    date: Thing.common.simpleDate(),
+    name: name('Unnamed News Entry'),
+    directory: directory(),
+    date: simpleDate(),
 
-    content: Thing.common.simpleString(),
+    content: simpleString(),
 
     // Expose only
 
diff --git a/src/data/things/static-page.js b/src/data/things/static-page.js
index 3d8d474..ab9c5f9 100644
--- a/src/data/things/static-page.js
+++ b/src/data/things/static-page.js
@@ -1,16 +1,21 @@
+import {isName} from '#validators';
+
+import {
+  directory,
+  name,
+  simpleString,
+} from '#composite/wiki-properties';
+
 import Thing from './thing.js';
 
 export class StaticPage extends Thing {
   static [Thing.referenceType] = 'static';
+  static [Thing.friendlyName] = `Static Page`;
 
-  static [Thing.getPropertyDescriptors] = ({
-    validators: {
-      isName,
-    },
-  }) => ({
+  static [Thing.getPropertyDescriptors] = () => ({
     // Update & expose
 
-    name: Thing.common.name('Unnamed Static Page'),
+    name: name('Unnamed Static Page'),
 
     nameShort: {
       flags: {update: true, expose: true},
@@ -22,8 +27,8 @@ export class StaticPage extends Thing {
       },
     },
 
-    directory: Thing.common.directory(),
-    content: Thing.common.simpleString(),
-    stylesheet: Thing.common.simpleString(),
+    directory: directory(),
+    content: simpleString(),
+    stylesheet: simpleString(),
   });
 }
diff --git a/src/data/things/thing.js b/src/data/things/thing.js
index c2876f5..def7e91 100644
--- a/src/data/things/thing.js
+++ b/src/data/things/thing.js
@@ -1,399 +1,19 @@
-// Thing: base class for wiki data types, providing wiki-specific utility
-// functions on top of essential CacheableObject behavior.
+// Thing: base class for wiki data types, providing interfaces generally useful
+// to all wiki data objects on top of foundational CacheableObject behavior.
 
 import {inspect} from 'node:util';
 
-import {color} from '#cli';
-import find from '#find';
-import {empty} from '#sugar';
-import {getKebabCase} from '#wiki-data';
-
-import {
-  isAdditionalFileList,
-  isBoolean,
-  isCommentary,
-  isColor,
-  isContributionList,
-  isDate,
-  isDirectory,
-  isFileExtension,
-  isName,
-  isString,
-  isURL,
-  validateArrayItems,
-  validateInstanceOf,
-  validateReference,
-  validateReferenceList,
-} from '#validators';
+import {colors} from '#cli';
 
 import CacheableObject from './cacheable-object.js';
 
 export default class Thing extends CacheableObject {
-  static referenceType = Symbol('Thing.referenceType');
+  static referenceType = Symbol.for('Thing.referenceType');
+  static friendlyName = Symbol.for(`Thing.friendlyName`);
 
   static getPropertyDescriptors = Symbol('Thing.getPropertyDescriptors');
   static getSerializeDescriptors = Symbol('Thing.getSerializeDescriptors');
 
-  // Regularly reused property descriptors, for ease of access and generally
-  // duplicating less code across wiki data types. These are specialized utility
-  // functions, so check each for how its own arguments behave!
-  static common = {
-    name: (defaultName) => ({
-      flags: {update: true, expose: true},
-      update: {validate: isName, default: defaultName},
-    }),
-
-    color: () => ({
-      flags: {update: true, expose: true},
-      update: {validate: isColor},
-    }),
-
-    directory: () => ({
-      flags: {update: true, expose: true},
-      update: {validate: isDirectory},
-      expose: {
-        dependencies: ['name'],
-        transform(directory, {name}) {
-          if (directory === null && name === null) return null;
-          else if (directory === null) return getKebabCase(name);
-          else return directory;
-        },
-      },
-    }),
-
-    urls: () => ({
-      flags: {update: true, expose: true},
-      update: {validate: validateArrayItems(isURL)},
-      expose: {transform: (value) => value ?? []},
-    }),
-
-    // A file extension! Or the default, if provided when calling this.
-    fileExtension: (defaultFileExtension = null) => ({
-      flags: {update: true, expose: true},
-      update: {validate: isFileExtension},
-      expose: {transform: (value) => value ?? defaultFileExtension},
-    }),
-
-    // Straightforward flag descriptor for a variety of property purposes.
-    // Provide a default value, true or false!
-    flag: (defaultValue = false) => {
-      if (typeof defaultValue !== 'boolean') {
-        throw new TypeError(`Always set explicit defaults for flags!`);
-      }
-
-      return {
-        flags: {update: true, expose: true},
-        update: {validate: isBoolean, default: defaultValue},
-      };
-    },
-
-    // General date type, used as the descriptor for a bunch of properties.
-    // This isn't dynamic though - it won't inherit from a date stored on
-    // another object, for example.
-    simpleDate: () => ({
-      flags: {update: true, expose: true},
-      update: {validate: isDate},
-    }),
-
-    // General string type. This should probably generally be avoided in favor
-    // of more specific validation, but using it makes it easy to find where we
-    // might want to improve later, and it's a useful shorthand meanwhile.
-    simpleString: () => ({
-      flags: {update: true, expose: true},
-      update: {validate: isString},
-    }),
-
-    // External function. These should only be used as dependencies for other
-    // properties, so they're left unexposed.
-    externalFunction: () => ({
-      flags: {update: true},
-      update: {validate: (t) => typeof t === 'function'},
-    }),
-
-    // Super simple "contributions by reference" list, used for a variety of
-    // properties (Artists, Cover Artists, etc). This is the property which is
-    // externally provided, in the form:
-    //
-    //     [
-    //         {who: 'Artist Name', what: 'Viola'},
-    //         {who: 'artist:john-cena', what: null},
-    //         ...
-    //     ]
-    //
-    // ...processed from YAML, spreadsheet, or any other kind of input.
-    contribsByRef: () => ({
-      flags: {update: true, expose: true},
-      update: {validate: isContributionList},
-    }),
-
-    // Artist commentary! Generally present on tracks and albums.
-    commentary: () => ({
-      flags: {update: true, expose: true},
-      update: {validate: isCommentary},
-    }),
-
-    // This is a somewhat more involved data structure - it's for additional
-    // or "bonus" files associated with albums or tracks (or anything else).
-    // It's got this form:
-    //
-    //     [
-    //         {title: 'Booklet', files: ['Booklet.pdf']},
-    //         {
-    //             title: 'Wallpaper',
-    //             description: 'Cool Wallpaper!',
-    //             files: ['1440x900.png', '1920x1080.png']
-    //         },
-    //         {title: 'Alternate Covers', description: null, files: [...]},
-    //         ...
-    //     ]
-    //
-    additionalFiles: () => ({
-      flags: {update: true, expose: true},
-      update: {validate: isAdditionalFileList},
-      expose: {
-        transform: (additionalFiles) =>
-          additionalFiles ?? [],
-      },
-    }),
-
-    // A reference list! Keep in mind this is for general references to wiki
-    // objects of (usually) other Thing subclasses, not specifically leitmotif
-    // references in tracks (although that property uses referenceList too!).
-    //
-    // The underlying function validateReferenceList expects a string like
-    // 'artist' or 'track', but this utility keeps from having to hard-code the
-    // string in multiple places by referencing the value saved on the class
-    // instead.
-    referenceList: (thingClass) => {
-      const {[Thing.referenceType]: referenceType} = thingClass;
-      if (!referenceType) {
-        throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`);
-      }
-
-      return {
-        flags: {update: true, expose: true},
-        update: {validate: validateReferenceList(referenceType)},
-      };
-    },
-
-    // Corresponding function for a single reference.
-    singleReference: (thingClass) => {
-      const {[Thing.referenceType]: referenceType} = thingClass;
-      if (!referenceType) {
-        throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`);
-      }
-
-      return {
-        flags: {update: true, expose: true},
-        update: {validate: validateReference(referenceType)},
-      };
-    },
-
-    // Corresponding dynamic property to referenceList, which takes the values
-    // in the provided property and searches the specified wiki data for
-    // matching actual Thing-subclass objects.
-    dynamicThingsFromReferenceList: (
-      referenceListProperty,
-      thingDataProperty,
-      findFn
-    ) => ({
-      flags: {expose: true},
-
-      expose: {
-        dependencies: [referenceListProperty, thingDataProperty],
-        compute: ({
-          [referenceListProperty]: refs,
-          [thingDataProperty]: thingData,
-        }) =>
-          refs && thingData
-            ? refs
-                .map((ref) => findFn(ref, thingData, {mode: 'quiet'}))
-                .filter(Boolean)
-            : [],
-      },
-    }),
-
-    // Corresponding function for a single reference.
-    dynamicThingFromSingleReference: (
-      singleReferenceProperty,
-      thingDataProperty,
-      findFn
-    ) => ({
-      flags: {expose: true},
-
-      expose: {
-        dependencies: [singleReferenceProperty, thingDataProperty],
-        compute: ({
-          [singleReferenceProperty]: ref,
-          [thingDataProperty]: thingData,
-        }) => (ref && thingData ? findFn(ref, thingData, {mode: 'quiet'}) : null),
-      },
-    }),
-
-    // Corresponding dynamic property to contribsByRef, which takes the values
-    // in the provided property and searches the object's artistData for
-    // matching actual Artist objects. The computed structure has the same form
-    // as contribsByRef, but with Artist objects instead of string references:
-    //
-    //     [
-    //         {who: (an Artist), what: 'Viola'},
-    //         {who: (an Artist), what: null},
-    //         ...
-    //     ]
-    //
-    // Contributions whose "who" values don't match anything in artistData are
-    // filtered out. (So if the list is all empty, chances are that either the
-    // reference list is somehow messed up, or artistData isn't being provided
-    // properly.)
-    dynamicContribs: (contribsByRefProperty) => ({
-      flags: {expose: true},
-      expose: {
-        dependencies: ['artistData', contribsByRefProperty],
-        compute: ({artistData, [contribsByRefProperty]: contribsByRef}) =>
-          contribsByRef && artistData
-            ? contribsByRef
-                .map(({who: ref, what}) => ({
-                  who: find.artist(ref, artistData),
-                  what,
-                }))
-                .filter(({who}) => who)
-            : [],
-      },
-    }),
-
-    // Dynamically inherit a contribution list from some other object, if it
-    // hasn't been overridden on this object. This is handy for solo albums
-    // where all tracks have the same artist, for example.
-    dynamicInheritContribs: (
-      // If this property is explicitly false, the contribution list returned
-      // will always be empty.
-      nullerProperty,
-
-      // Property holding contributions on the current object.
-      contribsByRefProperty,
-
-      // Property holding corresponding "default" contributions on the parent
-      // object, which will fallen back to if the object doesn't have its own
-      // contribs.
-      parentContribsByRefProperty,
-
-      // Data array to search in and "find" function to locate parent object
-      // (which will be passed the child object and the wiki data array).
-      thingDataProperty,
-      findFn
-    ) => ({
-      flags: {expose: true},
-      expose: {
-        dependencies: [
-          contribsByRefProperty,
-          thingDataProperty,
-          nullerProperty,
-          'artistData',
-        ].filter(Boolean),
-
-        compute({
-          [Thing.instance]: thing,
-          [nullerProperty]: nuller,
-          [contribsByRefProperty]: contribsByRef,
-          [thingDataProperty]: thingData,
-          artistData,
-        }) {
-          if (!artistData) return [];
-          if (nuller === false) return [];
-          const refs =
-            contribsByRef ??
-            findFn(thing, thingData, {mode: 'quiet'})?.[parentContribsByRefProperty];
-          if (!refs) return [];
-          return refs
-            .map(({who: ref, what}) => ({
-              who: find.artist(ref, artistData),
-              what,
-            }))
-            .filter(({who}) => who);
-        },
-      },
-    }),
-
-    // Nice 'n simple shorthand for an exposed-only flag which is true when any
-    // contributions are present in the specified property.
-    contribsPresent: (contribsByRefProperty) => ({
-      flags: {expose: true},
-      expose: {
-        dependencies: [contribsByRefProperty],
-        compute({
-          [contribsByRefProperty]: contribsByRef,
-        }) {
-          return !empty(contribsByRef);
-        },
-      }
-    }),
-
-    // Neat little shortcut for "reversing" the reference lists stored on other
-    // things - for example, tracks specify a "referenced tracks" property, and
-    // you would use this to compute a corresponding "referenced *by* tracks"
-    // property. Naturally, the passed ref list property is of the things in the
-    // wiki data provided, not the requesting Thing itself.
-    reverseReferenceList: (thingDataProperty, referencerRefListProperty) => ({
-      flags: {expose: true},
-
-      expose: {
-        dependencies: [thingDataProperty],
-
-        compute: ({[thingDataProperty]: thingData, [Thing.instance]: thing}) =>
-          thingData?.filter(t => t[referencerRefListProperty].includes(thing)) ?? [],
-      },
-    }),
-
-    // Corresponding function for single references. Note that the return value
-    // is still a list - this is for matching all the objects whose single
-    // reference (in the given property) matches this Thing.
-    reverseSingleReference: (thingDataProperty, referencerRefListProperty) => ({
-      flags: {expose: true},
-
-      expose: {
-        dependencies: [thingDataProperty],
-
-        compute: ({[thingDataProperty]: thingData, [Thing.instance]: thing}) =>
-          thingData?.filter((t) => t[referencerRefListProperty] === thing) ?? [],
-      },
-    }),
-
-    // General purpose wiki data constructor, for properties like artistData,
-    // trackData, etc.
-    wikiData: (thingClass) => ({
-      flags: {update: true},
-      update: {
-        validate: validateArrayItems(validateInstanceOf(thingClass)),
-      },
-    }),
-
-    // This one's kinda tricky: it parses artist "references" from the
-    // commentary content, and finds the matching artist for each reference.
-    // This is mostly useful for credits and listings on artist pages.
-    commentatorArtists: () => ({
-      flags: {expose: true},
-
-      expose: {
-        dependencies: ['artistData', 'commentary'],
-
-        compute: ({artistData, commentary}) =>
-          artistData && commentary
-            ? Array.from(
-                new Set(
-                  Array.from(
-                    commentary
-                      .replace(/<\/?b>/g, '')
-                      .matchAll(/<i>(?<who>.*?):<\/i>/g)
-                  ).map(({groups: {who}}) =>
-                    find.artist(who, artistData, {mode: 'quiet'})
-                  )
-                )
-              )
-            : [],
-      },
-    }),
-  };
-
   // Default custom inspect function, which may be overridden by Thing
   // subclasses. This will be used when displaying aggregate errors and other
   // command-line logging - it's the place to provide information useful in
@@ -402,8 +22,8 @@ export default class Thing extends CacheableObject {
     const cname = this.constructor.name;
 
     return (
-      (this.name ? `${cname} ${color.green(`"${this.name}"`)}` : `${cname}`) +
-      (this.directory ? ` (${color.blue(Thing.getReference(this))})` : '')
+      (this.name ? `${cname} ${colors.green(`"${this.name}"`)}` : `${cname}`) +
+      (this.directory ? ` (${colors.blue(Thing.getReference(this))})` : '')
     );
   }
 
diff --git a/src/data/things/track.js b/src/data/things/track.js
index e176acb..db325a1 100644
--- a/src/data/things/track.js
+++ b/src/data/things/track.js
@@ -1,460 +1,330 @@
 import {inspect} from 'node:util';
 
-import {color} from '#cli';
+import {colors} from '#cli';
+import {input} from '#composite';
 import find from '#find';
-import {empty} from '#sugar';
 
+import {
+  isColor,
+  isContributionList,
+  isDate,
+  isFileExtension,
+} from '#validators';
+
+import {withPropertyFromObject} from '#composite/data';
+import {withResolvedContribs} from '#composite/wiki-data';
+
+import {
+  exitWithoutDependency,
+  exposeConstant,
+  exposeDependency,
+  exposeDependencyOrContinue,
+  exposeUpdateValueOrContinue,
+} from '#composite/control-flow';
+
+import {
+  additionalFiles,
+  commentary,
+  commentatorArtists,
+  contributionList,
+  directory,
+  duration,
+  flag,
+  name,
+  referenceList,
+  reverseReferenceList,
+  simpleDate,
+  singleReference,
+  simpleString,
+  urls,
+  wikiData,
+} from '#composite/wiki-properties';
+
+import {
+  exitWithoutUniqueCoverArt,
+  inheritFromOriginalRelease,
+  trackReverseReferenceList,
+  withAlbum,
+  withAlwaysReferenceByDirectory,
+  withContainingTrackSection,
+  withHasUniqueCoverArt,
+  withOtherReleases,
+  withPropertyFromAlbum,
+} from '#composite/things/track';
+
+import CacheableObject from './cacheable-object.js';
 import Thing from './thing.js';
 
 export class Track extends Thing {
   static [Thing.referenceType] = 'track';
 
-  static [Thing.getPropertyDescriptors] = ({
-    Album,
-    ArtTag,
-    Artist,
-    Flash,
-
-    validators: {
-      isBoolean,
-      isColor,
-      isDate,
-      isDuration,
-      isFileExtension,
-    },
-  }) => ({
+  static [Thing.getPropertyDescriptors] = ({Album, ArtTag, Artist, Flash}) => ({
     // Update & expose
 
-    name: Thing.common.name('Unnamed Track'),
-    directory: Thing.common.directory(),
-
-    duration: {
-      flags: {update: true, expose: true},
-      update: {validate: isDuration},
-    },
-
-    urls: Thing.common.urls(),
-    dateFirstReleased: Thing.common.simpleDate(),
-
-    artistContribsByRef: Thing.common.contribsByRef(),
-    contributorContribsByRef: Thing.common.contribsByRef(),
-    coverArtistContribsByRef: Thing.common.contribsByRef(),
-
-    referencedTracksByRef: Thing.common.referenceList(Track),
-    sampledTracksByRef: Thing.common.referenceList(Track),
-    artTagsByRef: Thing.common.referenceList(ArtTag),
-
-    hasCoverArt: {
-      flags: {update: true, expose: true},
-
-      update: {
-        validate(value) {
-          if (value !== false) {
-            throw new TypeError(`Expected false or null`);
-          }
-
-          return true;
-        },
-      },
-
-      expose: {
-        dependencies: ['albumData', 'coverArtistContribsByRef'],
-        transform: (hasCoverArt, {
-          albumData,
-          coverArtistContribsByRef,
-          [Track.instance]: track,
-        }) =>
-          Track.hasCoverArt(
-            track,
-            albumData,
-            coverArtistContribsByRef,
-            hasCoverArt
-          ),
-      },
-    },
-
-    coverArtFileExtension: {
-      flags: {update: true, expose: true},
-
-      update: {validate: isFileExtension},
-
-      expose: {
-        dependencies: ['albumData', 'coverArtistContribsByRef'],
-        transform: (coverArtFileExtension, {
-          albumData,
-          coverArtistContribsByRef,
-          hasCoverArt,
-          [Track.instance]: track,
-        }) =>
-          coverArtFileExtension ??
-          (Track.hasCoverArt(
-            track,
-            albumData,
-            coverArtistContribsByRef,
-            hasCoverArt
-          )
-            ? Track.findAlbum(track, albumData)?.trackCoverArtFileExtension
-            : Track.findAlbum(track, albumData)?.coverArtFileExtension) ??
-          'jpg',
-      },
-    },
-
-    originalReleaseTrackByRef: Thing.common.singleReference(Track),
-
-    dataSourceAlbumByRef: Thing.common.singleReference(Album),
-
-    commentary: Thing.common.commentary(),
-    lyrics: Thing.common.simpleString(),
-    additionalFiles: Thing.common.additionalFiles(),
-    sheetMusicFiles: Thing.common.additionalFiles(),
-    midiProjectFiles: Thing.common.additionalFiles(),
+    name: name('Unnamed Track'),
+    directory: directory(),
+
+    duration: duration(),
+    urls: urls(),
+    dateFirstReleased: simpleDate(),
+
+    color: [
+      exposeUpdateValueOrContinue({
+        validate: input.value(isColor),
+      }),
+
+      withContainingTrackSection(),
+
+      withPropertyFromObject({
+        object: '#trackSection',
+        property: input.value('color'),
+      }),
+
+      exposeDependencyOrContinue({dependency: '#trackSection.color'}),
+
+      withPropertyFromAlbum({
+        property: input.value('color'),
+      }),
+
+      exposeDependency({dependency: '#album.color'}),
+    ],
+
+    alwaysReferenceByDirectory: [
+      withAlwaysReferenceByDirectory(),
+      exposeDependency({dependency: '#alwaysReferenceByDirectory'}),
+    ],
+
+    // Disables presenting the track as though it has its own unique artwork.
+    // This flag should only be used in select circumstances, i.e. to override
+    // an album's trackCoverArtists. This flag supercedes that property, as well
+    // as the track's own coverArtists.
+    disableUniqueCoverArt: flag(),
+
+    // File extension for track's corresponding media file. This represents the
+    // track's unique cover artwork, if any, and does not inherit the extension
+    // of the album's main artwork. It does inherit trackCoverArtFileExtension,
+    // if present on the album.
+    coverArtFileExtension: [
+      exitWithoutUniqueCoverArt(),
+
+      exposeUpdateValueOrContinue({
+        validate: input.value(isFileExtension),
+      }),
+
+      withPropertyFromAlbum({
+        property: input.value('trackCoverArtFileExtension'),
+      }),
+
+      exposeDependencyOrContinue({dependency: '#album.trackCoverArtFileExtension'}),
+
+      exposeConstant({
+        value: input.value('jpg'),
+      }),
+    ],
+
+    // Date of cover art release. Like coverArtFileExtension, this represents
+    // only the track's own unique cover artwork, if any. This exposes only as
+    // the track's own coverArtDate or its album's trackArtDate, so if neither
+    // is specified, this value is null.
+    coverArtDate: [
+      withHasUniqueCoverArt(),
+
+      exitWithoutDependency({
+        dependency: '#hasUniqueCoverArt',
+        mode: input.value('falsy'),
+      }),
+
+      exposeUpdateValueOrContinue({
+        validate: input.value(isDate),
+      }),
+
+      withPropertyFromAlbum({
+        property: input.value('trackArtDate'),
+      }),
+
+      exposeDependency({dependency: '#album.trackArtDate'}),
+    ],
+
+    commentary: commentary(),
+    lyrics: simpleString(),
+
+    additionalFiles: additionalFiles(),
+    sheetMusicFiles: additionalFiles(),
+    midiProjectFiles: additionalFiles(),
+
+    originalReleaseTrack: singleReference({
+      class: input.value(Track),
+      find: input.value(find.track),
+      data: 'trackData',
+    }),
+
+    // Internal use only - for directly identifying an album inside a track's
+    // util.inspect display, if it isn't indirectly available (by way of being
+    // included in an album's track list).
+    dataSourceAlbum: singleReference({
+      class: input.value(Album),
+      find: input.value(find.album),
+      data: 'albumData',
+    }),
+
+    artistContribs: [
+      inheritFromOriginalRelease({
+        property: input.value('artistContribs'),
+      }),
+
+      withResolvedContribs({
+        from: input.updateValue({validate: isContributionList}),
+      }).outputs({
+        '#resolvedContribs': '#artistContribs',
+      }),
+
+      exposeDependencyOrContinue({
+        dependency: '#artistContribs',
+        mode: input.value('empty'),
+      }),
+
+      withPropertyFromAlbum({
+        property: input.value('artistContribs'),
+      }),
+
+      exposeDependency({dependency: '#album.artistContribs'}),
+    ],
+
+    contributorContribs: [
+      inheritFromOriginalRelease({
+        property: input.value('contributorContribs'),
+      }),
+
+      contributionList(),
+    ],
+
+    // Cover artists aren't inherited from the original release, since it
+    // typically varies by release and isn't defined by the musical qualities
+    // of the track.
+    coverArtistContribs: [
+      exitWithoutUniqueCoverArt({
+        value: input.value([]),
+      }),
+
+      withResolvedContribs({
+        from: input.updateValue({validate: isContributionList}),
+      }).outputs({
+        '#resolvedContribs': '#coverArtistContribs',
+      }),
+
+      exposeDependencyOrContinue({
+        dependency: '#coverArtistContribs',
+        mode: input.value('empty'),
+      }),
+
+      withPropertyFromAlbum({
+        property: input.value('trackCoverArtistContribs'),
+      }),
+
+      exposeDependency({dependency: '#album.trackCoverArtistContribs'}),
+    ],
+
+    referencedTracks: [
+      inheritFromOriginalRelease({
+        property: input.value('referencedTracks'),
+      }),
+
+      referenceList({
+        class: input.value(Track),
+        find: input.value(find.track),
+        data: 'trackData',
+      }),
+    ],
+
+    sampledTracks: [
+      inheritFromOriginalRelease({
+        property: input.value('sampledTracks'),
+      }),
+
+      referenceList({
+        class: input.value(Track),
+        find: input.value(find.track),
+        data: 'trackData',
+      }),
+    ],
+
+    artTags: referenceList({
+      class: input.value(ArtTag),
+      find: input.value(find.artTag),
+      data: 'artTagData',
+    }),
 
     // Update only
 
-    albumData: Thing.common.wikiData(Album),
-    artistData: Thing.common.wikiData(Artist),
-    artTagData: Thing.common.wikiData(ArtTag),
-    flashData: Thing.common.wikiData(Flash),
-    trackData: Thing.common.wikiData(Track),
+    albumData: wikiData(Album),
+    artistData: wikiData(Artist),
+    artTagData: wikiData(ArtTag),
+    flashData: wikiData(Flash),
+    trackData: wikiData(Track),
 
     // Expose only
 
-    commentatorArtists: Thing.common.commentatorArtists(),
-
-    album: {
-      flags: {expose: true},
-
-      expose: {
-        dependencies: ['albumData'],
-        compute: ({[Track.instance]: track, albumData}) =>
-          albumData?.find((album) => album.tracks.includes(track)) ?? null,
-      },
-    },
-
-    // Note - this is an internal property used only to help identify a track.
-    // It should not be assumed in general that the album and dataSourceAlbum match
-    // (i.e. a track may dynamically be moved from one album to another, at
-    // which point dataSourceAlbum refers to where it was originally from, and is
-    // not generally relevant information). It's also not guaranteed that
-    // dataSourceAlbum is available (depending on the Track creator to optionally
-    // provide dataSourceAlbumByRef).
-    dataSourceAlbum: Thing.common.dynamicThingFromSingleReference(
-      'dataSourceAlbumByRef',
-      'albumData',
-      find.album
-    ),
-
-    date: {
-      flags: {expose: true},
-
-      expose: {
-        dependencies: ['albumData', 'dateFirstReleased'],
-        compute: ({albumData, dateFirstReleased, [Track.instance]: track}) =>
-          dateFirstReleased ?? Track.findAlbum(track, albumData)?.date ?? null,
-      },
-    },
-
-    color: {
-      flags: {update: true, expose: true},
-
-      update: {validate: isColor},
-
-      expose: {
-        dependencies: ['albumData'],
-
-        transform: (color, {albumData, [Track.instance]: track}) =>
-          color ??
-            Track.findAlbum(track, albumData)
-              ?.trackSections.find(({tracks}) => tracks.includes(track))
-                ?.color ?? null,
-      },
-    },
-
-    coverArtDate: {
-      flags: {update: true, expose: true},
-
-      update: {validate: isDate},
-
-      expose: {
-        dependencies: [
-          'albumData',
-          'coverArtistContribsByRef',
-          'dateFirstReleased',
-          'hasCoverArt',
-        ],
-        transform: (coverArtDate, {
-          albumData,
-          coverArtistContribsByRef,
-          dateFirstReleased,
-          hasCoverArt,
-          [Track.instance]: track,
-        }) =>
-          (Track.hasCoverArt(track, albumData, coverArtistContribsByRef, hasCoverArt)
-            ? coverArtDate ??
-              dateFirstReleased ??
-              Track.findAlbum(track, albumData)?.trackArtDate ??
-              Track.findAlbum(track, albumData)?.date ??
-              null
-            : null),
-      },
-    },
-
-    hasUniqueCoverArt: {
-      flags: {expose: true},
-
-      expose: {
-        dependencies: ['albumData', 'coverArtistContribsByRef', 'hasCoverArt'],
-        compute: ({
-          albumData,
-          coverArtistContribsByRef,
-          hasCoverArt,
-          [Track.instance]: track,
-        }) =>
-          Track.hasUniqueCoverArt(
-            track,
-            albumData,
-            coverArtistContribsByRef,
-            hasCoverArt
-          ),
-      },
-    },
-
-    originalReleaseTrack: Thing.common.dynamicThingFromSingleReference(
-      'originalReleaseTrackByRef',
-      'trackData',
-      find.track
-    ),
-
-    otherReleases: {
-      flags: {expose: true},
-
-      expose: {
-        dependencies: ['originalReleaseTrackByRef', 'trackData'],
-
-        compute: ({
-          originalReleaseTrackByRef: t1origRef,
-          trackData,
-          [Track.instance]: t1,
-        }) => {
-          if (!trackData) {
-            return [];
-          }
-
-          const t1orig = find.track(t1origRef, trackData);
-
-          return [
-            t1orig,
-            ...trackData.filter((t2) => {
-              const {originalReleaseTrack: t2orig} = t2;
-              return t2 !== t1 && t2orig && (t2orig === t1orig || t2orig === t1);
-            }),
-          ].filter(Boolean);
-        },
-      },
-    },
-
-    artistContribs:
-      Track.inheritFromOriginalRelease('artistContribs', [],
-        Thing.common.dynamicInheritContribs(
-          null,
-          'artistContribsByRef',
-          'artistContribsByRef',
-          'albumData',
-          Track.findAlbum)),
-
-    contributorContribs:
-      Track.inheritFromOriginalRelease('contributorContribs', [],
-        Thing.common.dynamicContribs('contributorContribsByRef')),
-
-    // Cover artists aren't inherited from the original release, since it
-    // typically varies by release and isn't defined by the musical qualities
-    // of the track.
-    coverArtistContribs:
-      Thing.common.dynamicInheritContribs(
-        'hasCoverArt',
-        'coverArtistContribsByRef',
-        'trackCoverArtistContribsByRef',
-        'albumData',
-        Track.findAlbum),
-
-    referencedTracks:
-      Track.inheritFromOriginalRelease('referencedTracks', [],
-        Thing.common.dynamicThingsFromReferenceList(
-          'referencedTracksByRef',
-          'trackData',
-          find.track)),
-
-    sampledTracks:
-      Track.inheritFromOriginalRelease('sampledTracks', [],
-        Thing.common.dynamicThingsFromReferenceList(
-          'sampledTracksByRef',
-          'trackData',
-          find.track)),
-
-    // Specifically exclude re-releases from this list - while it's useful to
-    // get from a re-release to the tracks it references, re-releases aren't
-    // generally relevant from the perspective of the tracks being referenced.
-    // Filtering them from data here hides them from the corresponding field
-    // on the site (obviously), and has the bonus of not counting them when
-    // counting the number of times a track has been referenced, for use in
-    // the "Tracks - by Times Referenced" listing page (or other data
-    // processing).
-    referencedByTracks: {
-      flags: {expose: true},
-
-      expose: {
-        dependencies: ['trackData'],
-
-        compute: ({trackData, [Track.instance]: track}) =>
-          trackData
-            ? trackData
-                .filter((t) => !t.originalReleaseTrack)
-                .filter((t) => t.referencedTracks?.includes(track))
-            : [],
-      },
-    },
-
-    // For the same reasoning, exclude re-releases from sampled tracks too.
-    sampledByTracks: {
-      flags: {expose: true},
-
-      expose: {
-        dependencies: ['trackData'],
-
-        compute: ({trackData, [Track.instance]: track}) =>
-          trackData
-            ? trackData
-                .filter((t) => !t.originalReleaseTrack)
-                .filter((t) => t.sampledTracks?.includes(track))
-            : [],
-      },
-    },
-
-    featuredInFlashes: Thing.common.reverseReferenceList(
-      'flashData',
-      'featuredTracks'
-    ),
-
-    artTags: Thing.common.dynamicThingsFromReferenceList(
-      'artTagsByRef',
-      'artTagData',
-      find.artTag
-    ),
-  });
-
-  // This is a quick utility function for now, since the same code is reused in
-  // several places. Ideally it wouldn't be - we'd just reuse the `album`
-  // property - but support for that hasn't been coded yet :P
-  static findAlbum = (track, albumData) =>
-    albumData?.find((album) => album.tracks.includes(track));
-
-  // Another reused utility function. This one's logic is a bit more complicated.
-  static hasCoverArt(
-    track,
-    albumData,
-    coverArtistContribsByRef,
-    hasCoverArt
-  ) {
-    if (!empty(coverArtistContribsByRef)) {
-      return true;
-    }
+    commentatorArtists: commentatorArtists(),
 
-    const album = Track.findAlbum(track, albumData);
-    if (album && !empty(album.trackCoverArtistContribsByRef)) {
-      return true;
-    }
+    album: [
+      withAlbum(),
+      exposeDependency({dependency: '#album'}),
+    ],
 
-    return false;
-  }
+    date: [
+      exposeDependencyOrContinue({dependency: 'dateFirstReleased'}),
 
-  static hasUniqueCoverArt(
-    track,
-    albumData,
-    coverArtistContribsByRef,
-    hasCoverArt
-  ) {
-    if (!empty(coverArtistContribsByRef)) {
-      return true;
-    }
+      withPropertyFromAlbum({
+        property: input.value('date'),
+      }),
 
-    if (hasCoverArt === false) {
-      return false;
-    }
+      exposeDependency({dependency: '#album.date'}),
+    ],
 
-    const album = Track.findAlbum(track, albumData);
-    if (album && !empty(album.trackCoverArtistContribsByRef)) {
-      return true;
-    }
+    hasUniqueCoverArt: [
+      withHasUniqueCoverArt(),
+      exposeDependency({dependency: '#hasUniqueCoverArt'}),
+    ],
 
-    return false;
-  }
+    otherReleases: [
+      withOtherReleases(),
+      exposeDependency({dependency: '#otherReleases'}),
+    ],
 
-  static inheritFromOriginalRelease(
-    originalProperty,
-    originalMissingValue,
-    ownPropertyDescriptor
-  ) {
-    return {
-      flags: {expose: true},
-
-      expose: {
-        dependencies: [
-          ...ownPropertyDescriptor.expose.dependencies,
-          'originalReleaseTrackByRef',
-          'trackData',
-        ],
-
-        compute(dependencies) {
-          const {
-            originalReleaseTrackByRef,
-            trackData,
-          } = dependencies;
-
-          if (originalReleaseTrackByRef) {
-            if (!trackData) return originalMissingValue;
-            const original = find.track(originalReleaseTrackByRef, trackData, {mode: 'quiet'});
-            if (!original) return originalMissingValue;
-            return original[originalProperty];
-          }
-
-          return ownPropertyDescriptor.expose.compute(dependencies);
-        },
-      },
-    };
-  }
+    referencedByTracks: trackReverseReferenceList({
+      list: input.value('referencedTracks'),
+    }),
 
-  [inspect.custom]() {
-    const base = Thing.prototype[inspect.custom].apply(this);
+    sampledByTracks: trackReverseReferenceList({
+      list: input.value('sampledTracks'),
+    }),
 
-    const rereleasePart =
-      (this.originalReleaseTrackByRef
-        ? `${color.yellow('[rerelease]')} `
-        : ``);
+    featuredInFlashes: reverseReferenceList({
+      data: 'flashData',
+      list: input.value('featuredTracks'),
+    }),
+  });
 
-    const {album, dataSourceAlbum} = this;
+  [inspect.custom](depth) {
+    const parts = [];
 
-    const albumName =
-      (album
-        ? album.name
-        : dataSourceAlbum?.name);
+    parts.push(Thing.prototype[inspect.custom].apply(this));
 
-    const albumIndex =
-      albumName &&
-        (album
-          ? album.tracks.indexOf(this)
-          : dataSourceAlbum.tracks.indexOf(this));
+    if (CacheableObject.getUpdateValue(this, 'originalReleaseTrack')) {
+      parts.unshift(`${colors.yellow('[rerelease]')} `);
+    }
 
-    const trackNum =
-      albumName &&
+    let album;
+    if (depth >= 0 && (album = this.album ?? this.dataSourceAlbum)) {
+      const albumName = album.name;
+      const albumIndex = album.tracks.indexOf(this);
+      const trackNum =
         (albumIndex === -1
           ? '#?'
           : `#${albumIndex + 1}`);
+      parts.push(` (${colors.yellow(trackNum)} in ${colors.green(albumName)})`);
+    }
 
-    const albumPart =
-      albumName
-        ? ` (${color.yellow(trackNum)} in ${color.green(albumName)})`
-        : ``;
-
-    return rereleasePart + base + albumPart;
+    return parts.join('');
   }
 }
diff --git a/src/data/things/validators.js b/src/data/things/validators.js
index fc953c2..ee301f1 100644
--- a/src/data/things/validators.js
+++ b/src/data/things/validators.js
@@ -1,7 +1,7 @@
 import {inspect as nodeInspect} from 'node:util';
 
-import {color, ENABLE_COLOR} from '#cli';
-import {withAggregate} from '#sugar';
+import {colors, ENABLE_COLOR} from '#cli';
+import {empty, typeAppearance, withAggregate} from '#sugar';
 
 function inspect(value) {
   return nodeInspect(value, {colors: ENABLE_COLOR});
@@ -9,13 +9,13 @@ function inspect(value) {
 
 // Basic types (primitives)
 
-function a(noun) {
+export function a(noun) {
   return /[aeiou]/.test(noun[0]) ? `an ${noun}` : `a ${noun}`;
 }
 
-function isType(value, type) {
+export function isType(value, type) {
   if (typeof value !== type)
-    throw new TypeError(`Expected ${a(type)}, got ${typeof value}`);
+    throw new TypeError(`Expected ${a(type)}, got ${typeAppearance(value)}`);
 
   return true;
 }
@@ -132,7 +132,7 @@ export function isObject(value) {
 
 export function isArray(value) {
   if (typeof value !== 'object' || value === null || !Array.isArray(value))
-    throw new TypeError(`Expected an array, got ${value}`);
+    throw new TypeError(`Expected an array, got ${typeAppearance(value)}`);
 
   return true;
 }
@@ -174,7 +174,8 @@ function validateArrayItemsHelper(itemValidator) {
         throw new Error(`Expected validator to return true`);
       }
     } catch (error) {
-      error.message = `(index: ${color.green(index)}, item: ${inspect(item)}) ${error.message}`;
+      error.message = `(index: ${colors.yellow(`${index}`)}, item: ${inspect(item)}) ${error.message}`;
+      error[Symbol.for('hsmusic.decorate.indexInSourceArray')] = index;
       throw error;
     }
   };
@@ -264,7 +265,7 @@ export function validateProperties(spec) {
           try {
             specValidator(value);
           } catch (error) {
-            error.message = `(key: ${color.green(specKey)}, value: ${inspect(value)}) ${error.message}`;
+            error.message = `(key: ${colors.green(specKey)}, value: ${inspect(value)}) ${error.message}`;
             throw error;
           }
         });
@@ -308,7 +309,7 @@ export const isTrackSection = validateProperties({
   color: optional(isColor),
   dateOriginallyReleased: optional(isDate),
   isDefaultTrackSection: optional(isBoolean),
-  tracksByRef: optional(validateReferenceList('track')),
+  tracks: optional(validateReferenceList('track')),
 });
 
 export const isTrackSectionList = validateArrayItems(isTrackSection);
@@ -404,6 +405,76 @@ export function validateReferenceList(type = '') {
   return validateArrayItems(validateReference(type));
 }
 
+const validateWikiData_cache = {};
+
+export function validateWikiData({
+  referenceType = '',
+  allowMixedTypes = false,
+}) {
+  if (referenceType && allowMixedTypes) {
+    throw new TypeError(`Don't specify both referenceType and allowMixedTypes`);
+  }
+
+  validateWikiData_cache[referenceType] ??= {};
+  validateWikiData_cache[referenceType][allowMixedTypes] ??= new WeakMap();
+
+  const isArrayOfObjects = validateArrayItems(isObject);
+
+  return (array) => {
+    const subcache = validateWikiData_cache[referenceType][allowMixedTypes];
+    if (subcache.has(array)) return subcache.get(array);
+
+    let OK = false;
+
+    try {
+      isArrayOfObjects(array);
+
+      if (empty(array)) {
+        OK = true; return true;
+      }
+
+      const allRefTypes =
+        new Set(array.map(object =>
+          object.constructor[Symbol.for('Thing.referenceType')]));
+
+      if (allRefTypes.has(undefined)) {
+        if (allRefTypes.size === 1) {
+          throw new TypeError(`Expected array of wiki data objects, got array of other objects`);
+        } else {
+          throw new TypeError(`Expected array of wiki data objects, got mixed items`);
+        }
+      }
+
+      if (allRefTypes.size > 1) {
+        if (allowMixedTypes) {
+          OK = true; return true;
+        }
+
+        const types = () => Array.from(allRefTypes).join(', ');
+
+        if (referenceType) {
+          if (allRefTypes.has(referenceType)) {
+            allRefTypes.remove(referenceType);
+            throw new TypeError(`Expected array of only ${referenceType}, also got other types: ${types()}`)
+          } else {
+            throw new TypeError(`Expected array of only ${referenceType}, got other types: ${types()}`);
+          }
+        }
+
+        throw new TypeError(`Expected array of unmixed reference types, got multiple: ${types()}`);
+      }
+
+      if (referenceType && !allRefTypes.has(referenceType)) {
+        throw new TypeError(`Expected array of ${referenceType}, got array of ${allRefTypes[0]}`)
+      }
+
+      OK = true; return true;
+    } finally {
+      subcache.set(array, OK);
+    }
+  };
+}
+
 // Compositional utilities
 
 export function oneOf(...checks) {
diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js
index e906cab..6286a26 100644
--- a/src/data/things/wiki-info.js
+++ b/src/data/things/wiki-info.js
@@ -1,20 +1,25 @@
+import {input} from '#composite';
 import find from '#find';
+import {isLanguageCode, isName, isURL} from '#validators';
+
+import {
+  color,
+  flag,
+  name,
+  referenceList,
+  simpleString,
+  wikiData,
+} from '#composite/wiki-properties';
 
 import Thing from './thing.js';
 
 export class WikiInfo extends Thing {
-  static [Thing.getPropertyDescriptors] = ({
-    Group,
+  static [Thing.friendlyName] = `Wiki Info`;
 
-    validators: {
-      isLanguageCode,
-      isName,
-      isURL,
-    },
-  }) => ({
+  static [Thing.getPropertyDescriptors] = ({Group}) => ({
     // Update & expose
 
-    name: Thing.common.name('Unnamed Wiki'),
+    name: name('Unnamed Wiki'),
 
     // Displayed in nav bar.
     nameShort: {
@@ -27,12 +32,12 @@ export class WikiInfo extends Thing {
       },
     },
 
-    color: Thing.common.color(),
+    color: color(),
 
     // One-line description used for <meta rel="description"> tag.
-    description: Thing.common.simpleString(),
+    description: simpleString(),
 
-    footerContent: Thing.common.simpleString(),
+    footerContent: simpleString(),
 
     defaultLanguage: {
       flags: {update: true, expose: true},
@@ -44,25 +49,21 @@ export class WikiInfo extends Thing {
       update: {validate: isURL},
     },
 
-    divideTrackListsByGroupsByRef: Thing.common.referenceList(Group),
+    divideTrackListsByGroups: referenceList({
+      class: input.value(Group),
+      find: input.value(find.group),
+      data: 'groupData',
+    }),
 
     // Feature toggles
-    enableFlashesAndGames: Thing.common.flag(false),
-    enableListings: Thing.common.flag(false),
-    enableNews: Thing.common.flag(false),
-    enableArtTagUI: Thing.common.flag(false),
-    enableGroupUI: Thing.common.flag(false),
+    enableFlashesAndGames: flag(false),
+    enableListings: flag(false),
+    enableNews: flag(false),
+    enableArtTagUI: flag(false),
+    enableGroupUI: flag(false),
 
     // Update only
 
-    groupData: Thing.common.wikiData(Group),
-
-    // Expose only
-
-    divideTrackListsByGroups: Thing.common.dynamicThingsFromReferenceList(
-      'divideTrackListsByGroupsByRef',
-      'groupData',
-      find.group
-    ),
+    groupData: wikiData(Group),
   });
 }
diff --git a/src/data/yaml.js b/src/data/yaml.js
index 3594319..f7856cb 100644
--- a/src/data/yaml.js
+++ b/src/data/yaml.js
@@ -7,10 +7,15 @@ import {inspect as nodeInspect} from 'node:util';
 
 import yaml from 'js-yaml';
 
-import {color, ENABLE_COLOR, logInfo, logWarn} from '#cli';
+import {colors, ENABLE_COLOR, logInfo, logWarn} from '#cli';
 import find, {bindFind} from '#find';
 import {traverse} from '#node-utils';
-import T from '#things';
+
+import T, {
+  CacheableObject,
+  CacheableObjectPropertyValueError,
+  Thing,
+} from '#things';
 
 import {
   conditionallySuppressError,
@@ -59,7 +64,7 @@ export const DATA_STATIC_PAGE_DIRECTORY = 'static-page';
 // document and apply the configuration passed to makeProcessDocument in order
 // to construct a Thing subclass.
 function makeProcessDocument(
-  thingClass,
+  thingConstructor,
   {
     // Optional early step for transforming field values before providing them
     // to the Thing's update() method. This is useful when the input format
@@ -110,7 +115,7 @@ function makeProcessDocument(
     invalidFieldCombinations = [],
   }
 ) {
-  if (!thingClass) {
+  if (!thingConstructor) {
     throw new Error(`Missing Thing class`);
   }
 
@@ -137,22 +142,47 @@ function makeProcessDocument(
         const name = document[nameField];
         error.message = name
           ? `(name: ${inspect(name)}) ${error.message}`
-          : `(${color.dim(`no name found`)}) ${error.message}`;
+          : `(${colors.dim(`no name found`)}) ${error.message}`;
         throw error;
       }
     };
   };
 
   const fn = decorateErrorWithName((document) => {
+    const nameField = propertyFieldMapping['name'];
+    const namePart =
+      (nameField
+        ? (document[nameField]
+          ? ` named ${colors.green(`"${document[nameField]}"`)}`
+          : ` (name field, "${nameField}", not specified)`)
+        : ``);
+
+    const constructorPart =
+      (thingConstructor[Thing.friendlyName]
+        ? colors.green(thingConstructor[Thing.friendlyName])
+     : thingConstructor.name
+        ? colors.green(thingConstructor.name)
+        : `document`);
+
+    const aggregate = openAggregate({
+      message: `Errors processing ${constructorPart}` + namePart,
+    });
+
     const documentEntries = Object.entries(document)
       .filter(([field]) => !ignoredFields.includes(field));
 
+    const skippedFields = new Set();
+
     const unknownFields = documentEntries
       .map(([field]) => field)
       .filter((field) => !knownFields.includes(field));
 
     if (!empty(unknownFields)) {
-      throw new makeProcessDocument.UnknownFieldsError(unknownFields);
+      aggregate.push(new UnknownFieldsError(unknownFields));
+
+      for (const field of unknownFields) {
+        skippedFields.add(field);
+      }
     }
 
     const presentFields = Object.keys(document);
@@ -162,28 +192,57 @@ function makeProcessDocument(
     for (const {message, fields} of invalidFieldCombinations) {
       const fieldsPresent = presentFields.filter(field => fields.includes(field));
 
-      if (fieldsPresent.length <= 1) {
-        continue;
-      }
+      if (fieldsPresent.length >= 2) {
+        const filteredDocument =
+          filterProperties(
+            document,
+            fieldsPresent,
+            {preserveOriginalOrder: true});
 
-      fieldCombinationErrors.push(
-        new makeProcessDocument.FieldCombinationError(
-          filterProperties(document, fieldsPresent),
-          message));
+        fieldCombinationErrors.push(new FieldCombinationError(filteredDocument, message));
+
+        for (const field of Object.keys(filteredDocument)) {
+          skippedFields.add(field);
+        }
+      }
     }
 
     if (!empty(fieldCombinationErrors)) {
-      throw new makeProcessDocument.FieldCombinationsError(fieldCombinationErrors);
+      aggregate.push(new FieldCombinationAggregateError(fieldCombinationErrors));
     }
 
     const fieldValues = {};
 
-    for (const [field, value] of documentEntries) {
-      if (Object.hasOwn(fieldTransformations, field)) {
-        fieldValues[field] = fieldTransformations[field](value);
-      } else {
-        fieldValues[field] = value;
+    for (const [field, documentValue] of documentEntries) {
+      if (skippedFields.has(field)) continue;
+
+      // This variable would like to certify itself as "not into capitalism".
+      let propertyValue =
+        (Object.hasOwn(fieldTransformations, field)
+          ? fieldTransformations[field](documentValue)
+          : documentValue);
+
+      // Completely blank items in a YAML list are read as null.
+      // They're handy to have around when filling out a document and shouldn't
+      // be considered an error (or data at all).
+      if (Array.isArray(propertyValue)) {
+        const wasEmpty = empty(propertyValue);
+
+        propertyValue =
+          propertyValue.filter(item => item !== null);
+
+        const isEmpty = empty(propertyValue);
+
+        // Don't set arrays which are empty as a result of the above filter.
+        // Arrays which were originally empty, i.e. `Field: []`, are still
+        // valid data, but if it's just an array not containing any filled out
+        // items, it should be treated as a placeholder and skipped over.
+        if (isEmpty && !wasEmpty) {
+          propertyValue = null;
+        }
       }
+
+      fieldValues[field] = propertyValue;
     }
 
     const sourceProperties = {};
@@ -193,15 +252,34 @@ function makeProcessDocument(
       sourceProperties[property] = value;
     }
 
-    const thing = Reflect.construct(thingClass, []);
+    const thing = Reflect.construct(thingConstructor, []);
 
-    withAggregate({message: `Errors applying ${color.green(thingClass.name)} properties`}, ({call}) => {
-      for (const [property, value] of Object.entries(sourceProperties)) {
-        call(() => (thing[property] = value));
+    const fieldValueErrors = [];
+
+    for (const [property, value] of Object.entries(sourceProperties)) {
+      const field = propertyFieldMapping[property];
+      try {
+        thing[property] = value;
+      } catch (caughtError) {
+        skippedFields.add(field);
+        fieldValueErrors.push(new FieldValueError(field, property, value, caughtError));
       }
-    });
+    }
+
+    if (!empty(fieldValueErrors)) {
+      aggregate.push(new FieldValueAggregateError(thingConstructor, fieldValueErrors));
+    }
 
-    return thing;
+    if (skippedFields.size >= 1) {
+      aggregate.push(
+        new SkippedFieldsSummaryError(
+          filterProperties(
+            document,
+            Array.from(skippedFields),
+            {preserveOriginalOrder: true})));
+    }
+
+    return {thing, aggregate};
   });
 
   Object.assign(fn, {
@@ -212,36 +290,81 @@ function makeProcessDocument(
   return fn;
 }
 
-makeProcessDocument.UnknownFieldsError = class UnknownFieldsError extends Error {
+export class UnknownFieldsError extends Error {
   constructor(fields) {
-    super(`Unknown fields present: ${fields.join(', ')}`);
+    super(`Unknown fields ignored: ${fields.map(field => colors.red(field)).join(', ')}`);
     this.fields = fields;
   }
-};
+}
 
-makeProcessDocument.FieldCombinationsError = class FieldCombinationsError extends AggregateError {
+export class FieldCombinationAggregateError extends AggregateError {
   constructor(errors) {
-    super(errors, `Errors in combinations of fields present`);
+    super(errors, `Invalid field combinations - all involved fields ignored`);
   }
-};
+}
 
-makeProcessDocument.FieldCombinationError = class FieldCombinationError extends Error {
+export class FieldCombinationError extends Error {
   constructor(fields, message) {
     const fieldNames = Object.keys(fields);
-    const combinePart = `Don't combine ${fieldNames.map(field => color.red(field)).join(', ')}`;
 
-    const messagePart =
+    const mainMessage = `Don't combine ${fieldNames.map(field => colors.red(field)).join(', ')}`;
+
+    const causeMessage =
       (typeof message === 'function'
-        ? `: ${message(fields)}`
+        ? message(fields)
      : typeof message === 'string'
-        ? `: ${message}`
-        : ``);
+        ? message
+        : null);
+
+    super(mainMessage, {
+      cause:
+        (causeMessage
+          ? new Error(causeMessage)
+          : null),
+    });
 
-    super(combinePart + messagePart);
     this.fields = fields;
   }
 }
 
+export class FieldValueAggregateError extends AggregateError {
+  constructor(thingConstructor, errors) {
+    super(errors, `Errors processing field values for ${colors.green(thingConstructor.name)}`);
+  }
+}
+
+export class FieldValueError extends Error {
+  constructor(field, property, value, caughtError) {
+    const cause =
+      (caughtError instanceof CacheableObjectPropertyValueError
+        ? caughtError.cause
+        : caughtError);
+
+    super(
+      `Failed to set ${colors.green(`"${field}"`)} field (${colors.green(property)}) to ${inspect(value)}`,
+      {cause});
+  }
+}
+
+export class SkippedFieldsSummaryError extends Error {
+  constructor(filteredDocument) {
+    const entries = Object.entries(filteredDocument);
+
+    const lines =
+      entries.map(([field, value]) =>
+        ` - ${field}: ` +
+        inspect(value)
+          .split('\n')
+          .map((line, index) => index === 0 ? line : `   ${line}`)
+          .join('\n'));
+
+    super(
+      colors.bright(colors.yellow(`Altogether, skipped ${entries.length === 1 ? `1 field` : `${entries.length} fields`}:\n`)) +
+      lines.join('\n') + '\n' +
+      colors.bright(colors.yellow(`See above errors for details.`)));
+  }
+}
+
 export const processAlbumDocument = makeProcessDocument(T.Album, {
   fieldTransformations: {
     'Artists': parseContributors,
@@ -278,11 +401,11 @@ export const processAlbumDocument = makeProcessDocument(T.Album, {
     coverArtFileExtension: 'Cover Art File Extension',
     trackCoverArtFileExtension: 'Track Art File Extension',
 
-    wallpaperArtistContribsByRef: 'Wallpaper Artists',
+    wallpaperArtistContribs: 'Wallpaper Artists',
     wallpaperStyle: 'Wallpaper Style',
     wallpaperFileExtension: 'Wallpaper File Extension',
 
-    bannerArtistContribsByRef: 'Banner Artists',
+    bannerArtistContribs: 'Banner Artists',
     bannerStyle: 'Banner Style',
     bannerFileExtension: 'Banner File Extension',
     bannerDimensions: 'Banner Dimensions',
@@ -290,11 +413,11 @@ export const processAlbumDocument = makeProcessDocument(T.Album, {
     commentary: 'Commentary',
     additionalFiles: 'Additional Files',
 
-    artistContribsByRef: 'Artists',
-    coverArtistContribsByRef: 'Cover Artists',
-    trackCoverArtistContribsByRef: 'Default Track Cover Artists',
-    groupsByRef: 'Groups',
-    artTagsByRef: 'Art Tags',
+    artistContribs: 'Artists',
+    coverArtistContribs: 'Cover Artists',
+    trackCoverArtistContribs: 'Default Track Cover Artists',
+    groups: 'Groups',
+    artTags: 'Art Tags',
   },
 });
 
@@ -316,6 +439,10 @@ export const processTrackDocument = makeProcessDocument(T.Track, {
 
     'Date First Released': (value) => new Date(value),
     'Cover Art Date': (value) => new Date(value),
+    'Has Cover Art': (value) =>
+      (value === true ? false :
+       value === false ? true :
+       value),
 
     'Artists': parseContributors,
     'Contributors': parseContributors,
@@ -336,7 +463,9 @@ export const processTrackDocument = makeProcessDocument(T.Track, {
     dateFirstReleased: 'Date First Released',
     coverArtDate: 'Cover Art Date',
     coverArtFileExtension: 'Cover Art File Extension',
-    hasCoverArt: 'Has Cover Art',
+    disableUniqueCoverArt: 'Has Cover Art', // This gets transformed to flip true/false.
+
+    alwaysReferenceByDirectory: 'Always Reference By Directory',
 
     lyrics: 'Lyrics',
     commentary: 'Commentary',
@@ -344,13 +473,13 @@ export const processTrackDocument = makeProcessDocument(T.Track, {
     sheetMusicFiles: 'Sheet Music Files',
     midiProjectFiles: 'MIDI Project Files',
 
-    originalReleaseTrackByRef: 'Originally Released As',
-    referencedTracksByRef: 'Referenced Tracks',
-    sampledTracksByRef: 'Sampled Tracks',
-    artistContribsByRef: 'Artists',
-    contributorContribsByRef: 'Contributors',
-    coverArtistContribsByRef: 'Cover Artists',
-    artTagsByRef: 'Art Tags',
+    originalReleaseTrack: 'Originally Released As',
+    referencedTracks: 'Referenced Tracks',
+    sampledTracks: 'Sampled Tracks',
+    artistContribs: 'Artists',
+    contributorContribs: 'Contributors',
+    coverArtistContribs: 'Cover Artists',
+    artTags: 'Art Tags',
   },
 
   invalidFieldCombinations: [
@@ -415,21 +544,25 @@ export const processFlashDocument = makeProcessDocument(T.Flash, {
     name: 'Flash',
     directory: 'Directory',
     page: 'Page',
+    color: 'Color',
     urls: 'URLs',
 
     date: 'Date',
     coverArtFileExtension: 'Cover Art File Extension',
 
-    featuredTracksByRef: 'Featured Tracks',
-    contributorContribsByRef: 'Contributors',
+    featuredTracks: 'Featured Tracks',
+    contributorContribs: 'Contributors',
   },
 });
 
 export const processFlashActDocument = makeProcessDocument(T.FlashAct, {
   propertyFieldMapping: {
     name: 'Act',
+    directory: 'Directory',
+
     color: 'Color',
-    anchor: 'Anchor',
+    listTerminology: 'List Terminology',
+
     jump: 'Jump',
     jumpColor: 'Jump Color',
   },
@@ -466,7 +599,7 @@ export const processGroupDocument = makeProcessDocument(T.Group, {
     description: 'Description',
     urls: 'URLs',
 
-    featuredAlbumsByRef: 'Featured Albums',
+    featuredAlbums: 'Featured Albums',
   },
 });
 
@@ -497,7 +630,7 @@ export const processWikiInfoDocument = makeProcessDocument(T.WikiInfo, {
     footerContent: 'Footer Content',
     defaultLanguage: 'Default Language',
     canonicalBase: 'Canonical Base',
-    divideTrackListsByGroupsByRef: 'Divide Track Lists By Groups',
+    divideTrackListsByGroups: 'Divide Track Lists By Groups',
     enableFlashesAndGames: 'Enable Flashes & Games',
     enableListings: 'Enable Listings',
     enableNews: 'Enable News',
@@ -532,9 +665,9 @@ export const homepageLayoutRowTypeProcessMapping = {
   albums: makeProcessHomepageLayoutRowDocument(T.HomepageLayoutAlbumsRow, {
     propertyFieldMapping: {
       displayStyle: 'Display Style',
-      sourceGroupByRef: 'Group',
+      sourceGroup: 'Group',
       countAlbumsFromGroup: 'Count',
-      sourceAlbumsByRef: 'Albums',
+      sourceAlbums: 'Albums',
       actionLinks: 'Actions',
     },
   }),
@@ -591,33 +724,17 @@ export function parseContributors(contributors) {
     return contributors;
   }
 
-  if (contributors.length === 1 && contributors[0].startsWith('<i>')) {
-    const arr = [];
-    arr.textContent = contributors[0];
-    return arr;
-  }
-
   contributors = contributors.map((contrib) => {
-    // 8asically, the format is "Who (What)", or just "Who". 8e sure to
-    // keep in mind that "what" doesn't necessarily have a value!
+    if (typeof contrib !== 'string') return contrib;
+
     const match = contrib.match(/^(.*?)( \((.*)\))?$/);
-    if (!match) {
-      return contrib;
-    }
+    if (!match) return contrib;
+
     const who = match[1];
     const what = match[3] || null;
     return {who, what};
   });
 
-  const badContributor = contributors.find((val) => typeof val === 'string');
-  if (badContributor) {
-    throw new Error(`Incorrectly formatted contribution: "${badContributor}".`);
-  }
-
-  if (contributors.length === 1 && contributors[0].who === 'none') {
-    return null;
-  }
-
   return contributors;
 }
 
@@ -767,13 +884,13 @@ export const dataSteps = [
         let currentTrackSection = {
           name: `Default Track Section`,
           isDefaultTrackSection: true,
-          tracksByRef: [],
+          tracks: [],
         };
 
-        const albumRef = T.Thing.getReference(album);
+        const albumRef = Thing.getReference(album);
 
         const closeCurrentTrackSection = () => {
-          if (!empty(currentTrackSection.tracksByRef)) {
+          if (!empty(currentTrackSection.tracks)) {
             trackSections.push(currentTrackSection);
           }
         };
@@ -787,7 +904,7 @@ export const dataSteps = [
               color: entry.color,
               dateOriginallyReleased: entry.dateOriginallyReleased,
               isDefaultTrackSection: false,
-              tracksByRef: [],
+              tracks: [],
             };
 
             continue;
@@ -795,9 +912,9 @@ export const dataSteps = [
 
           trackData.push(entry);
 
-          entry.dataSourceAlbumByRef = albumRef;
+          entry.dataSourceAlbum = albumRef;
 
-          currentTrackSection.tracksByRef.push(T.Thing.getReference(entry));
+          currentTrackSection.tracks.push(Thing.getReference(entry));
         }
 
         closeCurrentTrackSection();
@@ -821,12 +938,12 @@ export const dataSteps = [
       const artistData = results;
 
       const artistAliasData = results.flatMap((artist) => {
-        const origRef = T.Thing.getReference(artist);
+        const origRef = Thing.getReference(artist);
         return artist.aliasNames?.map((name) => {
           const alias = new T.Artist();
           alias.name = name;
           alias.isAlias = true;
-          alias.aliasedArtistRef = origRef;
+          alias.aliasedArtist = origRef;
           alias.artistData = artistData;
           return alias;
         }) ?? [];
@@ -850,7 +967,7 @@ export const dataSteps = [
 
     save(results) {
       let flashAct;
-      let flashesByRef = [];
+      let flashRefs = [];
 
       if (results[0] && !(results[0] instanceof T.FlashAct)) {
         throw new Error(`Expected an act at top of flash data file`);
@@ -859,18 +976,18 @@ export const dataSteps = [
       for (const thing of results) {
         if (thing instanceof T.FlashAct) {
           if (flashAct) {
-            Object.assign(flashAct, {flashesByRef});
+            Object.assign(flashAct, {flashes: flashRefs});
           }
 
           flashAct = thing;
-          flashesByRef = [];
+          flashRefs = [];
         } else {
-          flashesByRef.push(T.Thing.getReference(thing));
+          flashRefs.push(Thing.getReference(thing));
         }
       }
 
       if (flashAct) {
-        Object.assign(flashAct, {flashesByRef});
+        Object.assign(flashAct, {flashes: flashRefs});
       }
 
       const flashData = results.filter((x) => x instanceof T.Flash);
@@ -893,7 +1010,7 @@ export const dataSteps = [
 
     save(results) {
       let groupCategory;
-      let groupsByRef = [];
+      let groupRefs = [];
 
       if (results[0] && !(results[0] instanceof T.GroupCategory)) {
         throw new Error(`Expected a category at top of group data file`);
@@ -902,18 +1019,18 @@ export const dataSteps = [
       for (const thing of results) {
         if (thing instanceof T.GroupCategory) {
           if (groupCategory) {
-            Object.assign(groupCategory, {groupsByRef});
+            Object.assign(groupCategory, {groups: groupRefs});
           }
 
           groupCategory = thing;
-          groupsByRef = [];
+          groupRefs = [];
         } else {
-          groupsByRef.push(T.Thing.getReference(thing));
+          groupRefs.push(Thing.getReference(thing));
         }
       }
 
       if (groupCategory) {
-        Object.assign(groupCategory, {groupsByRef});
+        Object.assign(groupCategory, {groups: groupRefs});
       }
 
       const groupData = results.filter((x) => x instanceof T.Group);
@@ -925,6 +1042,10 @@ export const dataSteps = [
 
   {
     title: `Process homepage layout file`,
+
+    // Kludge: This benefits from the same headerAndEntries style messaging as
+    // albums and tracks (for example), but that document mode is designed to
+    // support multiple files, and only one is actually getting processed here.
     files: [HOMEPAGE_LAYOUT_DATA_FILE],
 
     documentMode: documentModes.headerAndEntries,
@@ -1005,7 +1126,7 @@ export async function loadAndProcessDataDocuments({dataPath}) {
       } catch (error) {
         error.message +=
           (error.message.includes('\n') ? '\n' : ' ') +
-          `(file: ${color.bright(color.blue(path.relative(dataPath, x.file)))})`;
+          `(file: ${colors.bright(colors.blue(path.relative(dataPath, x.file)))})`;
         throw error;
       }
     };
@@ -1013,8 +1134,8 @@ export async function loadAndProcessDataDocuments({dataPath}) {
 
   for (const dataStep of dataSteps) {
     await processDataAggregate.nestAsync(
-      {message: `Errors during data step: ${dataStep.title}`},
-      async ({call, callAsync, map, mapAsync, nest}) => {
+      {message: `Errors during data step: ${colors.bright(dataStep.title)}`},
+      async ({call, callAsync, map, mapAsync, push, nest}) => {
         const {documentMode} = dataStep;
 
         if (!Object.values(documentModes).includes(documentMode)) {
@@ -1028,7 +1149,7 @@ export async function loadAndProcessDataDocuments({dataPath}) {
         // just without the callbacks. Thank you.
         const filterBlankDocuments = documents => {
           const aggregate = openAggregate({
-            message: `Found blank documents - check for extra '${color.cyan(`---`)}'`,
+            message: `Found blank documents - check for extra '${colors.cyan(`---`)}'`,
           });
 
           const filteredDocuments =
@@ -1072,10 +1193,10 @@ export async function loadAndProcessDataDocuments({dataPath}) {
 
               if (count === 1) {
                 const range = `#${start + 1}`;
-                parts.push(`${count} document (${color.yellow(range)}), `);
+                parts.push(`${count} document (${colors.yellow(range)}), `);
               } else {
                 const range = `#${start + 1}-${end + 1}`;
-                parts.push(`${count} documents (${color.yellow(range)}), `);
+                parts.push(`${count} documents (${colors.yellow(range)}), `);
               }
 
               if (previous === null) {
@@ -1085,7 +1206,7 @@ export async function loadAndProcessDataDocuments({dataPath}) {
               } else {
                 const previousDescription = Object.entries(previous).at(0).join(': ');
                 const nextDescription = Object.entries(next).at(0).join(': ');
-                parts.push(`between "${color.cyan(previousDescription)}" and "${color.cyan(nextDescription)}"`);
+                parts.push(`between "${colors.cyan(previousDescription)}" and "${colors.cyan(nextDescription)}"`);
               }
 
               aggregate.push(new Error(parts.join('')));
@@ -1139,32 +1260,52 @@ export async function loadAndProcessDataDocuments({dataPath}) {
             return;
           }
 
-          const yamlResult =
-            documentMode === documentModes.oneDocumentTotal
-              ? call(yaml.load, readResult)
-              : call(yaml.loadAll, readResult);
+          let processResults;
 
-          if (!yamlResult) {
-            return;
-          }
+          switch (documentMode) {
+            case documentModes.oneDocumentTotal: {
+              const yamlResult = call(yaml.load, readResult);
 
-          let processResults;
+              if (!yamlResult) {
+                processResults = null;
+                break;
+              }
+
+              const {thing, aggregate} =
+                dataStep.processDocument(yamlResult);
+
+              processResults = thing;
+
+              call(() => aggregate.close());
+
+              break;
+            }
+
+            case documentModes.allInOne: {
+              const yamlResults = call(yaml.loadAll, readResult);
+
+              if (!yamlResults) {
+                processResults = [];
+                return;
+              }
+
+              const {documents, aggregate: filterAggregate} =
+                filterBlankDocuments(yamlResults);
+
+              call(filterAggregate.close);
+
+              processResults = [];
 
-          if (documentMode === documentModes.oneDocumentTotal) {
-            nest({message: `Errors processing document`}, ({call}) => {
-              processResults = call(dataStep.processDocument, yamlResult);
-            });
-          } else {
-            const {documents, aggregate: aggregate1} = filterBlankDocuments(yamlResult);
-            call(aggregate1.close);
-
-            const {result, aggregate: aggregate2} = mapAggregate(
-              documents,
-              decorateErrorWithIndex(dataStep.processDocument),
-              {message: `Errors processing documents`});
-            call(aggregate2.close);
-
-            processResults = result;
+              map(documents, decorateErrorWithIndex(document => {
+                const {thing, aggregate} =
+                  dataStep.processDocument(document);
+
+                processResults.push(thing);
+                aggregate.close();
+              }), {message: `Errors processing documents`});
+
+              break;
+            }
           }
 
           if (!processResults) return;
@@ -1222,81 +1363,74 @@ export async function loadAndProcessDataDocuments({dataPath}) {
           return {file, documents: filteredDocuments};
         });
 
-        let processResults;
+        const processResults = [];
 
-        if (documentMode === documentModes.headerAndEntries) {
-          nest({message: `Errors processing data files as valid documents`}, ({call, map}) => {
-            processResults = [];
+        switch (documentMode) {
+          case documentModes.headerAndEntries:
+            map(yamlResults, decorateErrorWithFile(({documents}) => {
+              const headerDocument = documents[0];
+              const entryDocuments = documents.slice(1).filter(Boolean);
 
-            yamlResults.forEach(({file, documents}) => {
-              const [headerDocument, ...entryDocuments] = documents;
+              if (!headerDocument)
+                throw new Error(`Missing header document (empty file or erroneously starting with "---"?)`);
 
-              if (!headerDocument) {
-                call(decorateErrorWithFile(() => {
-                  throw new Error(`Missing header document (empty file or erroneously starting with "---"?)`);
-                }), {file});
-                return;
-              }
+              // This'll be decorated with the file, and groups together any
+              // errors from processing the header and entry documents.
+              const fileAggregate =
+                openAggregate({message: `Errors processing documents`});
 
-              const header = call(
-                decorateErrorWithFile(({document}) =>
-                  dataStep.processHeaderDocument(document)),
-                {file, document: headerDocument});
+              const {thing: headerObject, aggregate: headerAggregate} =
+                dataStep.processHeaderDocument(headerDocument);
 
-              // Don't continue processing files whose header
-              // document is invalid - the entire file is excempt
-              // from data in this case.
-              if (!header) {
-                return;
+              try {
+                headerAggregate.close()
+              } catch (caughtError) {
+                caughtError.message = `(${colors.yellow(`header`)}) ${caughtError.message}`;
+                fileAggregate.push(caughtError);
               }
 
-              const entries = map(
-                entryDocuments
-                  .filter(Boolean)
-                  .map((document) => ({file, document})),
-                decorateErrorWithFile(
-                  decorateErrorWithIndex(({document}) =>
-                    dataStep.processEntryDocument(document))),
-                {message: `Errors processing entry documents`});
-
-              // Entries may be incomplete (i.e. any errored
-              // documents won't have a processed output
-              // represented here) - this is intentional! By
-              // principle, partial output is preferred over
-              // erroring an entire file.
-              processResults.push({header, entries});
-            });
-          });
-        }
+              const entryObjects = [];
 
-        if (documentMode === documentModes.onePerFile) {
-          nest({message: `Errors processing data files as valid documents`}, ({call}) => {
-            processResults = [];
+              for (let index = 0; index < entryDocuments.length; index++) {
+                const entryDocument = entryDocuments[index];
 
-            yamlResults.forEach(({file, documents}) => {
-              if (documents.length > 1) {
-                call(decorateErrorWithFile(() => {
-                  throw new Error(`Only expected one document to be present per file`);
-                }), {file});
-                return;
-              } else if (empty(documents) || !documents[0]) {
-                call(decorateErrorWithFile(() => {
-                  throw new Error(`Expected a document, this file is empty`);
-                }), {file});
-              }
+                const {thing: entryObject, aggregate: entryAggregate} =
+                  dataStep.processEntryDocument(entryDocument);
 
-              const result = call(
-                decorateErrorWithFile(({document}) =>
-                  dataStep.processDocument(document)),
-                {file, document: documents[0]});
+                entryObjects.push(entryObject);
 
-              if (!result) {
-                return;
+                try {
+                  entryAggregate.close();
+                } catch (caughtError) {
+                  caughtError.message = `(${colors.yellow(`entry #${index + 1}`)}) ${caughtError.message}`;
+                  fileAggregate.push(caughtError);
+                }
               }
 
-              processResults.push(result);
-            });
-          });
+              processResults.push({
+                header: headerObject,
+                entries: entryObjects,
+              });
+
+              fileAggregate.close();
+            }), {message: `Errors processing documents in data files`});
+            break;
+
+          case documentModes.onePerFile:
+            map(yamlResults, decorateErrorWithFile(({documents}) => {
+              if (documents.length > 1)
+                throw new Error(`Only expected one document to be present per file, got ${documents.length} here`);
+
+              if (empty(documents) || !documents[0])
+                throw new Error(`Expected a document, this file is empty`);
+
+              const {thing, aggregate} =
+                dataStep.processDocument(documents[0]);
+
+              processResults.push(thing);
+              aggregate.close();
+            }), {message: `Errors processing data files as valid documents`});
+            break;
         }
 
         const saveResult = call(dataStep.save, processResults);
@@ -1316,13 +1450,27 @@ export async function loadAndProcessDataDocuments({dataPath}) {
 
 // Data linking! Basically, provide (portions of) wikiData to the Things which
 // require it - they'll expose dynamically computed properties as a result (many
-// of which are required for page HTML generation).
-export function linkWikiDataArrays(wikiData) {
+// of which are required for page HTML generation and other expected behavior).
+//
+// The XXX_decacheWikiData option should be used specifically to mark
+// points where you *aren't* replacing any of the arrays under wikiData with
+// new values, and are using linkWikiDataArrays to instead "decache" data
+// properties which depend on any of them. It's currently not possible for
+// a CacheableObject to depend directly on the value of a property exposed
+// on some other CacheableObject, so when those values change, you have to
+// manually decache before the object will realize its cache isn't valid
+// anymore.
+export function linkWikiDataArrays(wikiData, {
+  XXX_decacheWikiData = false,
+} = {}) {
   function assignWikiData(things, ...keys) {
+    if (things === undefined) return;
     for (let i = 0; i < things.length; i++) {
       const thing = things[i];
       for (let j = 0; j < keys.length; j++) {
         const key = keys[j];
+        if (!(key in wikiData)) continue;
+        if (XXX_decacheWikiData) thing[key] = [];
         thing[key] = wikiData[key];
       }
     }
@@ -1340,7 +1488,7 @@ export function linkWikiDataArrays(wikiData) {
   assignWikiData(WD.flashData, 'artistData', 'flashActData', 'trackData');
   assignWikiData(WD.flashActData, 'flashData');
   assignWikiData(WD.artTagData, 'albumData', 'trackData');
-  assignWikiData(WD.homepageLayout.rows, 'albumData', 'groupData');
+  assignWikiData(WD.homepageLayout?.rows, 'albumData', 'groupData');
 }
 
 export function sortWikiDataArrays(wikiData) {
@@ -1368,7 +1516,9 @@ export function filterDuplicateDirectories(wikiData) {
   const deduplicateSpec = [
     'albumData',
     'artTagData',
+    'artistData',
     'flashData',
+    'flashActData',
     'groupData',
     'newsData',
     'trackData',
@@ -1377,7 +1527,7 @@ export function filterDuplicateDirectories(wikiData) {
   const aggregate = openAggregate({message: `Duplicate directories found`});
   for (const thingDataProp of deduplicateSpec) {
     const thingData = wikiData[thingDataProp];
-    aggregate.nest({message: `Duplicate directories found in ${color.green('wikiData.' + thingDataProp)}`}, ({call}) => {
+    aggregate.nest({message: `Duplicate directories found in ${colors.green('wikiData.' + thingDataProp)}`}, ({call}) => {
       const directoryPlaces = Object.create(null);
       const duplicateDirectories = [];
 
@@ -1403,7 +1553,7 @@ export function filterDuplicateDirectories(wikiData) {
         const places = directoryPlaces[directory];
         call(() => {
           throw new Error(
-            `Duplicate directory ${color.green(directory)}:\n` +
+            `Duplicate directory ${colors.green(directory)}:\n` +
               places.map((thing) => ` - ` + inspect(thing)).join('\n')
           );
         });
@@ -1444,45 +1594,45 @@ export function filterDuplicateDirectories(wikiData) {
 export function filterReferenceErrors(wikiData) {
   const referenceSpec = [
     ['wikiInfo', processWikiInfoDocument, {
-      divideTrackListsByGroupsByRef: 'group',
+      divideTrackListsByGroups: 'group',
     }],
 
     ['albumData', processAlbumDocument, {
-      artistContribsByRef: '_contrib',
-      coverArtistContribsByRef: '_contrib',
-      trackCoverArtistContribsByRef: '_contrib',
-      wallpaperArtistContribsByRef: '_contrib',
-      bannerArtistContribsByRef: '_contrib',
-      groupsByRef: 'group',
-      artTagsByRef: 'artTag',
+      artistContribs: '_contrib',
+      coverArtistContribs: '_contrib',
+      trackCoverArtistContribs: '_contrib',
+      wallpaperArtistContribs: '_contrib',
+      bannerArtistContribs: '_contrib',
+      groups: 'group',
+      artTags: 'artTag',
     }],
 
     ['trackData', processTrackDocument, {
-      artistContribsByRef: '_contrib',
-      contributorContribsByRef: '_contrib',
-      coverArtistContribsByRef: '_contrib',
-      referencedTracksByRef: '_trackNotRerelease',
-      sampledTracksByRef: '_trackNotRerelease',
-      artTagsByRef: 'artTag',
-      originalReleaseTrackByRef: '_trackNotRerelease',
+      artistContribs: '_contrib',
+      contributorContribs: '_contrib',
+      coverArtistContribs: '_contrib',
+      referencedTracks: '_trackNotRerelease',
+      sampledTracks: '_trackNotRerelease',
+      artTags: 'artTag',
+      originalReleaseTrack: '_trackNotRerelease',
     }],
 
     ['groupCategoryData', processGroupCategoryDocument, {
-      groupsByRef: 'group',
+      groups: 'group',
     }],
 
     ['homepageLayout.rows', undefined, {
-      sourceGroupByRef: 'group',
-      sourceAlbumsByRef: 'album',
+      sourceGroup: '_homepageSourceGroup',
+      sourceAlbums: 'album',
     }],
 
     ['flashData', processFlashDocument, {
-      contributorContribsByRef: '_contrib',
-      featuredTracksByRef: 'track',
+      contributorContribs: '_contrib',
+      featuredTracks: 'track',
     }],
 
     ['flashActData', processFlashActDocument, {
-      flashesByRef: 'flash',
+      flashes: 'flash',
     }],
   ];
 
@@ -1498,7 +1648,7 @@ export function filterReferenceErrors(wikiData) {
   for (const [thingDataProp, providedProcessDocumentFn, propSpec] of referenceSpec) {
     const thingData = getNestedProp(wikiData, thingDataProp);
 
-    aggregate.nest({message: `Reference errors in ${color.green('wikiData.' + thingDataProp)}`}, ({nest}) => {
+    aggregate.nest({message: `Reference errors in ${colors.green('wikiData.' + thingDataProp)}`}, ({nest}) => {
       const things = Array.isArray(thingData) ? thingData : [thingData];
 
       for (const thing of things) {
@@ -1514,10 +1664,10 @@ export function filterReferenceErrors(wikiData) {
 
         nest({message: `Reference errors in ${inspect(thing)}`}, ({push, filter}) => {
           for (const [property, findFnKey] of Object.entries(propSpec)) {
-            const value = thing[property];
+            const value = CacheableObject.getUpdateValue(thing, property);
 
             if (value === undefined) {
-              push(new TypeError(`Property ${color.red(property)} isn't valid for ${color.green(thing.constructor.name)}`));
+              push(new TypeError(`Property ${colors.red(property)} isn't valid for ${colors.green(thing.constructor.name)}`));
               continue;
             }
 
@@ -1534,23 +1684,34 @@ export function filterReferenceErrors(wikiData) {
                   if (alias) {
                     // No need to check if the original exists here. Aliases are automatically
                     // created from a field on the original, so the original certainly exists.
-                    const original = find.artist(alias.aliasedArtistRef, wikiData.artistData, {mode: 'quiet'});
-                    throw new Error(`Reference ${color.red(contribRef.who)} is to an alias, should be ${color.green(original.name)}`);
+                    const original = alias.aliasedArtist;
+                    throw new Error(`Reference ${colors.red(contribRef.who)} is to an alias, should be ${colors.green(original.name)}`);
                   }
 
                   return boundFind.artist(contribRef.who);
                 };
                 break;
 
+              case '_homepageSourceGroup':
+                findFn = groupRef => {
+                  if (groupRef === 'new-additions' || groupRef === 'new-releases') {
+                    return true;
+                  }
+
+                  return boundFind.group(groupRef);
+                };
+                break;
+
               case '_trackNotRerelease':
                 findFn = trackRef => {
                   const track = find.track(trackRef, wikiData.trackData, {mode: 'error'});
+                  const originalRef = track && CacheableObject.getUpdateValue(track, 'originalReleaseTrack');
 
-                  if (track?.originalReleaseTrackByRef) {
+                  if (originalRef) {
                     // It's possible for the original to not actually exist, in this case.
                     // It should still be reported since the 'Originally Released As' field
                     // was present.
-                    const original = find.track(track.originalReleaseTrackByRef, wikiData.trackData, {mode: 'quiet'});
+                    const original = find.track(originalRef, wikiData.trackData, {mode: 'quiet'});
 
                     // Prefer references by name, but only if it's unambiguous.
                     const originalByName =
@@ -1560,12 +1721,12 @@ export function filterReferenceErrors(wikiData) {
 
                     const shouldBeMessage =
                       (originalByName
-                        ? color.green(original.name)
+                        ? colors.green(original.name)
                      : original
-                        ? color.green('track:' + original.directory)
-                        : color.green(track.originalReleaseTrackByRef));
+                        ? colors.green('track:' + original.directory)
+                        : colors.green(originalRef));
 
-                    throw new Error(`Reference ${color.red(trackRef)} is to a rerelease, should be ${shouldBeMessage}`);
+                    throw new Error(`Reference ${colors.red(trackRef)} is to a rerelease, should be ${shouldBeMessage}`);
                   }
 
                   return track;
@@ -1578,7 +1739,7 @@ export function filterReferenceErrors(wikiData) {
             }
 
             const suppress = fn => conditionallySuppressError(error => {
-              if (property === 'sampledTracksByRef') {
+              if (property === 'sampledTracks') {
                 // Suppress "didn't match anything" errors in particular, just for samples.
                 // In hsmusic-data we have a lot of "stub" sample data which don't have
                 // corresponding tracks yet, so it won't be useful to report such reference
@@ -1596,13 +1757,13 @@ export function filterReferenceErrors(wikiData) {
 
             const fieldPropertyMessage =
               (processDocumentFn?.propertyFieldMapping?.[property]
-                ? ` in field ${color.green(processDocumentFn.propertyFieldMapping[property])}`
-                : ` in property ${color.green(property)}`);
+                ? ` in field ${colors.green(processDocumentFn.propertyFieldMapping[property])}`
+                : ` in property ${colors.green(property)}`);
 
             const findFnMessage =
               (findFnKey.startsWith('_')
                 ? ``
-                : ` (${color.green('find.' + findFnKey)})`);
+                : ` (${colors.green('find.' + findFnKey)})`);
 
             const errorMessage =
               (Array.isArray(value)
diff --git a/src/file-size-preloader.js b/src/file-size-preloader.js
index 38e60e6..4eadde7 100644
--- a/src/file-size-preloader.js
+++ b/src/file-size-preloader.js
@@ -29,6 +29,8 @@ export default class FileSizePreloader {
   #loadingPromise = null;
   #resolveLoadingPromise = null;
 
+  hadErrored = false;
+
   loadPaths(...paths) {
     this.#paths.push(...paths.filter((p) => !this.#paths.includes(p)));
     return this.#startLoadingPaths();
@@ -67,6 +69,7 @@ export default class FileSizePreloader {
       // Oops! Discard that path, and don't increment the index before
       // moving on, since the next path will now be in its place.
       this.#paths.splice(this.#loadedPathIndex + 1, 1);
+      this.hasErrored = true;
       logWarn`Failed to process file size for ${path}: ${error.message}`;
       return this.#loadNextPath();
     }
diff --git a/src/find.js b/src/find.js
index b823080..8c9413b 100644
--- a/src/find.js
+++ b/src/find.js
@@ -1,6 +1,7 @@
 import {inspect} from 'node:util';
 
-import {color, logWarn} from '#cli';
+import {colors, logWarn} from '#cli';
+import {typeAppearance} from '#sugar';
 
 function warnOrThrow(mode, message) {
   if (mode === 'error') {
@@ -14,115 +15,169 @@ function warnOrThrow(mode, message) {
   return null;
 }
 
-function findHelper(keys, findFns = {}) {
+export function processAllAvailableMatches(data, {
+  getMatchableNames = thing =>
+    (Object.hasOwn(thing, 'name')
+      ? [thing.name]
+      : []),
+} = {}) {
+  const byName = Object.create(null);
+  const byDirectory = Object.create(null);
+  const multipleNameMatches = Object.create(null);
+
+  for (const thing of data) {
+    for (const name of getMatchableNames(thing)) {
+      if (typeof name !== 'string') {
+        logWarn`Unexpected ${typeAppearance(name)} returned in names for ${inspect(thing)}`;
+        continue;
+      }
+
+      const normalizedName = name.toLowerCase();
+      if (normalizedName in byName) {
+        const alreadyMatchesByName = byName[normalizedName];
+        byName[normalizedName] = null;
+        if (normalizedName in multipleNameMatches) {
+          multipleNameMatches[normalizedName].push(thing);
+        } else {
+          multipleNameMatches[normalizedName] = [alreadyMatchesByName, thing];
+        }
+      } else {
+        byName[normalizedName] = thing;
+      }
+    }
+
+    byDirectory[thing.directory] = thing;
+  }
+
+  return {byName, byDirectory, multipleNameMatches};
+}
+
+function findHelper({
+  referenceTypes,
+
+  getMatchableNames = undefined,
+}) {
+  const keyRefRegex =
+    new RegExp(String.raw`^(?:(${referenceTypes.join('|')}):(?=\S))?(.*)$`);
+
   // Note: This cache explicitly *doesn't* support mutable data arrays. If the
   // data array is modified, make sure it's actually a new array object, not
   // the original, or the cache here will break and act as though the data
   // hasn't changed!
   const cache = new WeakMap();
 
-  const byDirectory = findFns.byDirectory || matchDirectory;
-  const byName = findFns.byName || matchName;
-
-  const keyRefRegex = new RegExp(String.raw`^(?:(${keys.join('|')}):(?=\S))?(.*)$`);
-
   // The mode argument here may be 'warn', 'error', or 'quiet'. 'error' throws
   // errors for null matches (with details about the error), while 'warn' and
   // 'quiet' both return null, with 'warn' logging details directly to the
   // console.
-  return (fullRef, data, {mode = 'warn'} = {}) => {
+  return (fullRef, data, {mode = 'warn'}) => {
     if (!fullRef) return null;
     if (typeof fullRef !== 'string') {
-      throw new Error(`Got a reference that is ${typeof fullRef}, not string: ${fullRef}`);
+      throw new TypeError(`Expected a string, got ${typeAppearance(fullRef)}`);
     }
 
     if (!data) {
-      throw new Error(`Expected data to be present`);
+      throw new TypeError(`Expected data to be present`);
     }
 
-    if (!Array.isArray(data) && data.wikiData) {
-      throw new Error(`Old {wikiData: {...}} format provided`);
-    }
+    let subcache = cache.get(data);
+    if (!subcache) {
+      subcache =
+        processAllAvailableMatches(data, {
+          getMatchableNames,
+        });
 
-    let cacheForThisData = cache.get(data);
-    const cachedValue = cacheForThisData?.[fullRef];
-    if (cachedValue) {
-      globalThis.NUM_CACHE = (globalThis.NUM_CACHE || 0) + 1;
-      return cachedValue;
-    }
-    if (!cacheForThisData) {
-      cacheForThisData = Object.create(null);
-      cache.set(data, cacheForThisData);
+      cache.set(data, subcache);
     }
 
-    const match = fullRef.match(keyRefRegex);
-    if (!match) {
-      return warnOrThrow(mode, `Malformed link reference: "${fullRef}"`);
+    const regexMatch = fullRef.match(keyRefRegex);
+    if (!regexMatch) {
+      warnOrThrow(mode, `Malformed link reference: "${fullRef}"`);
     }
 
-    const key = match[1];
-    const ref = match[2];
-
-    const found = key ? byDirectory(ref, data, mode) : byName(ref, data, mode);
-
-    if (!found) {
-      warnOrThrow(mode, `Didn't match anything for ${color.bright(fullRef)}`);
+    const typePart = regexMatch[1];
+    const refPart = regexMatch[2];
+
+    const normalizedName =
+      (typePart
+        ? null
+        : refPart.toLowerCase());
+
+    const match =
+      (typePart
+        ? subcache.byDirectory[refPart]
+        : subcache.byName[normalizedName]);
+
+    if (!match && !typePart) {
+      if (subcache.multipleNameMatches[normalizedName]) {
+        return warnOrThrow(mode,
+          `Multiple matches for reference "${fullRef}". Please resolve:\n` +
+          subcache.multipleNameMatches[normalizedName]
+            .map(match => `- ${inspect(match)}\n`)
+            .join('') +
+          `Returning null for this reference.`);
+      }
     }
 
-    cacheForThisData[fullRef] = found;
+    if (!match) {
+      warnOrThrow(mode, `Didn't match anything for ${colors.bright(fullRef)}`);
+      return null;
+    }
 
-    return found;
+    return match;
   };
 }
 
-function matchDirectory(ref, data) {
-  return data.find(({directory}) => directory === ref);
-}
-
-function matchName(ref, data, mode) {
-  const matches = data.filter(
-    ({name}) => name.toLowerCase() === ref.toLowerCase()
-  );
-
-  if (matches.length > 1) {
-    return warnOrThrow(
-      mode,
-      `Multiple matches for reference "${ref}". Please resolve:\n` +
-        matches.map((match) => `- ${inspect(match)}\n`).join('') +
-        `Returning null for this reference.`
-    );
-  }
-
-  if (matches.length === 0) {
-    return null;
-  }
-
-  const thing = matches[0];
-
-  if (ref !== thing.name) {
-    warnOrThrow(
-      mode,
-      `Bad capitalization: ${color.red(ref)} -> ${color.green(thing.name)}`
-    );
-  }
-
-  return thing;
-}
-
-function matchTagName(ref, data, quiet) {
-  return matchName(ref.startsWith('cw: ') ? ref.slice(4) : ref, data, quiet);
-}
-
 const find = {
-  album: findHelper(['album', 'album-commentary', 'album-gallery']),
-  artist: findHelper(['artist', 'artist-gallery']),
-  artTag: findHelper(['tag'], {byName: matchTagName}),
-  flash: findHelper(['flash']),
-  group: findHelper(['group', 'group-gallery']),
-  listing: findHelper(['listing']),
-  newsEntry: findHelper(['news-entry']),
-  staticPage: findHelper(['static']),
-  track: findHelper(['track']),
+  album: findHelper({
+    referenceTypes: ['album', 'album-commentary', 'album-gallery'],
+  }),
+
+  artist: findHelper({
+    referenceTypes: ['artist', 'artist-gallery'],
+  }),
+
+  artTag: findHelper({
+    referenceTypes: ['tag'],
+
+    getMatchableNames: tag =>
+      (tag.isContentWarning
+        ? [`cw: ${tag.name}`]
+        : [tag.name]),
+  }),
+
+  flash: findHelper({
+    referenceTypes: ['flash'],
+  }),
+
+  flashAct: findHelper({
+    referenceTypes: ['flash-act'],
+  }),
+
+  group: findHelper({
+    referenceTypes: ['group', 'group-gallery'],
+  }),
+
+  listing: findHelper({
+    referenceTypes: ['listing'],
+  }),
+
+  newsEntry: findHelper({
+    referenceTypes: ['news-entry'],
+  }),
+
+  staticPage: findHelper({
+    referenceTypes: ['static'],
+  }),
+
+  track: findHelper({
+    referenceTypes: ['track'],
+
+    getMatchableNames: track =>
+      (track.alwaysReferenceByDirectory
+        ? []
+        : [track.name]),
+  }),
 };
 
 export default find;
@@ -139,6 +194,7 @@ export function bindFind(wikiData, opts1) {
       artist: 'artistData',
       artTag: 'artTagData',
       flash: 'flashData',
+      flashAct: 'flashActData',
       group: 'groupData',
       listing: 'listingSpec',
       newsEntry: 'newsData',
@@ -155,7 +211,9 @@ export function bindFind(wikiData, opts1) {
                 ? findFn(ref, thingData, {...opts1, ...opts2})
                 : findFn(ref, thingData, opts1)
           : (ref, opts2) =>
-              opts2 ? findFn(ref, thingData, opts2) : findFn(ref, thingData),
+              opts2
+                ? findFn(ref, thingData, opts2)
+                : findFn(ref, thingData),
       ];
     })
   );
diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js
index e993282..3d441bc 100644
--- a/src/gen-thumbs.js
+++ b/src/gen-thumbs.js
@@ -74,7 +74,7 @@
 
 'use strict';
 
-const CACHE_FILE = 'thumbnail-cache.json';
+export const CACHE_FILE = 'thumbnail-cache.json';
 const WARNING_DELAY_TIME = 10000;
 
 const thumbnailSpec = {
@@ -91,8 +91,13 @@ import {createReadStream} from 'node:fs';
 import {readFile, stat, unlink, writeFile} from 'node:fs/promises';
 import * as path from 'node:path';
 
+import dimensionsOf from 'image-size';
+
+import {delay, empty, queue} from '#sugar';
+import {CacheableObject} from '#things';
+
 import {
-  color,
+  colors,
   fileIssue,
   logError,
   logInfo,
@@ -108,10 +113,119 @@ import {
   traverse,
 } from '#node-utils';
 
-import {delay, empty, queue} from '#sugar';
-
 export const defaultMagickThreads = 8;
 
+export function getThumbnailsAvailableForDimensions([width, height]) {
+  // This function is intended to be portable, so it can be used both for
+  // calculating which thumbnails to generate, and which ones will be ready
+  // to reference in generated code. Sizes are in array [name, size] form
+  // with larger sizes earlier in return. Keep in mind this isn't a direct
+  // 1:1 mapping with the sizes listed in the thumbnail spec, because the
+  // largest thumbnail (first in return) will be adjusted to the provided
+  // dimensions.
+
+  const {all} = getThumbnailsAvailableForDimensions;
+
+  // Find the largest size which is beneath the passed dimensions. We use the
+  // longer edge here (of width and height) so that each resulting thumbnail is
+  // fully constrained within the size*size square defined by its spec.
+  const longerEdge = Math.max(width, height);
+  const index = all.findIndex(([name, size]) => size <= longerEdge);
+
+  // Literal edge cases are handled specially. For dimensions which are bigger
+  // than the biggest thumbnail in the spec, return all possible results.
+  // These don't need any adjustments since the largest is already smaller than
+  // the provided dimensions.
+  if (index === 0) {
+    return [
+      ...all,
+    ];
+  }
+
+  // For dimensions which are smaller than the smallest thumbnail, return only
+  // the smallest, adjusted to the provided dimensions.
+  if (index === -1) {
+    const smallest = all[all.length - 1];
+    return [
+      [smallest[0], longerEdge],
+    ];
+  }
+
+  // For non-edge cases, we return the largest size below the dimensions
+  // as well as everything smaller, but also the next size larger - that way
+  // there's a size which is as big as the original, but still JPEG compressed.
+  // The size larger is adjusted to the provided dimensions to represent the
+  // actual dimensions it'll provide.
+  const larger = all[index - 1];
+  const rest = all.slice(index);
+  return [
+    [larger[0], longerEdge],
+    ...rest,
+  ];
+}
+
+getThumbnailsAvailableForDimensions.all =
+  Object.entries(thumbnailSpec)
+    .map(([name, {size}]) => [name, size])
+    .sort((a, b) => b[1] - a[1]);
+
+function getCacheEntryForMediaPath(mediaPath, cache) {
+  // Gets the cache entry for the provided image path, which should always be
+  // a forward-slashes path (i.e. suitable for display online). Since the cache
+  // file may have forward or back-slashes, this checks both.
+
+  const entryFromMediaPath = cache[mediaPath];
+  if (entryFromMediaPath) return entryFromMediaPath;
+
+  const winPath = mediaPath.split(path.posix.sep).join(path.win32.sep);
+  const entryFromWinPath = cache[winPath];
+  if (entryFromWinPath) return entryFromWinPath;
+
+  return null;
+}
+
+export function checkIfImagePathHasCachedThumbnails(mediaPath, cache) {
+  // Generic utility for checking if the thumbnail cache includes any info for
+  // the provided image path, so that the other functions don't hard-code the
+  // cache format.
+
+  return !!getCacheEntryForMediaPath(mediaPath, cache);
+}
+
+export function getDimensionsOfImagePath(mediaPath, cache) {
+  // This function is really generic. It takes the gen-thumbs image cache and
+  // returns the dimensions in that cache, so that other functions don't need
+  // to hard-code the cache format.
+
+  const cacheEntry = getCacheEntryForMediaPath(mediaPath, cache);
+
+  if (!cacheEntry) {
+    throw new Error(`Expected mediaPath to be included in cache, got ${mediaPath}`);
+  }
+
+  const [width, height] = cacheEntry.slice(1);
+  return [width, height];
+}
+
+export function getThumbnailEqualOrSmaller(preferred, mediaPath, cache) {
+  // This function is totally exclusive to page generation. It's a shorthand
+  // for accessing dimensions from the thumbnail cache, calculating all the
+  // thumbnails available, and selecting the one which is equal to or smaller
+  // than the provided size. Since the path provided might not be the actual
+  // one which is being thumbnail-ified, this just returns the name of the
+  // selected thumbnail size.
+
+  if (!getCacheEntryForMediaPath(mediaPath, cache)) {
+    throw new Error(`Expected mediaPath to be included in cache, got ${mediaPath}`);
+  }
+
+  const {size: preferredSize} = thumbnailSpec[preferred];
+  const [width, height] = getDimensionsOfImagePath(mediaPath, cache);
+  const available = getThumbnailsAvailableForDimensions([width, height]);
+  const [selected] = available.find(([name, size]) => size <= preferredSize);
+  return selected;
+}
+
 function readFileMD5(filePath) {
   return new Promise((resolve, reject) => {
     const md5 = createHash('md5');
@@ -122,15 +236,26 @@ function readFileMD5(filePath) {
   });
 }
 
-async function getImageMagickVersion(spawnConvert) {
-  const proc = spawnConvert(['--version'], false);
+async function identifyImageDimensions(filePath) {
+  // See: https://github.com/image-size/image-size/issues/96
+  const buffer = await readFile(filePath);
+  const dimensions = dimensionsOf(buffer);
+  return [dimensions.width, dimensions.height];
+}
+
+async function getImageMagickVersion(binary) {
+  const proc = spawn(binary, ['--version']);
 
   let allData = '';
   proc.stdout.on('data', (data) => {
     allData += data.toString();
   });
 
-  await promisifyProcess(proc, false);
+  try {
+    await promisifyProcess(proc, false);
+  } catch (error) {
+    return null;
+  }
 
   if (!allData.match(/ImageMagick/i)) {
     return null;
@@ -144,29 +269,45 @@ async function getImageMagickVersion(spawnConvert) {
   return match[1];
 }
 
-async function getSpawnConvert() {
-  let fn, description, version;
-  if (await commandExists('convert')) {
-    fn = (args) => spawn('convert', args);
-    description = 'convert';
-  } else if (await commandExists('magick')) {
-    fn = (args, prefix = true) =>
-      spawn('magick', prefix ? ['convert', ...args] : args);
-    description = 'magick convert';
-  } else {
-    return [`no convert or magick binary`, null];
+async function getSpawnMagick(tool) {
+  if (tool !== 'identify' && tool !== 'convert') {
+    throw new Error(`Expected identify or convert`);
+  }
+
+  let fn = null;
+  let description = null;
+  let version = null;
+
+  if (await commandExists(tool)) {
+    version = await getImageMagickVersion(tool);
+    if (version !== null) {
+      fn = (args) => spawn(tool, args);
+      description = tool;
+    }
   }
 
-  version = await getImageMagickVersion(fn);
+  if (fn === null && await commandExists('magick')) {
+    version = await getImageMagickVersion('magick');
+    if (version !== null) {
+      fn = (args) => spawn('magick', [tool, ...args]);
+      description = `magick ${tool}`;
+    }
+  }
 
-  if (version === null) {
-    return [`binary --version output didn't indicate it's ImageMagick`];
+  if (fn === null) {
+    return [`no ${tool} or magick binary`, null];
   }
 
   return [`${description} (${version})`, fn];
 }
 
-function generateImageThumbnails(filePath, {spawnConvert}) {
+// Note: This returns an array of no-argument functions, suitable for passing
+// to queue().
+function generateImageThumbnails({
+  filePath,
+  dimensions,
+  spawnConvert,
+}) {
   const dirname = path.dirname(filePath);
   const extname = path.extname(filePath);
   const basename = path.basename(filePath, extname);
@@ -185,10 +326,11 @@ function generateImageThumbnails(filePath, {spawnConvert}) {
       output(name),
     ]);
 
-  return Promise.all(
-    Object.entries(thumbnailSpec)
-      .map(([ext, details]) =>
-        promisifyProcess(convert('.' + ext, details), false)));
+  return (
+    getThumbnailsAvailableForDimensions(dimensions)
+      .map(([name]) => [name, thumbnailSpec[name]])
+      .map(([name, details]) => () =>
+        promisifyProcess(convert('.' + name, details), false)));
 }
 
 export async function clearThumbs(mediaPath, {
@@ -224,7 +366,7 @@ export async function clearThumbs(mediaPath, {
         console.error(file);
       }
       fileIssue();
-      return;
+      return {success: false};
     }
 
     logInfo`Clearing out ${thumbFiles.length} thumbs.`;
@@ -249,6 +391,7 @@ export async function clearThumbs(mediaPath, {
         console.error(file);
       }
       logError`Check for permission errors?`;
+      return {success: false};
     } else {
       logInfo`Successfully deleted all ${thumbFiles.length} thumbnail files!`;
     }
@@ -277,6 +420,8 @@ export async function clearThumbs(mediaPath, {
       logWarn`Failed to remove cache file. Check its permissions?`;
     }
   }
+
+  return {success: true};
 }
 
 export default async function genThumbs(mediaPath, {
@@ -290,18 +435,21 @@ export default async function genThumbs(mediaPath, {
 
   const quietInfo = quiet ? () => null : logInfo;
 
-  const [convertInfo, spawnConvert] = (await getSpawnConvert()) ?? [];
+  const [convertInfo, spawnConvert] = await getSpawnMagick('convert');
+
   if (!spawnConvert) {
     logError`${`It looks like you don't have ImageMagick installed.`}`;
     logError`ImageMagick is required to generate thumbnails for display on the wiki.`;
-    logError`(Error message: ${convertInfo})`;
+    for (const error of [convertInfo].filter(Boolean)) {
+      logError`(Error message: ${error})`;
+    }
     logInfo`You can find info to help install ImageMagick on Linux, Windows, or macOS`;
     logInfo`from its official website: ${`https://imagemagick.org/script/download.php`}`;
     logInfo`If you have trouble working ImageMagick and would like some help, feel free`;
     logInfo`to drop a message in the HSMusic Discord server! ${'https://hsmusic.wiki/discord/'}`;
-    return false;
+    return {success: false};
   } else {
-    logInfo`Found ImageMagick binary: ${convertInfo}`;
+    logInfo`Found ImageMagick binary:  ${convertInfo}`;
   }
 
   quietInfo`Running up to ${magickThreads + ' magick threads'} simultaneously.`;
@@ -341,19 +489,16 @@ export default async function genThumbs(mediaPath, {
 
   const imagePaths = await traverseSourceImagePaths(mediaPath, {target: 'generate'});
 
-  const imageToMD5Entries = await progressPromiseAll(
-    `Generating MD5s of image files`,
-    queue(
-      imagePaths.map(
-        (imagePath) => () =>
-          readFileMD5(path.join(mediaPath, imagePath)).then(
-            (md5) => [imagePath, md5],
-            (error) => [imagePath, {error}]
-          )
-      ),
-      queueSize
-    )
-  );
+  const imageToMD5Entries =
+    await progressPromiseAll(
+      `Generating MD5s of image files`,
+      queue(
+        imagePaths.map(imagePath => () =>
+          readFileMD5(path.join(mediaPath, imagePath))
+            .then(
+              md5 => [imagePath, md5],
+              error => [imagePath, {error}])),
+        queueSize));
 
   {
     let error = false;
@@ -367,23 +512,53 @@ export default async function genThumbs(mediaPath, {
       logError`Failed to read at least one image file!`;
       logError`This implies a thumbnail probably won't be generatable.`;
       logError`So, exiting early.`;
-      return false;
+      return {success: false};
     } else {
       quietInfo`All image files successfully read.`;
     }
   }
 
+  const imageToDimensionsEntries =
+    await progressPromiseAll(
+      `Identifying dimensions of image files`,
+      queue(
+        imagePaths.map(imagePath => () =>
+          identifyImageDimensions(path.join(mediaPath, imagePath))
+            .then(
+              dimensions => [imagePath, dimensions],
+              error => [imagePath, {error}])),
+        queueSize));
+
+  {
+    let error = false;
+    for (const entry of imageToDimensionsEntries) {
+      if (entry[1].error) {
+        logError`Failed to identify dimensions ${entry[0]}: ${entry[1].error}`;
+        error = true;
+      }
+    }
+    if (error) {
+      logError`Failed to identify dimensions of at least one image file!`;
+      logError`This implies a thumbnail probably won't be generatable.`;
+      logError`So, exiting early.`;
+      return {success: false};
+    } else {
+      quietInfo`All image files successfully had dimensions identified.`;
+    }
+  }
+
+  const imageToDimensions = Object.fromEntries(imageToDimensionsEntries);
+
   // Technically we could pro8a8ly mut8te the cache varia8le in-place?
   // 8ut that seems kinda iffy.
   const updatedCache = Object.assign({}, cache);
 
   const entriesToGenerate = imageToMD5Entries.filter(
-    ([filePath, md5]) => md5 !== cache[filePath]
-  );
+    ([filePath, md5]) => md5 !== cache[filePath]?.[0]);
 
   if (empty(entriesToGenerate)) {
     logInfo`All image thumbnails are already up-to-date - nice!`;
-    return true;
+    return {success: true, cache};
   }
 
   logInfo`Generating thumbnails for ${entriesToGenerate.length} media files.`;
@@ -392,31 +567,45 @@ export default async function genThumbs(mediaPath, {
   }
 
   const failed = [];
-  const succeeded = [];
+
   const writeMessageFn = () =>
     `Writing image thumbnails. [failed: ${failed.length}]`;
 
+  const generateCalls =
+    entriesToGenerate.flatMap(([filePath, md5]) =>
+      generateImageThumbnails({
+        filePath: path.join(mediaPath, filePath),
+        dimensions: imageToDimensions[filePath],
+        spawnConvert,
+      }).map(call => async () => {
+        try {
+          await call();
+        } catch (error) {
+          failed.push([filePath, error]);
+        }
+      }));
+
   await progressPromiseAll(writeMessageFn,
-    queue(
-      entriesToGenerate.map(([filePath, md5]) => () =>
-        generateImageThumbnails(path.join(mediaPath, filePath), {spawnConvert}).then(
-          () => {
-            updatedCache[filePath] = md5;
-            succeeded.push(filePath);
-          },
-          error => {
-            failed.push([filePath, error]);
-          })),
-      magickThreads));
+    queue(generateCalls, magickThreads));
+
+  // Sort by file path.
+  failed.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
+
+  const failedFilePaths = new Set(failed.map(([filePath]) => filePath));
+
+  for (const [filePath, md5] of entriesToGenerate) {
+    if (failedFilePaths.has(filePath)) continue;
+    updatedCache[filePath] = [md5, ...imageToDimensions[filePath]];
+  }
 
   if (empty(failed)) {
     logInfo`Generated all (updated) thumbnails successfully!`;
   } else {
     for (const [path, error] of failed) {
-      logError`Thumbnails failed to generate for ${path} - ${error}`;
+      logError`Thumbnail failed to generate for ${path} - ${error}`;
     }
-    logWarn`Result is incomplete - the above ${failed.length} thumbnails should be checked for errors.`;
-    logWarn`${succeeded.length} successfully generated images won't be regenerated next run, though!`;
+    logWarn`Result is incomplete - the above thumbnails should be checked for errors.`;
+    logWarn`Successfully generated images won't be regenerated next run, though!`;
   }
 
   try {
@@ -431,7 +620,7 @@ export default async function genThumbs(mediaPath, {
     logWarn`Sorry about that!`;
   }
 
-  return true;
+  return {success: true, cache: updatedCache};
 }
 
 export function getExpectedImagePaths(mediaPath, {urls, wikiData}) {
@@ -441,8 +630,8 @@ export function getExpectedImagePaths(mediaPath, {urls, wikiData}) {
     wikiData.albumData
       .flatMap(album => [
         album.hasCoverArt && fromRoot.to('media.albumCover', album.directory, album.coverArtFileExtension),
-        !empty(album.bannerArtistContribsByRef) && fromRoot.to('media.albumBanner', album.directory, album.bannerFileExtension),
-        !empty(album.wallpaperArtistContribsByRef) && fromRoot.to('media.albumWallpaper', album.directory, album.wallpaperFileExtension),
+        !empty(CacheableObject.getUpdateValue(album, 'bannerArtistContribs')) && fromRoot.to('media.albumBanner', album.directory, album.bannerFileExtension),
+        !empty(CacheableObject.getUpdateValue(album, 'wallpaperArtistContribs')) && fromRoot.to('media.albumWallpaper', album.directory, album.wallpaperFileExtension),
       ])
       .filter(Boolean),
 
@@ -489,22 +678,24 @@ export async function verifyImagePaths(mediaPath, {urls, wikiData}) {
 
   if (empty(missing) && empty(misplaced)) {
     logInfo`All image paths are good - nice! None are missing or misplaced.`;
-    return;
+    return {missing, misplaced};
   }
 
   if (!empty(missing)) {
     logWarn`** Some image files are missing! (${missing.length + ' files'}) **`;
     for (const file of missing) {
-      console.warn(color.yellow(` - `) + file);
+      console.warn(colors.yellow(` - `) + file);
     }
   }
 
   if (!empty(misplaced)) {
     logWarn`** Some image files are misplaced! (${misplaced.length + ' files'}) **`;
     for (const file of misplaced) {
-      console.warn(color.yellow(` - `) + file);
+      console.warn(colors.yellow(` - `) + file);
     }
   }
+
+  return {missing, misplaced};
 }
 
 // Recursively traverses the provided (extant) media path, filtering so only
diff --git a/src/listing-spec.js b/src/listing-spec.js
index 7918dd1..f57762b 100644
--- a/src/listing-spec.js
+++ b/src/listing-spec.js
@@ -1,14 +1,4 @@
-import {accumulateSum, empty, showAggregate} from '#sugar';
-
-import {
-  chunkByProperties,
-  getArtistNumContributions,
-  getTotalDuration,
-  sortAlphabetically,
-  sortByDate,
-  sortChronologically,
-  sortFlashesChronologically,
-} from '#wiki-data';
+import {empty, showAggregate} from '#sugar';
 
 const listingSpec = [];
 
diff --git a/src/page/album.js b/src/page/album.js
index 69fcabc..af41076 100644
--- a/src/page/album.js
+++ b/src/page/album.js
@@ -5,7 +5,6 @@ export function targets({wikiData}) {
 }
 
 export function pathsForTarget(album) {
-  const hasGalleryPage = album.tracks.some(t => t.hasUniqueCoverArt);
   const hasCommentaryPage = !!album.commentary || album.tracks.some(t => t.commentary);
 
   return [
@@ -19,7 +18,7 @@ export function pathsForTarget(album) {
       },
     },
 
-    hasGalleryPage && {
+    {
       type: 'page',
       path: ['albumGallery', album.directory],
 
diff --git a/src/page/artist-alias.js b/src/page/artist-alias.js
index 1da2af4..d230522 100644
--- a/src/page/artist-alias.js
+++ b/src/page/artist-alias.js
@@ -7,6 +7,12 @@ export function targets({wikiData}) {
 export function pathsForTarget(aliasArtist) {
   const {aliasedArtist} = aliasArtist;
 
+  // Don't generate a redirect page if this aliased name resolves to the same
+  // directory as the original artist! See issue #280.
+  if (aliasArtist.directory === aliasedArtist.directory) {
+    return [];
+  }
+
   return [
     {
       type: 'redirect',
diff --git a/src/page/flash-act.js b/src/page/flash-act.js
new file mode 100644
index 0000000..e54525a
--- /dev/null
+++ b/src/page/flash-act.js
@@ -0,0 +1,23 @@
+export const description = `flash act gallery pages`;
+
+export function condition({wikiData}) {
+  return wikiData.wikiInfo.enableFlashesAndGames;
+}
+
+export function targets({wikiData}) {
+  return wikiData.flashActData;
+}
+
+export function pathsForTarget(flashAct) {
+  return [
+    {
+      type: 'page',
+      path: ['flashActGallery', flashAct.directory],
+
+      contentFunction: {
+        name: 'generateFlashActGalleryPage',
+        args: [flashAct],
+      },
+    },
+  ];
+}
diff --git a/src/page/flash.js b/src/page/flash.js
index b9d27d0..7df7415 100644
--- a/src/page/flash.js
+++ b/src/page/flash.js
@@ -1,5 +1,3 @@
-import {empty} from '#sugar';
-
 export const description = `flash & game pages`;
 
 export function condition({wikiData}) {
diff --git a/src/page/index.js b/src/page/index.js
index 48e22d2..21d93c8 100644
--- a/src/page/index.js
+++ b/src/page/index.js
@@ -2,6 +2,7 @@ export * as album from './album.js';
 export * as artist from './artist.js';
 export * as artistAlias from './artist-alias.js';
 export * as flash from './flash.js';
+export * as flashAct from './flash-act.js';
 export * as group from './group.js';
 export * as homepage from './homepage.js';
 export * as listing from './listing.js';
diff --git a/src/repl.js b/src/repl.js
index 9ab4ddf..ead0156 100644
--- a/src/repl.js
+++ b/src/repl.js
@@ -11,7 +11,7 @@ import {generateURLs, urlSpec} from '#urls';
 import {quickLoadAllFromYAML} from '#yaml';
 
 import _find, {bindFind} from '#find';
-import thingConstructors from '#things';
+import thingConstructors, {CacheableObject} from '#things';
 import * as serialize from '#serialize';
 import * as sugar from '#sugar';
 import * as wikiDataUtils from '#wiki-data';
@@ -63,6 +63,7 @@ export async function getContextAssignments({
     WD: wikiData,
 
     ...thingConstructors,
+    CacheableObject,
     language,
 
     ...sugar,
diff --git a/src/static/client2.js b/src/static/client2.js
index 0cdb8b0..758d91a 100644
--- a/src/static/client2.js
+++ b/src/static/client2.js
@@ -6,13 +6,28 @@
 // ephemeral nature.
 
 import {getColors} from '../util/colors.js';
-import {getArtistNumContributions} from '../util/wiki-data.js';
+import {empty, stitchArrays} from '../util/sugar.js';
+
+import {
+  filterMultipleArrays,
+  getArtistNumContributions,
+} from '../util/wiki-data.js';
 
 let albumData, artistData;
 let officialAlbumData, fandomAlbumData, beyondAlbumData;
 
 let ready = false;
 
+const clientInfo = window.hsmusicClientInfo = Object.create(null);
+
+const clientSteps = {
+  getPageReferences: [],
+  addInternalListeners: [],
+  mutatePageContent: [],
+  initializeState: [],
+  addPageListeners: [],
+};
+
 // Localiz8tion nonsense ----------------------------------
 
 const language = document.documentElement.getAttribute('lang');
@@ -86,113 +101,148 @@ function fetchData(type, directory) {
 
 // JS-based links -----------------------------------------
 
-for (const a of document.body.querySelectorAll('[data-random]')) {
-  a.addEventListener('click', (evt) => {
-    if (!ready) {
-      evt.preventDefault();
-      return;
-    }
+const scriptedLinkInfo = clientInfo.scriptedLinkInfo = {
+  randomLinks: null,
+  revealLinks: null,
 
-    const tracks = albumData =>
-      albumData
-        .map(album => album.tracks)
-        .reduce((acc, tracks) => acc.concat(tracks), []);
+  nextLink: null,
+  previousLink: null,
+  randomLink: null,
+};
 
-    setTimeout(() => {
-      a.href = rebase('js-disabled');
-    });
+function getScriptedLinkReferences() {
+  scriptedLinkInfo.randomLinks =
+    document.querySelectorAll('[data-random]');
 
-    switch (a.dataset.random) {
-      case 'album':
-        a.href = openAlbum(pick(albumData).directory);
-        break;
-
-      case 'album-in-official':
-        a.href = openAlbum(pick(officialAlbumData).directory);
-        break;
-
-      case 'album-in-fandom':
-        a.href = openAlbum(pick(fandomAlbumData).directory);
-        break;
-
-      case 'album-in-beyond':
-        a.href = openAlbum(pick(beyondAlbumData).directory);
-        break;
-
-      case 'track':
-        a.href = openTrack(getRefDirectory(pick(tracks(albumData))));
-        break;
-
-      case 'track-in-album':
-        a.href = openTrack(getRefDirectory(pick(getAlbum(a).tracks)));
-        break;
-
-      case 'track-in-official':
-        a.href = openTrack(getRefDirectory(pick(tracks(officialAlbumData))));
-        break;
-
-      case 'track-in-fandom':
-        a.href = openTrack(getRefDirectory(pick(tracks(fandomAlbumData))));
-        break;
-
-      case 'track-in-beyond':
-        a.href = openTrack(getRefDirectory(pick(tracks(beyondAlbumData))));
-        break;
-
-      case 'artist':
-        a.href = openArtist(pick(artistData).directory);
-        break;
-
-      case 'artist-more-than-one-contrib':
-        a.href =
-          openArtist(
-            pick(artistData.filter((artist) => getArtistNumContributions(artist) > 1))
-              .directory);
-        break;
-    }
-  });
+  scriptedLinkInfo.revealLinks =
+    document.getElementsByClassName('reveal');
+
+  scriptedLinkInfo.nextNavLink =
+    document.getElementById('next-button');
+
+  scriptedLinkInfo.previousNavLink =
+    document.getElementById('previous-button');
+
+  scriptedLinkInfo.randomNavLink =
+    document.getElementById('random-button');
 }
 
-const next = document.getElementById('next-button');
-const previous = document.getElementById('previous-button');
-const random = document.getElementById('random-button');
+function addRandomLinkListeners() {
+  for (const a of scriptedLinkInfo.randomLinks ?? []) {
+    a.addEventListener('click', evt => {
+      if (!ready) {
+        evt.preventDefault();
+        return;
+      }
+
+      const tracks = albumData =>
+        albumData
+          .map(album => album.tracks)
+          .reduce((acc, tracks) => acc.concat(tracks), []);
 
-const prependTitle = (el, prepend) => {
-  const existing = el.getAttribute('title');
-  if (existing) {
-    el.setAttribute('title', prepend + ' ' + existing);
-  } else {
-    el.setAttribute('title', prepend);
-  }
-};
+      setTimeout(() => {
+        a.href = rebase('js-disabled');
+      });
 
-if (next) prependTitle(next, '(Shift+N)');
-if (previous) prependTitle(previous, '(Shift+P)');
-if (random) prependTitle(random, '(Shift+R)');
-
-document.addEventListener('keypress', (event) => {
-  if (event.shiftKey) {
-    if (event.charCode === 'N'.charCodeAt(0)) {
-      if (next) next.click();
-    } else if (event.charCode === 'P'.charCodeAt(0)) {
-      if (previous) previous.click();
-    } else if (event.charCode === 'R'.charCodeAt(0)) {
-      if (random && ready) random.click();
-    }
+      switch (a.dataset.random) {
+        case 'album':
+          a.href = openAlbum(pick(albumData).directory);
+          break;
+
+        case 'album-in-official':
+          a.href = openAlbum(pick(officialAlbumData).directory);
+          break;
+
+        case 'album-in-fandom':
+          a.href = openAlbum(pick(fandomAlbumData).directory);
+          break;
+
+        case 'album-in-beyond':
+          a.href = openAlbum(pick(beyondAlbumData).directory);
+          break;
+
+        case 'track':
+          a.href = openTrack(getRefDirectory(pick(tracks(albumData))));
+          break;
+
+        case 'track-in-album':
+          a.href = openTrack(getRefDirectory(pick(getAlbum(a).tracks)));
+          break;
+
+        case 'track-in-official':
+          a.href = openTrack(getRefDirectory(pick(tracks(officialAlbumData))));
+          break;
+
+        case 'track-in-fandom':
+          a.href = openTrack(getRefDirectory(pick(tracks(fandomAlbumData))));
+          break;
+
+        case 'track-in-beyond':
+          a.href = openTrack(getRefDirectory(pick(tracks(beyondAlbumData))));
+          break;
+
+        case 'artist':
+          a.href = openArtist(pick(artistData).directory);
+          break;
+
+        case 'artist-more-than-one-contrib':
+          a.href =
+            openArtist(
+              pick(artistData.filter((artist) => getArtistNumContributions(artist) > 1))
+                .directory);
+          break;
+      }
+    });
   }
-});
+}
 
-for (const reveal of document.querySelectorAll('.reveal')) {
-  reveal.addEventListener('click', (event) => {
-    if (!reveal.classList.contains('revealed')) {
-      reveal.classList.add('revealed');
-      event.preventDefault();
-      event.stopPropagation();
-      reveal.dispatchEvent(new CustomEvent('hsmusic-reveal'));
+function mutateNavigationLinkContent() {
+  const prependTitle = (el, prepend) =>
+    el?.setAttribute('title',
+      (el.hasAttribute('title')
+        ? prepend + ' ' + el.getAttribute('title')
+        : prepend));
+
+  prependTitle(scriptedLinkInfo.nextNavLink, '(Shift+N)');
+  prependTitle(scriptedLinkInfo.previousNavLink, '(Shift+P)');
+  prependTitle(scriptedLinkInfo.randomNavLink, '(Shift+R)');
+}
+
+function addNavigationKeyPressListeners() {
+  document.addEventListener('keypress', (event) => {
+    if (event.shiftKey) {
+      if (event.charCode === 'N'.charCodeAt(0)) {
+        scriptedLinkInfo.nextNavLink?.click();
+      } else if (event.charCode === 'P'.charCodeAt(0)) {
+        scriptedLinkInfo.previousNavLink?.click();
+      } else if (event.charCode === 'R'.charCodeAt(0)) {
+        if (ready) {
+          scriptedLinkInfo.randomNavLink?.click();
+        }
+      }
     }
   });
 }
 
+function addRevealLinkClickListeners() {
+  for (const reveal of scriptedLinkInfo.revealLinks ?? []) {
+    reveal.addEventListener('click', (event) => {
+      if (!reveal.classList.contains('revealed')) {
+        reveal.classList.add('revealed');
+        event.preventDefault();
+        event.stopPropagation();
+        reveal.dispatchEvent(new CustomEvent('hsmusic-reveal'));
+      }
+    });
+  }
+}
+
+clientSteps.getPageReferences.push(getScriptedLinkReferences);
+clientSteps.addPageListeners.push(addRandomLinkListeners);
+clientSteps.addPageListeners.push(addNavigationKeyPressListeners);
+clientSteps.addPageListeners.push(addRevealLinkClickListeners);
+clientSteps.mutatePageContent.push(mutateNavigationLinkContent);
+
 const elements1 = document.getElementsByClassName('js-hide-once-data');
 const elements2 = document.getElementsByClassName('js-show-once-data');
 
@@ -454,199 +504,393 @@ if (localStorage.tryInfoCards) {
 
 // Custom hash links --------------------------------------
 
-function addHashLinkHandlers() {
+const hashLinkInfo = clientInfo.hashLinkInfo = {
+  links: null,
+  hrefs: null,
+  targets: null,
+
+  state: {
+    highlightedTarget: null,
+    scrollingAfterClick: false,
+    concludeScrollingStateInterval: null,
+  },
+
+  event: {
+    whenHashLinkClicked: [],
+  },
+};
+
+function getHashLinkReferences() {
+  const info = hashLinkInfo;
+
+  info.links =
+    Array.from(document.querySelectorAll('a[href^="#"]:not([href="#"])'));
+
+  info.hrefs =
+    info.links
+      .map(link => link.getAttribute('href'));
+
+  info.targets =
+    info.hrefs
+      .map(href => document.getElementById(href.slice(1)));
+
+  filterMultipleArrays(
+    info.links,
+    info.hrefs,
+    info.targets,
+    (_link, _href, target) => target);
+}
+
+function processScrollingAfterHashLinkClicked() {
+  const {state} = hashLinkInfo;
+
+  if (state.concludeScrollingStateInterval) return;
+
+  let lastScroll = window.scrollY;
+  state.scrollingAfterClick = true;
+  state.concludeScrollingStateInterval = setInterval(() => {
+    if (Math.abs(window.scrollY - lastScroll) < 10) {
+      clearInterval(state.concludeScrollingStateInterval);
+      state.scrollingAfterClick = false;
+      state.concludeScrollingStateInterval = null;
+    } else {
+      lastScroll = window.scrollY;
+    }
+  }, 200);
+}
+
+function addHashLinkListeners() {
   // Instead of defining a scroll offset (to account for the sticky heading)
   // in JavaScript, we interface with the CSS property 'scroll-margin-top'.
   // This lets the scroll offset be consolidated where it makes sense, and
   // sets an appropriate offset when (re)loading a page with hash for free!
 
-  let wasHighlighted;
-
-  for (const a of document.links) {
-    const href = a.getAttribute('href');
-    if (!href || !href.startsWith('#')) {
-      continue;
-    }
+  const info = hashLinkInfo;
+  const {state, event} = info;
 
-    a.addEventListener('click', handleHashLinkClicked);
-  }
+  for (const {hashLink, href, target} of stitchArrays({
+    hashLink: info.links,
+    href: info.hrefs,
+    target: info.targets,
+  })) {
+    hashLink.addEventListener('click', evt => {
+      if (evt.metaKey || evt.shiftKey || evt.ctrlKey || evt.altKey) {
+        return;
+      }
 
-  function handleHashLinkClicked(evt) {
-    if (evt.metaKey || evt.shiftKey || evt.ctrlKey || evt.altKey) {
-      return;
-    }
+      // Hide skipper box right away, so the layout is updated on time for the
+      // math operations coming up next.
+      const skipper = document.getElementById('skippers');
+      skipper.style.display = 'none';
+      setTimeout(() => skipper.style.display = '');
 
-    const href = evt.target.getAttribute('href');
-    const id = href.slice(1);
-    const linked = document.getElementById(id);
+      const box = target.getBoundingClientRect();
+      const style = window.getComputedStyle(target);
 
-    if (!linked) {
-      return;
-    }
+      const scrollY =
+          window.scrollY
+        + box.top
+        - style['scroll-margin-top'].replace('px', '');
 
-    // Hide skipper box right away, so the layout is updated on time for the
-    // math operations coming up next.
-    const skipper = document.getElementById('skippers');
-    skipper.style.display = 'none';
-    setTimeout(() => skipper.style.display = '');
+      evt.preventDefault();
+      history.pushState({}, '', href);
+      window.scrollTo({top: scrollY, behavior: 'smooth'});
+      target.focus({preventScroll: true});
 
-    const box = linked.getBoundingClientRect();
-    const style = window.getComputedStyle(linked);
+      const maxScroll =
+          document.body.scrollHeight
+        - window.innerHeight;
 
-    const scrollY =
-        window.scrollY
-      + box.top
-      - style['scroll-margin-top'].replace('px', '');
+      if (scrollY > maxScroll && target.classList.contains('content-heading')) {
+        if (state.highlightedTarget) {
+          state.highlightedTarget.classList.remove('highlight-hash-link');
+        }
 
-    evt.preventDefault();
-    history.pushState({}, '', href);
-    window.scrollTo({top: scrollY, behavior: 'smooth'});
-    linked.focus({preventScroll: true});
+        target.classList.add('highlight-hash-link');
+        state.highlightedTarget = target;
+      }
 
-    const maxScroll =
-        document.body.scrollHeight
-      - window.innerHeight;
+      processScrollingAfterHashLinkClicked();
 
-    if (scrollY > maxScroll && linked.classList.contains('content-heading')) {
-      if (wasHighlighted) {
-        wasHighlighted.classList.remove('highlight-hash-link');
+      for (const handler of event.whenHashLinkClicked) {
+        handler({
+          link: hashLink,
+        });
       }
+    });
+  }
 
-      wasHighlighted = linked;
-      linked.classList.add('highlight-hash-link');
-      linked.addEventListener('animationend', function handle(evt) {
-        if (evt.animationName === 'highlight-hash-link') {
-          linked.removeEventListener('animationend', handle);
-          linked.classList.remove('highlight-hash-link');
-          wasHighlighted = null;
-        }
-      });
-    }
+  for (const target of info.targets) {
+    target.addEventListener('animationend', evt => {
+      if (evt.animationName !== 'highlight-hash-link') return;
+      target.classList.remove('highlight-hash-link');
+      if (target !== state.highlightedTarget) return;
+      state.highlightedTarget = null;
+    });
   }
 }
 
-addHashLinkHandlers();
+clientSteps.getPageReferences.push(getHashLinkReferences);
+clientSteps.addPageListeners.push(addHashLinkListeners);
 
 // Sticky content heading ---------------------------------
 
-const stickyHeadingInfo = Array.from(document.querySelectorAll('.content-sticky-heading-container'))
-  .map(stickyContainer => {
-    const {parentElement: contentContainer} = stickyContainer;
-    const stickySubheadingRow = stickyContainer.querySelector('.content-sticky-subheading-row');
-    const stickySubheading = stickySubheadingRow.querySelector('h2');
-    const stickyCoverContainer = stickyContainer.querySelector('.content-sticky-heading-cover-container');
-    const stickyCover = stickyCoverContainer?.querySelector('.content-sticky-heading-cover');
-    const contentHeadings = Array.from(contentContainer.querySelectorAll('.content-heading'));
-    const contentCover = contentContainer.querySelector('#cover-art-container');
-
-    return {
-      contentContainer,
-      contentCover,
-      contentHeadings,
-      stickyContainer,
-      stickyCover,
-      stickyCoverContainer,
-      stickySubheading,
-      stickySubheadingRow,
-      state: {
-        displayedHeading: null,
-      },
-    };
-  });
+const stickyHeadingInfo = clientInfo.stickyHeadingInfo = {
+  stickyContainers: null,
 
-const topOfViewInside = (el, scroll = window.scrollY) => (
-  scroll > el.offsetTop &&
-  scroll < el.offsetTop + el.offsetHeight
-);
-
-function prepareStickyHeadings() {
-  for (const {
-    contentCover,
-    stickyCover,
-  } of stickyHeadingInfo) {
-    const coverRevealImage = contentCover?.querySelector('.reveal');
-    if (coverRevealImage) {
-      stickyCover.classList.add('content-sticky-heading-cover-needs-reveal');
-      coverRevealImage.addEventListener('hsmusic-reveal', () => {
-        stickyCover.classList.remove('content-sticky-heading-cover-needs-reveal');
-      });
+  stickySubheadingRows: null,
+  stickySubheadings: null,
+
+  stickyCoverContainers: null,
+  stickyCoverTextAreas: null,
+  stickyCovers: null,
+
+  contentContainers: null,
+  contentHeadings: null,
+  contentCovers: null,
+  contentCoversReveal: null,
+
+  state: {
+    displayedHeading: null,
+  },
+
+  event: {
+    whenDisplayedHeadingChanges: [],
+  },
+};
+
+function getStickyHeadingReferences() {
+  const info = stickyHeadingInfo;
+
+  info.stickyContainers =
+    Array.from(document.getElementsByClassName('content-sticky-heading-container'));
+
+  info.stickyCoverContainers =
+    info.stickyContainers
+      .map(el => el.querySelector('.content-sticky-heading-cover-container'));
+
+  info.stickyCovers =
+    info.stickyCoverContainers
+      .map(el => el?.querySelector('.content-sticky-heading-cover'));
+
+  info.stickyCoverTextAreas =
+    info.stickyCovers
+      .map(el => el?.querySelector('.image-text-area'));
+
+  info.stickySubheadingRows =
+    info.stickyContainers
+      .map(el => el.querySelector('.content-sticky-subheading-row'));
+
+  info.stickySubheadings =
+    info.stickySubheadingRows
+      .map(el => el.querySelector('h2'));
+
+  info.contentContainers =
+    info.stickyContainers
+      .map(el => el.parentElement);
+
+  info.contentCovers =
+    info.contentContainers
+      .map(el => el.querySelector('#cover-art-container'));
+
+  info.contentCoversReveal =
+    info.contentCovers
+      .map(el => el ? !!el.querySelector('.reveal') : null);
+
+  info.contentHeadings =
+    info.contentContainers
+      .map(el => Array.from(el.querySelectorAll('.content-heading')));
+}
+
+function removeTextPlaceholderStickyHeadingCovers() {
+  const info = stickyHeadingInfo;
+
+  const hasTextArea =
+    info.stickyCoverTextAreas.map(el => !!el);
+
+  const coverContainersWithTextArea =
+    info.stickyCoverContainers
+      .filter((_el, index) => hasTextArea[index]);
+
+  for (const el of coverContainersWithTextArea) {
+    el.remove();
+  }
+
+  info.stickyCoverContainers =
+    info.stickyCoverContainers
+      .map((el, index) => hasTextArea[index] ? null : el);
+
+  info.stickyCovers =
+    info.stickyCovers
+      .map((el, index) => hasTextArea[index] ? null : el);
+
+  info.stickyCoverTextAreas =
+    info.stickyCoverTextAreas
+      .slice()
+      .fill(null);
+}
+
+function addRevealClassToStickyHeadingCovers() {
+  const info = stickyHeadingInfo;
+
+  const stickyCoversWhichReveal =
+    info.stickyCovers
+      .filter((_el, index) => info.contentCoversReveal[index]);
+
+  for (const el of stickyCoversWhichReveal) {
+    el.classList.add('content-sticky-heading-cover-needs-reveal');
+  }
+}
+
+function addRevealListenersForStickyHeadingCovers() {
+  const info = stickyHeadingInfo;
+
+  const stickyCovers = info.stickyCovers.slice();
+  const contentCovers = info.contentCovers.slice();
+
+  filterMultipleArrays(
+    stickyCovers,
+    contentCovers,
+    (_stickyCover, _contentCover, index) => info.contentCoversReveal[index]);
+
+  for (const {stickyCover, contentCover} of stitchArrays({
+    stickyCover: stickyCovers,
+    contentCover: contentCovers,
+  })) {
+    // TODO: Janky - should use internal event instead of DOM event
+    contentCover.querySelector('.reveal').addEventListener('hsmusic-reveal', () => {
+      stickyCover.classList.remove('content-sticky-heading-cover-needs-reveal');
+    });
+  }
+}
+
+function topOfViewInside(el, scroll = window.scrollY) {
+  return (
+    scroll > el.offsetTop &&
+    scroll < el.offsetTop + el.offsetHeight);
+}
+
+function updateStickyCoverVisibility(index) {
+  const info = stickyHeadingInfo;
+
+  const stickyCoverContainer = info.stickyCoverContainers[index];
+  const contentCover = info.contentCovers[index];
+
+  if (contentCover && stickyCoverContainer) {
+    if (contentCover.getBoundingClientRect().bottom < 0) {
+      stickyCoverContainer.classList.add('visible');
+    } else {
+      stickyCoverContainer.classList.remove('visible');
     }
   }
 }
 
-function updateStickyHeading() {
-  for (const {
-    contentContainer,
-    contentCover,
-    contentHeadings,
-    stickyContainer,
-    stickyCoverContainer,
-    stickySubheading,
-    stickySubheadingRow,
-    state,
-  } of stickyHeadingInfo) {
-    let closestHeading = null;
+function getContentHeadingClosestToStickySubheading(index) {
+  const info = stickyHeadingInfo;
 
-    if (contentCover && stickyCoverContainer) {
-      if (contentCover.getBoundingClientRect().bottom < 0) {
-        stickyCoverContainer.classList.add('visible');
-      } else {
-        stickyCoverContainer.classList.remove('visible');
-      }
+  const contentContainer = info.contentContainers[index];
+
+  if (!topOfViewInside(contentContainer)) {
+    return null;
+  }
+
+  const stickySubheading = info.stickySubheadings[index];
+
+  if (stickySubheading.childNodes.length === 0) {
+    // Supply a non-breaking space to ensure correct basic line height.
+    stickySubheading.appendChild(document.createTextNode('\xA0'));
+  }
+
+  const stickyContainer = info.stickyContainers[index];
+  const stickyRect = stickyContainer.getBoundingClientRect();
+
+  // TODO: Should this compute with the subheading row instead of h2?
+  const subheadingRect = stickySubheading.getBoundingClientRect();
+
+  const stickyBottom = stickyRect.bottom + subheadingRect.height;
+
+  // Iterate from bottom to top of the content area.
+  const contentHeadings = info.contentHeadings[index];
+  for (const heading of contentHeadings.slice().reverse()) {
+    const headingRect = heading.getBoundingClientRect();
+    if (headingRect.y + headingRect.height / 1.5 < stickyBottom + 20) {
+      return heading;
     }
+  }
 
-    if (topOfViewInside(contentContainer)) {
-      if (stickySubheading.childNodes.length === 0) {
-        // &nbsp; to ensure correct basic line height
-        stickySubheading.appendChild(document.createTextNode('\xA0'));
-      }
+  return null;
+}
 
-      const stickyRect = stickyContainer.getBoundingClientRect();
-      const subheadingRect = stickySubheading.getBoundingClientRect();
-      const stickyBottom = stickyRect.bottom + subheadingRect.height;
-
-      // This array is reversed so that we're starting from the bottom when
-      // iterating over it.
-      for (let i = contentHeadings.length - 1; i >= 0; i--) {
-        const heading = contentHeadings[i];
-        const headingRect = heading.getBoundingClientRect();
-        if (headingRect.y + headingRect.height / 1.5 < stickyBottom + 20) {
-          closestHeading = heading;
-          break;
+function updateStickySubheadingContent(index) {
+  const info = stickyHeadingInfo;
+  const {event, state} = info;
+
+  const closestHeading = getContentHeadingClosestToStickySubheading(index);
+
+  if (state.displayedHeading === closestHeading) return;
+
+  const stickySubheadingRow = info.stickySubheadingRows[index];
+
+  if (closestHeading) {
+    const stickySubheading = info.stickySubheadings[index];
+
+    // Array.from needed to iterate over a live array with for..of
+    for (const child of Array.from(stickySubheading.childNodes)) {
+      child.remove();
+    }
+
+    for (const child of closestHeading.childNodes) {
+      if (child.tagName === 'A') {
+        for (const grandchild of child.childNodes) {
+          stickySubheading.appendChild(grandchild.cloneNode(true));
         }
+      } else {
+        stickySubheading.appendChild(child.cloneNode(true));
       }
     }
 
-    if (state.displayedHeading !== closestHeading) {
-      if (closestHeading) {
-        // Array.from needed to iterate over a live array with for..of
-        for (const child of Array.from(stickySubheading.childNodes)) {
-          child.remove();
-        }
+    stickySubheadingRow.classList.add('visible');
+  } else {
+    stickySubheadingRow.classList.remove('visible');
+  }
 
-        for (const child of closestHeading.childNodes) {
-          if (child.tagName === 'A') {
-            for (const grandchild of child.childNodes) {
-              stickySubheading.appendChild(grandchild.cloneNode(true));
-            }
-          } else {
-            stickySubheading.appendChild(child.cloneNode(true));
-          }
-        }
+  const oldDisplayedHeading = state.displayedHeading;
 
-        stickySubheadingRow.classList.add('visible');
-      } else {
-        stickySubheadingRow.classList.remove('visible');
-      }
+  state.displayedHeading = closestHeading;
 
-      state.displayedHeading = closestHeading;
-    }
+  for (const handler of event.whenDisplayedHeadingChanges) {
+    handler(index, {
+      oldHeading: oldDisplayedHeading,
+      newHeading: closestHeading,
+    });
   }
 }
 
-document.addEventListener('scroll', updateStickyHeading);
-prepareStickyHeadings();
-updateStickyHeading();
+function updateStickyHeadings(index) {
+  updateStickyCoverVisibility(index);
+  updateStickySubheadingContent(index);
+}
+
+function initializeStateForStickyHeadings() {
+  for (let i = 0; i < stickyHeadingInfo.stickyContainers.length; i++) {
+    updateStickyHeadings(i);
+  }
+}
+
+function addScrollListenerForStickyHeadings() {
+  document.addEventListener('scroll', () => {
+    for (let i = 0; i < stickyHeadingInfo.stickyContainers.length; i++) {
+      updateStickyHeadings(i);
+    }
+  });
+}
+
+clientSteps.getPageReferences.push(getStickyHeadingReferences);
+clientSteps.mutatePageContent.push(removeTextPlaceholderStickyHeadingCovers);
+clientSteps.mutatePageContent.push(addRevealClassToStickyHeadingCovers);
+clientSteps.initializeState.push(initializeStateForStickyHeadings);
+clientSteps.addPageListeners.push(addRevealListenersForStickyHeadingCovers);
+clientSteps.addPageListeners.push(addScrollListenerForStickyHeadings);
 
 // Image overlay ------------------------------------------
 
@@ -709,24 +953,48 @@ function handleImageLinkClicked(evt) {
   const mainImage = document.getElementById('image-overlay-image');
   const thumbImage = document.getElementById('image-overlay-image-thumb');
 
-  const mainThumbSize = getPreferredThumbSize();
-
-  const source = evt.target.closest('a').href;
+  const {href: originalSrc} = evt.target.closest('a');
+  const {dataset: {
+    originalSize: originalFileSize,
+    thumbs: availableThumbList,
+  }} = evt.target.closest('a').querySelector('img');
+
+  updateFileSizeInformation(originalFileSize);
+
+  let mainSrc = null;
+  let thumbSrc = null;
+
+  if (availableThumbList) {
+    const {thumb: mainThumb, length: mainLength} = getPreferredThumbSize(availableThumbList);
+    const {thumb: smallThumb, length: smallLength} = getSmallestThumbSize(availableThumbList);
+    mainSrc = originalSrc.replace(/\.(jpg|png)$/, `.${mainThumb}.jpg`);
+    thumbSrc = originalSrc.replace(/\.(jpg|png)$/, `.${smallThumb}.jpg`);
+    // Show the thumbnail size on each <img> element's data attributes.
+    // Y'know, just for debugging convenience.
+    mainImage.dataset.displayingThumb = `${mainThumb}:${mainLength}`;
+    thumbImage.dataset.displayingThumb = `${smallThumb}:${smallLength}`;
+  } else {
+    mainSrc = originalSrc;
+    thumbSrc = null;
+    mainImage.dataset.displayingThumb = '';
+    thumbImage.dataset.displayingThumb = '';
+  }
 
-  const mainSrc = source.replace(/\.(jpg|png)$/, `.${mainThumbSize}.jpg`);
-  const thumbSrc = source.replace(/\.(jpg|png)$/, '.small.jpg');
+  if (thumbSrc) {
+    thumbImage.src = thumbSrc;
+    thumbImage.style.display = null;
+  } else {
+    thumbImage.src = '';
+    thumbImage.style.display = 'none';
+  }
 
-  thumbImage.src = thumbSrc;
   for (const viewOriginal of allViewOriginal) {
-    viewOriginal.href = source;
+    viewOriginal.href = originalSrc;
   }
 
   mainImage.addEventListener('load', handleMainImageLoaded);
   mainImage.addEventListener('error', handleMainImageErrored);
 
-  const fileSize = evt.target.closest('a').querySelector('img').dataset.originalSize;
-  updateFileSizeInformation(fileSize);
-
   container.style.setProperty('--download-progress', '0%');
   loadImage(mainSrc, progress => {
     container.style.setProperty('--download-progress', (20 + 0.8 * progress) + '%');
@@ -750,7 +1018,21 @@ function handleImageLinkClicked(evt) {
   }
 }
 
-function getPreferredThumbSize() {
+function parseThumbList(availableThumbList) {
+  // Parse all the available thumbnail sizes! These are provided by the actual
+  // content generation on each image.
+  const defaultThumbList = 'huge:1400 semihuge:1200 large:800 medium:400 small:250'
+  const availableSizes =
+    (availableThumbList || defaultThumbList)
+      .split(' ')
+      .map(part => part.split(':'))
+      .map(([thumb, length]) => ({thumb, length: parseInt(length)}))
+      .sort((a, b) => a.length - b.length);
+
+  return availableSizes;
+}
+
+function getPreferredThumbSize(availableThumbList) {
   // Assuming a square, the image will be constrained to the lesser window
   // dimension. Coefficient here matches CSS dimensions for image overlay.
   const constrainedLength = Math.floor(Math.min(
@@ -761,17 +1043,30 @@ function getPreferredThumbSize() {
   // device configurations.
   const visualLength = window.devicePixelRatio * constrainedLength;
 
-  const largeLength = 800;
-  const semihugeLength = 1200;
+  const availableSizes = parseThumbList(availableThumbList);
+
+  // Starting from the smallest dimensions, find (and return) the first
+  // available length which hits a "good enough" threshold - it's got to be
+  // at least that percent of the way to the actual displayed dimensions.
   const goodEnoughThreshold = 0.90;
 
-  if (Math.floor(visualLength * goodEnoughThreshold) <= largeLength) {
-    return 'large';
-  } else if (Math.floor(visualLength * goodEnoughThreshold) <= semihugeLength) {
-    return 'semihuge';
-  } else {
-    return 'huge';
+  // (The last item is skipped since we'd be falling back to it anyway.)
+  for (const {thumb, length} of availableSizes.slice(0, -1)) {
+    if (Math.floor(visualLength * goodEnoughThreshold) <= length) {
+      return {thumb, length};
+    }
   }
+
+  // If none of the items in the list were big enough to hit the "good enough"
+  // threshold, just use the largest size available.
+  return availableSizes[availableSizes.length - 1];
+}
+
+function getSmallestThumbSize(availableThumbList) {
+  // Just snag the smallest size. This'll be used for displaying the "preview"
+  // as the bigger one is loading.
+  const availableSizes = parseThumbList(availableThumbList);
+  return availableSizes[0];
 }
 
 function updateFileSizeInformation(fileSize) {
@@ -913,3 +1208,227 @@ for (const info of groupContributionsTableInfo) {
     sortGroupContributionsTableBy(info, 'count');
   });
 }
+
+// Sticky commentary sidebar ------------------------------
+
+const albumCommentarySidebarInfo = clientInfo.albumCommentarySidebarInfo = {
+  sidebar: null,
+
+  sidebarTrackLinks: null,
+  sidebarTrackDirectories: null,
+
+  sidebarTrackSections: null,
+  sidebarTrackSectionStartIndices: null,
+
+  state: {
+    currentTrackSection: null,
+    currentTrackLink: null,
+    justChangedTrackSection: false,
+  },
+};
+
+function getAlbumCommentarySidebarReferences() {
+  const info = albumCommentarySidebarInfo;
+
+  info.sidebar =
+    document.getElementById('sidebar-left');
+
+  info.sidebarHeading =
+    info.sidebar.querySelector('h1');
+
+  info.sidebarTrackLinks =
+    Array.from(info.sidebar.querySelectorAll('li a'));
+
+  info.sidebarTrackDirectories =
+    info.sidebarTrackLinks
+      .map(el => el.getAttribute('href')?.slice(1) ?? null);
+
+  info.sidebarTrackSections =
+    Array.from(info.sidebar.getElementsByTagName('details'));
+
+  info.sidebarTrackSectionStartIndices =
+    info.sidebarTrackSections
+      .map(details => details.querySelector('ol, ul'))
+      .reduce(
+        (accumulator, _list, index, array) =>
+          (empty(accumulator)
+            ? [0]
+            : [
+              ...accumulator,
+              (accumulator[accumulator.length - 1] +
+                array[index - 1].querySelectorAll('li a').length),
+            ]),
+        []);
+}
+
+function scrollAlbumCommentarySidebar() {
+  const info = albumCommentarySidebarInfo;
+  const {state} = info;
+  const {currentTrackLink, currentTrackSection} = state;
+
+  if (!currentTrackLink) {
+    return;
+  }
+
+  const {sidebar, sidebarHeading} = info;
+
+  const scrollTop = sidebar.scrollTop;
+
+  const headingRect = sidebarHeading.getBoundingClientRect();
+  const sidebarRect = sidebar.getBoundingClientRect();
+
+  const stickyPadding = headingRect.height;
+  const sidebarViewportHeight = sidebarRect.height - stickyPadding;
+
+  const linkRect = currentTrackLink.getBoundingClientRect();
+  const sectionRect = currentTrackSection.getBoundingClientRect();
+
+  const sectionTopEdge =
+    sectionRect.top - (sidebarRect.top - scrollTop);
+
+  const sectionHeight =
+    sectionRect.height;
+
+  const sectionScrollTop =
+    sectionTopEdge - stickyPadding - 10;
+
+  const linkTopEdge =
+    linkRect.top - (sidebarRect.top - scrollTop);
+
+  const linkBottomEdge =
+    linkRect.bottom - (sidebarRect.top - scrollTop);
+
+  const linkScrollTop =
+    linkTopEdge - stickyPadding - 5;
+
+  const linkDistanceFromSection =
+    linkScrollTop - sectionTopEdge;
+
+  const linkVisibleFromTopOfSection =
+    linkBottomEdge - sectionTopEdge > sidebarViewportHeight;
+
+  const linkScrollBottom =
+    linkScrollTop - sidebarViewportHeight + linkRect.height + 20;
+
+  const maxScrollInViewport =
+    scrollTop + stickyPadding + sidebarViewportHeight;
+
+  const minScrollInViewport =
+    scrollTop + stickyPadding;
+
+  if (linkBottomEdge > maxScrollInViewport) {
+    if (linkVisibleFromTopOfSection) {
+      sidebar.scrollTo({top: linkScrollBottom, behavior: 'smooth'});
+    } else {
+      sidebar.scrollTo({top: sectionScrollTop, behavior: 'smooth'});
+    }
+  } else if (linkTopEdge < minScrollInViewport) {
+    if (linkVisibleFromTopOfSection) {
+      sidebar.scrollTo({top: linkScrollTop, behavior: 'smooth'});
+    } else {
+      sidebar.scrollTo({top: sectionScrollTop, behavior: 'smooth'});
+    }
+  } else if (state.justChangedTrackSection) {
+    if (sectionHeight < sidebarViewportHeight) {
+      sidebar.scrollTo({top: sectionScrollTop, behavior: 'smooth'});
+    }
+  }
+}
+
+function markDirectoryAsCurrentForAlbumCommentary(trackDirectory) {
+  const info = albumCommentarySidebarInfo;
+  const {state} = info;
+
+  const trackIndex =
+    (trackDirectory
+      ? info.sidebarTrackDirectories
+          .indexOf(trackDirectory)
+      : -1);
+
+  const sectionIndex =
+    (trackIndex >= 0
+      ? info.sidebarTrackSectionStartIndices
+          .findIndex((start, index, array) =>
+            (index === array.length - 1
+              ? true
+              : trackIndex < array[index + 1]))
+      : -1);
+
+  const sidebarTrackLink =
+    (trackIndex >= 0
+      ? info.sidebarTrackLinks[trackIndex]
+      : null);
+
+  const sidebarTrackSection =
+    (sectionIndex >= 0
+      ? info.sidebarTrackSections[sectionIndex]
+      : null);
+
+  state.currentTrackLink?.classList?.remove('current');
+  state.currentTrackLink = sidebarTrackLink;
+  state.currentTrackLink?.classList?.add('current');
+
+  if (sidebarTrackSection !== state.currentTrackSection) {
+    if (sidebarTrackSection && !sidebarTrackSection.open) {
+      if (state.currentTrackSection) {
+        state.currentTrackSection.open = false;
+      }
+
+      sidebarTrackSection.open = true;
+    }
+
+    state.currentTrackSection?.classList?.remove('current');
+    state.currentTrackSection = sidebarTrackSection;
+    state.currentTrackSection?.classList?.add('current');
+    state.justChangedTrackSection = true;
+  } else {
+    state.justChangedTrackSection = false;
+  }
+}
+
+function addAlbumCommentaryInternalListeners() {
+  const info = albumCommentarySidebarInfo;
+
+  const mainContentIndex =
+    (stickyHeadingInfo.contentContainers ?? [])
+      .findIndex(({id}) => id === 'content');
+
+  if (mainContentIndex === -1) return;
+
+  stickyHeadingInfo.event.whenDisplayedHeadingChanges.push((index, {newHeading}) => {
+    if (index !== mainContentIndex) return;
+    if (hashLinkInfo.state.scrollingAfterClick) return;
+
+    const trackDirectory =
+      (newHeading
+        ? newHeading.id
+        : null);
+
+    markDirectoryAsCurrentForAlbumCommentary(trackDirectory);
+    scrollAlbumCommentarySidebar();
+  });
+
+  hashLinkInfo.event.whenHashLinkClicked.push(({link}) => {
+    const hash = link.getAttribute('href').slice(1);
+    if (!info.sidebarTrackDirectories.includes(hash)) return;
+    markDirectoryAsCurrentForAlbumCommentary(hash);
+  });
+}
+
+if (document.documentElement.dataset.urlKey === 'localized.albumCommentary') {
+  clientSteps.getPageReferences.push(getAlbumCommentarySidebarReferences);
+  clientSteps.addInternalListeners.push(addAlbumCommentaryInternalListeners);
+}
+
+// Run setup steps ----------------------------------------
+
+for (const [key, steps] of Object.entries(clientSteps)) {
+  for (const step of steps) {
+    try {
+      step();
+    } catch (error) {
+      console.warn(`During ${key}, failed to run ${step.name}`);
+      console.debug(error);
+    }
+  }
+}
diff --git a/src/static/site4.css b/src/static/site5.css
index f79c0c2..0eb7dcd 100644
--- a/src/static/site4.css
+++ b/src/static/site5.css
@@ -285,6 +285,8 @@ body::before {
 .sidebar > h3,
 .sidebar > p {
   text-align: center;
+  padding-left: 4px;
+  padding-right: 4px;
 }
 
 .sidebar h1 {
@@ -437,6 +439,14 @@ a.current {
   font-weight: 800;
 }
 
+a:not([href]) {
+  cursor: default;
+}
+
+a:not([href]):hover {
+  text-decoration: none;
+}
+
 .nav-main-links > span > span {
   white-space: nowrap;
 }
@@ -533,6 +543,13 @@ p .current {
   margin-top: 5px;
 }
 
+.commentary-art {
+  float: right;
+  width: 30%;
+  max-width: 250px;
+  margin: 15px 0 10px 20px;
+}
+
 .js-hide,
 .js-show-once-data,
 .js-hide-once-data {
@@ -558,7 +575,7 @@ a.box img {
   height: auto;
 }
 
-a.box .square .image-container {
+.square .image-container {
   width: 100%;
   height: 100%;
 }
@@ -680,10 +697,14 @@ p code {
   margin-bottom: 0;
 }
 
+main.long-content {
+  --long-content-padding-ratio: 0.12;
+}
+
 main.long-content .main-content-container,
 main.long-content > h1 {
-  padding-left: 12%;
-  padding-right: 12%;
+  padding-left: calc(var(--long-content-padding-ratio) * 100%);
+  padding-right: calc(var(--long-content-padding-ratio) * 100%);
 }
 
 dl dt {
@@ -773,6 +794,10 @@ li > ul {
   display: none;
 }
 
+html[data-url-key="localized.albumCommentary"] li.no-commentary {
+  opacity: 0.7;
+}
+
 /* Images */
 
 .image-container {
@@ -1250,6 +1275,10 @@ html[data-url-key="localized.home"] .carousel-container {
   animation-delay: 125ms;
 }
 
+h3.content-heading {
+  clear: both;
+}
+
 /* This animation's name is referenced in JavaScript */
 @keyframes highlight-hash-link {
   0% {
@@ -1298,8 +1327,8 @@ main.long-content .content-sticky-heading-container {
 
 main.long-content .content-sticky-heading-container .content-sticky-heading-row,
 main.long-content .content-sticky-heading-container .content-sticky-subheading-row {
-  padding-left: calc(0.12 * (100% - 2 * var(--content-padding)) + var(--content-padding));
-  padding-right: calc(0.12 * (100% - 2 * var(--content-padding)) + var(--content-padding));
+  padding-left: calc(var(--long-content-padding-ratio) * (100% - 2 * var(--content-padding)) + var(--content-padding));
+  padding-right: calc(var(--long-content-padding-ratio) * (100% - 2 * var(--content-padding)) + var(--content-padding));
 }
 
 .content-sticky-heading-row {
@@ -1438,6 +1467,45 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r
   align-self: flex-start;
 }
 
+.sidebar-column.sidebar.sticky-column {
+  max-height: calc(100vh - 20px);
+  align-self: start;
+  padding-bottom: 0;
+  box-sizing: border-box;
+  flex-basis: 275px;
+  padding-top: 0;
+  overflow-y: scroll;
+  scrollbar-width: thin;
+  scrollbar-color: var(--dark-color);
+}
+
+.sidebar-column.sidebar.sticky-column::-webkit-scrollbar {
+  background: var(--dark-color);
+  width: 12px;
+}
+
+.sidebar-column.sidebar.sticky-column::-webkit-scrollbar-thumb {
+  transition: background 0.2s;
+  background: rgba(255, 255, 255, 0.2);
+  border: 3px solid transparent;
+  border-radius: 10px;
+  background-clip: content-box;
+}
+
+.sidebar-column.sidebar.sticky-column > h1 {
+  position: sticky;
+  top: 0;
+  margin: 0 calc(-1 * var(--content-padding));
+  margin-bottom: 10px;
+
+  border-bottom: 1px dotted rgba(220, 220, 220, 0.4);
+  padding: 10px 5px;
+
+  background: var(--bg-black-color);
+  -webkit-backdrop-filter: blur(3px);
+  backdrop-filter: blur(3px);
+}
+
 /* Image overlay */
 
 #image-overlay-container {
@@ -1575,7 +1643,7 @@ html[data-language-code="preview-en"][data-url-key="localized.home"] #content
 /* Layout - Wide (most computers) */
 
 @media (min-width: 900px) {
-  #secondary-nav:not(.no-hide) {
+  #page-container:not(.has-zero-sidebars) #secondary-nav {
     display: none;
   }
 }
@@ -1617,12 +1685,12 @@ html[data-language-code="preview-en"][data-url-key="localized.home"] #content
     z-index: 2;
   }
 
-  html[data-url-key="localized.home"] .layout-columns.has-one-sidebar .grid-listing > .grid-item:not(:nth-child(n+10)) {
+  html[data-url-key="localized.home"] #page-container.has-one-sidebar .grid-listing > .grid-item:not(:nth-child(n+10)) {
     flex-basis: 23%;
     margin: 15px;
   }
 
-  html[data-url-key="localized.home"] .layout-columns.has-one-sidebar .grid-listing > .grid-item:nth-child(n+10) {
+  html[data-url-key="localized.home"] #page-container.has-one-sidebar .grid-listing > .grid-item:nth-child(n+10) {
     flex-basis: 18%;
     margin: 10px;
   }
@@ -1692,4 +1760,8 @@ html[data-language-code="preview-en"][data-url-key="localized.home"] #content
   #header > div:not(:first-child) {
     margin-top: 0.5em;
   }
+
+  main.long-content {
+    --long-content-padding-ratio: 0.04;
+  }
 }
diff --git a/src/strings-default.json b/src/strings-default.json
index e893a5c..6c841e7 100644
--- a/src/strings-default.json
+++ b/src/strings-default.json
@@ -197,6 +197,7 @@
   "misc.external.flash.homestuck.page": "{LINK} (page {PAGE})",
   "misc.external.flash.homestuck.secret": "{LINK} (secret page)",
   "misc.external.flash.youtube": "{LINK} (on any device)",
+  "misc.missingImage": "(This image file is missing)",
   "misc.missingLinkContent": "(Missing link content)",
   "misc.nav.previous": "Previous",
   "misc.nav.next": "Next",
@@ -266,6 +267,7 @@
   "albumGalleryPage.statsLine": "{TRACKS} totaling {DURATION}.",
   "albumGalleryPage.statsLine.withDate": "{TRACKS} totaling {DURATION}. Released {DATE}.",
   "albumGalleryPage.coverArtistsLine": "All track artwork by {ARTISTS}.",
+  "albumGalleryPage.noTrackArtworksLine": "This album doesn't have any track artwork.",
   "albumCommentaryPage.title": "{ALBUM} - Commentary",
   "albumCommentaryPage.infoLine": "{WORDS} across {ENTRIES}.",
   "albumCommentaryPage.nav.album": "Album: {ALBUM}",
@@ -321,6 +323,8 @@
   "flashIndex.title": "Flashes & Games",
   "flashPage.title": "{FLASH}",
   "flashPage.nav.flash": "{FLASH}",
+  "flashSidebar.flashList.flashesInThisAct": "Flashes in this act",
+  "flashSidebar.flashList.entriesInThisSection": "Entries in this section",
   "groupSidebar.title": "Groups",
   "groupSidebar.groupList.category": "{CATEGORY}",
   "groupSidebar.groupList.item": "{GROUP}",
@@ -335,8 +339,6 @@
   "groupInfoPage.albumList.item.otherGroupAccent": "(from {GROUP})",
   "groupGalleryPage.title": "{GROUP} - Gallery",
   "groupGalleryPage.infoLine": "{TRACKS} across {ALBUMS}, totaling {TIME}.",
-  "groupGalleryPage.anotherGroupLine": "({LINK})",
-  "groupGalleryPage.anotherGroupLine.link": "Choose another group to filter by!",
   "listingIndex.title": "Listings",
   "listingIndex.infoLine": "{WIKI}: {TRACKS} across {ALBUMS}, totaling {DURATION}.",
   "listingIndex.exploreList": "Feel free to explore any of the listings linked below and in the sidebar!",
@@ -447,15 +449,18 @@
   "listingPage.listTracks.inFlashes.byFlash.chunk.item": "{TRACK} (from {ALBUM})",
   "listingPage.listTracks.withLyrics.title": "Tracks - with Lyrics",
   "listingPage.listTracks.withLyrics.title.short": "...with Lyrics",
-  "listingPage.listTracks.withLyrics.chunk.title": "{ALBUM} ({DATE})",
+  "listingPage.listTracks.withLyrics.chunk.title": "{ALBUM}",
+  "listingPage.listTracks.withLyrics.chunk.title.withDate": "{ALBUM} ({DATE})",
   "listingPage.listTracks.withLyrics.chunk.item": "{TRACK}",
   "listingPage.listTracks.withSheetMusicFiles.title": "Tracks - with Sheet Music Files",
   "listingPage.listTracks.withSheetMusicFiles.title.short": "...with Sheet Music Files",
-  "listingPage.listTracks.withSheetMusicFiles.chunk.title": "{ALBUM} ({DATE})",
+  "listingPage.listTracks.withSheetMusicFiles.chunk.title": "{ALBUM}",
+  "listingPage.listTracks.withSheetMusicFiles.chunk.title.withDate": "{ALBUM} ({DATE})",
   "listingPage.listTracks.withSheetMusicFiles.chunk.item": "{TRACK}",
   "listingPage.listTracks.withMidiProjectFiles.title": "Tracks - with MIDI & Project Files",
   "listingPage.listTracks.withMidiProjectFiles.title.short": "...with MIDI & Project Files",
-  "listingPage.listTracks.withMidiProjectFiles.chunk.title": "{ALBUM} ({DATE})",
+  "listingPage.listTracks.withMidiProjectFiles.chunk.title": "{ALBUM}",
+  "listingPage.listTracks.withMidiProjectFiles.chunk.title.withDate": "{ALBUM} ({DATE})",
   "listingPage.listTracks.withMidiProjectFiles.chunk.item": "{TRACK}",
   "listingPage.listTags.byName.title": "Tags - by Name",
   "listingPage.listTags.byName.title.short": "...by Name",
diff --git a/src/upd8.js b/src/upd8.js
index bfdd1c2..27445a8 100755
--- a/src/upd8.js
+++ b/src/upd8.js
@@ -32,11 +32,13 @@
 // node.js and you'll 8e fine. ...Within the project root. O8viously.
 
 import {execSync} from 'node:child_process';
+import {readFile} from 'node:fs/promises';
 import * as path from 'node:path';
 import {fileURLToPath} from 'node:url';
 
 import wrap from 'word-wrap';
 
+import {displayCompositeCacheAnalysis} from '#composite';
 import {processLanguageFile} from '#language';
 import {isMain, traverse} from '#node-utils';
 import bootRepl from '#repl';
@@ -46,8 +48,9 @@ import {generateURLs, urlSpec} from '#urls';
 import {sortByName} from '#wiki-data';
 
 import {
-  color,
+  colors,
   decorateTime,
+  fileIssue,
   logWarn,
   logInfo,
   logError,
@@ -57,6 +60,7 @@ import {
 } from '#cli';
 
 import genThumbs, {
+  CACHE_FILE as thumbsCacheFile,
   clearThumbs,
   defaultMagickThreads,
   isThumb,
@@ -78,7 +82,7 @@ import * as buildModes from './write/build-modes/index.js';
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
 
-const CACHEBUST = 20;
+const CACHEBUST = 22;
 
 let COMMIT;
 try {
@@ -91,9 +95,64 @@ const BUILD_TIME = new Date();
 
 const DEFAULT_STRINGS_FILE = 'strings-default.json';
 
+const STATUS_NOT_STARTED       = `not started`;
+const STATUS_NOT_APPLICABLE    = `not applicable`;
+const STATUS_STARTED_NOT_DONE  = `started but not yet done`;
+const STATUS_DONE_CLEAN        = `done without warnings`;
+const STATUS_FATAL_ERROR       = `fatal error`;
+const STATUS_HAS_WARNINGS      = `has warnings`;
+
+const defaultStepStatus = {status: STATUS_NOT_STARTED, annotation: null};
+
+// Defined globally for quick access outside the main() function's contents.
+// This will be initialized and mutated over the course of main().
+let stepStatusSummary;
+let showStepStatusSummary = false;
+
 async function main() {
   Error.stackTraceLimit = Infinity;
 
+  stepStatusSummary = {
+    loadThumbnailCache:
+      {...defaultStepStatus, name: `load thumbnail cache file`},
+
+    generateThumbnails:
+      {...defaultStepStatus, name: `generate thumbnails`},
+
+    loadDataFiles:
+      {...defaultStepStatus, name: `load and process data files`},
+
+    linkWikiDataArrays:
+      {...defaultStepStatus, name: `link wiki data arrays`},
+
+    filterDuplicateDirectories:
+      {...defaultStepStatus, name: `filter duplicate directories`},
+
+    filterReferenceErrors:
+      {...defaultStepStatus, name: `filter reference errors`},
+
+    sortWikiDataArrays:
+      {...defaultStepStatus, name: `sort wiki data arrays`},
+
+    precacheData:
+      {...defaultStepStatus, name: `precache data`},
+
+    loadInternalDefaultLanguage:
+      {...defaultStepStatus, name: `load internal default language`},
+
+    loadLanguageFiles:
+      {...defaultStepStatus, name: `load custom language files`},
+
+    initializeDefaultLanguage:
+      {...defaultStepStatus, name: `initialize default language`},
+
+    preloadFileSizes:
+      {...defaultStepStatus, name: `preload file sizes`},
+
+    performBuild:
+      {...defaultStepStatus, name: `perform selected build mode`},
+  };
+
   const defaultQueueSize = 500;
 
   const buildModeFlagOptions = (
@@ -118,7 +177,7 @@ async function main() {
   } else if (selectedBuildModeFlags.length > 1) {
     logError`Building multiple modes (${selectedBuildModeFlags.join(', ')}) at once not supported.`;
     logError`Please specify a maximum of one build mode.`;
-    return;
+    return false;
   } else {
     selectedBuildModeFlag = selectedBuildModeFlags[0];
     usingDefaultBuildMode = false;
@@ -218,6 +277,11 @@ async function main() {
       type: 'flag',
     },
 
+    'show-step-summary': {
+      help: `Show a summary of all the top-level build steps once hsmusic exits. This is mostly useful for progammer debugging!`,
+      type: 'flag',
+    },
+
     'queue-size': {
       help: `Process more or fewer disk files at once to optimize performance or avoid I/O errors, unlimited if set to 0 (between 500 and 700 is usually a safe range for building HSMusic on Windows machines)\nDefaults to ${defaultQueueSize}`,
       type: 'value',
@@ -231,6 +295,12 @@ async function main() {
 
     'magick-threads': {
       help: `Process more or fewer thumbnail files at once with ImageMagick when generating thumbnails. (Each ImageMagick thread may also make use of multi-core processing at its own utility.)`,
+      type: 'value',
+      validate(threads) {
+        if (parseInt(threads) !== parseFloat(threads)) return 'an integer';
+        if (parseInt(threads) < 0) return 'a counting number or zero';
+        return true;
+      }
     },
     magick: {alias: 'magick-threads'},
 
@@ -271,7 +341,7 @@ async function main() {
     const indentWrap = (spaces, str) => wrap(str, {width: 60 - spaces, indent: ' '.repeat(spaces)});
 
     const showOptions = (msg, options) => {
-      console.log(color.bright(msg));
+      console.log(colors.bright(msg));
 
       const entries = Object.entries(options);
       const sortedOptions = sortByName(entries
@@ -302,13 +372,13 @@ async function main() {
           console.log('');
         }
 
-        console.log(color.bright(` --` + name) +
+        console.log(colors.bright(` --` + name) +
           (aliases.length
-            ? ` (or: ${aliases.map(alias => color.bright(`--` + alias)).join(', ')})`
+            ? ` (or: ${aliases.map(alias => colors.bright(`--` + alias)).join(', ')})`
             : '') +
           (descriptor.help
             ? ''
-            : color.dim('  (no help provided)')));
+            : colors.dim('  (no help provided)')));
 
         if (wrappedHelp) {
           console.log(wrappedHelp);
@@ -328,7 +398,7 @@ async function main() {
     };
 
     console.log(
-      color.bright(`hsmusic (aka. Homestuck Music Wiki)\n`) +
+      colors.bright(`hsmusic (aka. Homestuck Music Wiki)\n`) +
       `static wiki software cataloguing collaborative creation\n`);
 
     console.log(indentWrap(0,
@@ -352,7 +422,7 @@ async function main() {
       })`, buildOptions);
     }
 
-    return;
+    return true;
   }
 
   const dataPath = cliOptions['data-path'] || process.env.HSMUSIC_DATA;
@@ -364,6 +434,8 @@ async function main() {
   const clearThumbsFlag = cliOptions['clear-thumbs'] ?? false;
   const noBuild = cliOptions['no-build'] ?? false;
 
+  showStepStatusSummary = cliOptions['show-step-summary'] ?? false;
+
   const replFlag = cliOptions['repl'] ?? false;
   const disableReplHistory = cliOptions['no-repl-history'] ?? false;
 
@@ -379,19 +451,16 @@ async function main() {
 
   const magickThreads = +(cliOptions['magick-threads'] ?? defaultMagickThreads);
 
-  {
-    let errored = false;
-    const error = (cond, msg) => {
-      if (cond) {
-        console.error(`\x1b[31;1m${msg}\x1b[0m`);
-        errored = true;
-      }
-    };
-    error(!dataPath, `Expected --data-path option or HSMUSIC_DATA to be set`);
-    error(!mediaPath, `Expected --media-path option or HSMUSIC_MEDIA to be set`);
-    if (errored) {
-      return;
-    }
+  if (!dataPath) {
+    logError`${`Expected --data-path option or HSMUSIC_DATA to be set`}`;
+  }
+
+  if (!mediaPath) {
+    logError`${`Expected --media-path option or HSMUSIC_MEDIA to be set`}`;
+  }
+
+  if (!dataPath || !mediaPath) {
+    return false;
   }
 
   if (replFlag) {
@@ -414,31 +483,103 @@ async function main() {
 
   if (skipThumbs && thumbsOnly) {
     logInfo`Well, you've put yourself rather between a roc and a hard place, hmmmm?`;
-    return;
+    return false;
   }
 
   if (clearThumbsFlag) {
-    await clearThumbs(mediaPath, {queueSize});
-
-    logInfo`All done! Remove ${'--clear-thumbs'} to run the next build.`;
-    if (skipThumbs) {
-      logInfo`And don't forget to remove ${'--skip-thumbs'} too, eh?`;
+    const result = await clearThumbs(mediaPath, {queueSize});
+    if (result.success) {
+      logInfo`All done! Remove ${'--clear-thumbs'} to run the next build.`;
+      if (skipThumbs) {
+        logInfo`And don't forget to remove ${'--skip-thumbs'} too, eh?`;
+      }
     }
-    return;
+    return true;
   }
 
+  let thumbsCache;
+
   if (skipThumbs) {
+    Object.assign(stepStatusSummary.generateThumbnails, {
+      status: STATUS_NOT_APPLICABLE,
+      annotation: `provided --skip-thumbs`,
+    });
+
+    stepStatusSummary.loadThumbnailCache.status = STATUS_STARTED_NOT_DONE;
+
+    const thumbsCachePath = path.join(mediaPath, thumbsCacheFile);
+
+    try {
+      thumbsCache = JSON.parse(await readFile(thumbsCachePath));
+      logInfo`Thumbnail cache file successfully read.`;
+      stepStatusSummary.loadThumbnailCache.status = STATUS_DONE_CLEAN;
+    } catch (error) {
+      if (error.code === 'ENOENT') {
+        logError`The thumbnail cache doesn't exist, and it's necessary to build`
+        logError`the website. Please run once without ${'--skip-thumbs'} - after`
+        logError`that you'll be good to go and don't need to process thumbnails`
+        logError`again!`;
+
+        Object.assign(stepStatusSummary.loadThumbnailCache, {
+          status: STATUS_FATAL_ERROR,
+          annotation: `cache does not exist`,
+        });
+
+        return false;
+      } else {
+        logError`Malformed or unreadable thumbnail cache file: ${error}`;
+        logError`Path: ${thumbsCachePath}`;
+        logError`The thumbbnail cache is necessary to build the site, so you'll`;
+        logError`have to investigate this to get the build working. Try running`;
+        logError`again without ${'--skip-thumbs'}. If you can't get it working,`;
+        logError`you're welcome to message in the HSMusic Discord and we'll try`;
+        logError`to help you out with troubleshooting!`;
+        logError`${'https://hsmusic.wiki/discord/'}`;
+
+        Object.assign(stepStatusSummary.loadThumbnailCache, {
+          status: STATUS_FATAL_ERROR,
+          annotation: `cache malformed or unreadable`,
+        });
+
+        return false;
+      }
+    }
+
     logInfo`Skipping thumbnail generation.`;
   } else {
+    Object.assign(stepStatusSummary.loadThumbnailCache, {
+      status: STATUS_NOT_APPLICABLE,
+      annotation: `using cache from thumbnail generation`,
+    });
+
+    stepStatusSummary.generateThumbnails.status = STATUS_STARTED_NOT_DONE;
+
     logInfo`Begin thumbnail generation... -----+`;
+
     const result = await genThumbs(mediaPath, {
       queueSize,
       magickThreads,
       quiet: !thumbsOnly,
     });
+
     logInfo`Done thumbnail generation! --------+`;
-    if (!result) return;
-    if (thumbsOnly) return;
+
+    if (!result.success) {
+      Object.assign(stepStatusSummary.generateThumbnails, {
+        status: STATUS_FATAL_ERROR,
+        annotation: `view log for details`,
+      });
+
+      return false;
+    }
+
+    stepStatusSummary.generateThumbnails.status = STATUS_DONE_CLEAN;
+
+    if (thumbsOnly) {
+      return true;
+    }
+
+    thumbsCache = result.cache;
   }
 
   if (noBuild) {
@@ -453,14 +594,32 @@ async function main() {
     CacheableObject.DEBUG_SLOW_TRACK_INVALID_PROPERTIES = true;
   }
 
-  const {aggregate: processDataAggregate, result: wikiDataResult} =
-    await loadAndProcessDataDocuments({dataPath});
+  stepStatusSummary.loadDataFiles.status = STATUS_STARTED_NOT_DONE;
+
+  let processDataAggregate, wikiDataResult;
+
+  try {
+    ({aggregate: processDataAggregate, result: wikiDataResult} =
+        await loadAndProcessDataDocuments({dataPath}));
+  } catch (error) {
+    console.error(error);
+
+    logError`There was a JavaScript error loading data files.`;
+    fileIssue();
+
+    Object.assign(stepStatusSummary.loadDataFiles, {
+      status: STATUS_FATAL_ERROR,
+      annotation: `javascript error - view log for details`,
+    });
+
+    return false;
+  }
 
   Object.assign(wikiData, wikiDataResult);
 
   {
     const logThings = (thingDataProp, label) =>
-      logInfo` - ${wikiData[thingDataProp]?.length ?? color.red('(Missing!)')} ${color.normal(color.dim(label))}`;
+      logInfo` - ${wikiData[thingDataProp]?.length ?? colors.red('(Missing!)')} ${colors.normal(colors.dim(label))}`;
     try {
       logInfo`Loaded data and processed objects:`;
       logThings('albumData', 'albums');
@@ -499,84 +658,105 @@ async function main() {
       logWarn`still build - but all errored data will be skipped.`;
       logWarn`(Resolve errors for more complete output!)`;
       errorless = false;
-    }
 
-    if (errorless) {
-      logInfo`All data processed without any errors - nice!`;
-      logInfo`(This means all source files will be fully accounted for during page generation.)`;
+      Object.assign(stepStatusSummary.loadDataFiles, {
+        status: STATUS_HAS_WARNINGS,
+        annotation: `view log for details`,
+      });
     }
-  }
 
-  if (!wikiData.wikiInfo) {
-    logError`Can't proceed without wiki info file (${WIKI_INFO_FILE}) successfully loading`;
-    return;
-  }
+    if (!wikiData.wikiInfo) {
+      logError`Can't proceed without wiki info file (${WIKI_INFO_FILE}) successfully loading`;
 
-  let duplicateDirectoriesErrored = false;
+      Object.assign(stepStatusSummary.loadDataFiles, {
+        status: STATUS_FATAL_ERROR,
+        annotation: `wiki info object not available`,
+      });
 
-  function filterAndShowDuplicateDirectories() {
-    const aggregate = filterDuplicateDirectories(wikiData);
-    let errorless = true;
-    try {
-      aggregate.close();
-    } catch (aggregate) {
-      niceShowAggregate(aggregate);
-      logWarn`The above duplicate directories were detected while reviewing data files.`;
-      logWarn`Each thing listed above will been totally excempt from this build of the site!`;
-      logWarn`Specify unique 'Directory' fields in data entries to resolve these.`;
-      logWarn`${`Note:`} This will probably result in reference errors below.`;
-      logWarn`${`. . .`} You should fix duplicate directories first!`;
-      logWarn`(Resolve errors for more complete output!)`;
-      duplicateDirectoriesErrored = true;
-      errorless = false;
-    }
-    if (errorless) {
-      logInfo`No duplicate directories found - nice!`;
+      return false;
     }
-  }
 
-  function filterAndShowReferenceErrors() {
-    const aggregate = filterReferenceErrors(wikiData);
-    let errorless = true;
-    try {
-      aggregate.close();
-    } catch (error) {
-      niceShowAggregate(error);
-      logWarn`The above errors were detected while validating references in data files.`;
-      logWarn`If the remaining valid data is complete enough, the wiki will still build -`;
-      logWarn`but all errored references will be skipped.`;
-      if (duplicateDirectoriesErrored) {
-        logWarn`${`Note:`} Duplicate directories were found as well. Review those first,`;
-        logWarn`${`. . .`} as they may have caused some of the errors detected above.`;
-      }
-      logWarn`(Resolve errors for more complete output!)`;
-      errorless = false;
-    }
     if (errorless) {
-      logInfo`All references validated without any errors - nice!`;
-      logInfo`(This means all references between things, such as leitmotif references`;
-      logInfo` and artist credits, will be fully accounted for during page generation.)`;
+      logInfo`All data files processed without any errors - nice!`;
+      stepStatusSummary.loadDataFiles.status = STATUS_DONE_CLEAN;
     }
   }
 
   // Link data arrays so that all essential references between objects are
   // complete, so properties (like dates!) are inherited where that's
   // appropriate.
+
+  stepStatusSummary.linkWikiDataArrays.status = STATUS_STARTED_NOT_DONE;
+
   linkWikiDataArrays(wikiData);
 
+  stepStatusSummary.linkWikiDataArrays.status = STATUS_DONE_CLEAN;
+
   // Filter out any things with duplicate directories throughout the data,
   // warning about them too.
-  filterAndShowDuplicateDirectories();
+
+  stepStatusSummary.filterDuplicateDirectories.status = STATUS_STARTED_NOT_DONE;
+
+  const filterDuplicateDirectoriesAggregate =
+    filterDuplicateDirectories(wikiData);
+
+  try {
+    filterDuplicateDirectoriesAggregate.close();
+    logInfo`No duplicate directories found - nice!`;
+    stepStatusSummary.filterDuplicateDirectories.status = STATUS_DONE_CLEAN;
+  } catch (aggregate) {
+    niceShowAggregate(aggregate);
+
+    logWarn`The above duplicate directories were detected while reviewing data files.`;
+    logWarn`Since it's impossible to automatically determine which one's directory is`;
+    logWarn`correct, the build can't continue. Specify unique 'Directory' fields in`;
+    logWarn`some or all of these data entries to resolve the errors.`;
+
+    Object.assign(stepStatusSummary.filterDuplicateDirectories, {
+      status: STATUS_FATAL_ERROR,
+      annotation: `duplicate directories found`,
+    });
+
+    return false;
+  }
 
   // Filter out any reference errors throughout the data, warning about them
   // too.
-  filterAndShowReferenceErrors();
+
+  stepStatusSummary.filterReferenceErrors.status = STATUS_STARTED_NOT_DONE;
+
+  const filterReferenceErrorsAggregate = filterReferenceErrors(wikiData);
+
+  try {
+    filterReferenceErrorsAggregate.close();
+    logInfo`All references validated without any errors - nice!`;
+    stepStatusSummary.filterReferenceErrors.status = STATUS_DONE_CLEAN;
+  } catch (error) {
+    niceShowAggregate(error);
+
+    logWarn`The above errors were detected while validating references in data files.`;
+    logWarn`The wiki will still build, but these connections between data objects`;
+    logWarn`will be completely skipped. Resolve the errors for more complete output.`;
+
+    Object.assign(stepStatusSummary.filterReferenceErrors, {
+      status: STATUS_HAS_WARNINGS,
+      annotation: `view log for details`,
+    });
+  }
 
   // Sort data arrays so that they're all in order! This may use properties
   // which are only available after the initial linking.
+
+  stepStatusSummary.sortWikiDataArrays.status = STATUS_STARTED_NOT_DONE;
+
   sortWikiDataArrays(wikiData);
 
+  stepStatusSummary.sortWikiDataArrays.status = STATUS_DONE_CLEAN;
+
   if (precacheData) {
+    stepStatusSummary.precacheData.status = STATUS_STARTED_NOT_DONE;
+
+    // TODO: Aggregate errors here, instead of just throwing.
     progressCallAll('Caching all data values', Object.entries(wikiData)
       .filter(([key]) =>
         key !== 'listingSpec' &&
@@ -587,27 +767,96 @@ async function main() {
         [key, value])
       .flatMap(([_key, things]) => things)
       .map(thing => () => CacheableObject.cacheAllExposedProperties(thing)));
+
+    stepStatusSummary.precacheData.status = STATUS_DONE_CLEAN;
+  } else {
+    Object.assign(stepStatusSummary.precacheData, {
+      status: STATUS_NOT_APPLICABLE,
+      annotation: `--precache-data not provided`,
+    });
+  }
+
+  if (noBuild) {
+    Object.assign(stepStatusSummary.performBuild, {
+      status: STATUS_NOT_APPLICABLE,
+      annotation: `--no-build provided`,
+    });
+
+    displayCompositeCacheAnalysis();
+
+    if (precacheData) {
+      return true;
+    }
   }
 
-  const internalDefaultLanguage = await processLanguageFile(
-    path.join(__dirname, DEFAULT_STRINGS_FILE));
+  let internalDefaultLanguage;
+
+  try {
+    internalDefaultLanguage =
+      await processLanguageFile(path.join(__dirname, DEFAULT_STRINGS_FILE));
+
+    stepStatusSummary.loadInternalDefaultLanguage.status = STATUS_DONE_CLEAN;
+  } catch (error) {
+    console.error(error);
+
+    logError`There was an error reading the internal language file.`;
+    fileIssue();
+
+    Object.assign(stepStatusSummary.loadInternalDefaultLanguage, {
+      status: STATUS_FATAL_ERROR,
+      annotation: `see log for details`,
+    });
+
+    return false;
+  }
 
   let languages;
+
   if (langPath) {
+    stepStatusSummary.loadLanguageFiles.status = STATUS_STARTED_NOT_DONE;
+
     const languageDataFiles = await traverse(langPath, {
       filterFile: name => path.extname(name) === '.json',
       pathStyle: 'device',
     });
 
-    const results = await progressPromiseAll(`Reading & processing language files.`,
-      languageDataFiles.map((file) => processLanguageFile(file)));
+    let results;
 
-    languages = Object.fromEntries(
-      results.map((language) => [language.code, language]));
+    // TODO: Aggregate errors (with Promise.allSettled).
+    try {
+      results =
+        await progressPromiseAll(`Reading & processing language files.`,
+          languageDataFiles.map((file) => processLanguageFile(file)));
+    } catch (error) {
+      console.error(error);
+
+      logError`Failed to load language files. Please investigate these, or don't provide`;
+      logError`--lang-path (or HSMUSIC_LANG) and build again.`;
+
+      Object.assign(stepStatusSummary.loadLanguageFiles, {
+        status: STATUS_FATAL_ERROR,
+        annotation: `see log for details`,
+      });
+
+      return false;
+    }
+
+    languages =
+      Object.fromEntries(
+        results.map((language) => [language.code, language]));
+
+    stepStatusSummary.loadLanguageFiles.status = STATUS_DONE_CLEAN;
   } else {
     languages = {};
+
+    Object.assign(stepStatusSummary.loadLanguageFiles, {
+      status: STATUS_NOT_APPLICABLE,
+      annotation: `--lang-path and HSMUSIC_LANG not provided`,
+    });
   }
 
+  stepStatusSummary.initializeDefaultLanguage.status = STATUS_STARTED_NOT_DONE;
+
   const customDefaultLanguage =
     languages[wikiData.wikiInfo.defaultLanguage ?? internalDefaultLanguage.code];
   let finalDefaultLanguage;
@@ -616,17 +865,34 @@ async function main() {
     logInfo`Applying new default strings from custom ${customDefaultLanguage.code} language file.`;
     customDefaultLanguage.inheritedStrings = internalDefaultLanguage.strings;
     finalDefaultLanguage = customDefaultLanguage;
+
+    Object.assign(stepStatusSummary.initializeDefaultLanguage, {
+      status: STATUS_DONE_CLEAN,
+      annotation: `using wiki-specified custom default language`,
+    });
   } else if (wikiData.wikiInfo.defaultLanguage) {
     logError`Wiki info file specified default language is ${wikiData.wikiInfo.defaultLanguage}, but no such language file exists!`;
     if (langPath) {
       logError`Check if an appropriate file exists in ${langPath}?`;
     } else {
-      logError`Be sure to specify ${'--lang'} or ${'HSMUSIC_LANG'} with the path to language files.`;
+      logError`Be sure to specify ${'--lang-path'} or ${'HSMUSIC_LANG'} with the path to language files.`;
     }
-    return;
+
+    Object.assign(stepStatusSummary.initializeDefaultLanguage, {
+      status: STATUS_FATAL_ERROR,
+      annotation: `wiki specifies default language whose file is not available`,
+    });
+
+    return false;
   } else {
     languages[internalDefaultLanguage.code] = internalDefaultLanguage;
     finalDefaultLanguage = internalDefaultLanguage;
+    stepStatusSummary.initializeDefaultLanguage.status = STATUS_DONE_CLEAN;
+
+    Object.assign(stepStatusSummary.initializeDefaultLanguage, {
+      status: STATUS_DONE_CLEAN,
+      annotation: `no custom default language specified`,
+    });
   }
 
   for (const language of Object.values(languages)) {
@@ -641,7 +907,8 @@ async function main() {
 
   const urls = generateURLs(urlSpec);
 
-  await verifyImagePaths(mediaPath, {urls, wikiData});
+  const {missing: missingImagePaths} =
+    await verifyImagePaths(mediaPath, {urls, wikiData});
 
   const fileSizePreloader = new FileSizePreloader();
 
@@ -704,7 +971,9 @@ async function main() {
   };
 
   const getSizeOfAdditionalFile = getSizeOfMediaFileHelper(additionalFilePaths);
-  const getSizeOfImageFile = getSizeOfMediaFileHelper(imageFilePaths);
+  const getSizeOfImagePath = getSizeOfMediaFileHelper(imageFilePaths);
+
+  stepStatusSummary.preloadFileSizes.status = STATUS_STARTED_NOT_DONE;
 
   logInfo`Preloading filesizes for ${additionalFilePaths.length} additional files...`;
 
@@ -716,9 +985,23 @@ async function main() {
   fileSizePreloader.loadPaths(...imageFilePaths.map((path) => path.device));
   await fileSizePreloader.waitUntilDoneLoading();
 
-  logInfo`Done preloading filesizes!`;
+  if (fileSizePreloader.hasErrored) {
+    logWarn`Some media files couldn't be read for preloading filesizes.`;
+    logWarn`This means the wiki won't display file sizes for these files.`;
+    logWarn`Investigate missing or unreadable files to get that fixed!`;
 
-  if (noBuild) return;
+    Object.assign(stepStatusSummary.preloadFileSizes, {
+      status: STATUS_HAS_WARNINGS,
+      annotation: `see log for details`,
+    });
+  } else {
+    logInfo`Done preloading filesizes without any errors - nice!`;
+    stepStatusSummary.preloadFileSizes.status = STATUS_DONE_CLEAN;
+  }
+
+  if (noBuild) {
+    return true;
+  }
 
   const developersComment =
     `<!--\n` + [
@@ -750,25 +1033,58 @@ async function main() {
       .map(line => `    ` + line)
       .join('\n') + `\n-->`;
 
-  return selectedBuildMode.go({
-    cliOptions,
-    dataPath,
-    mediaPath,
-    queueSize,
-    srcRootPath: __dirname,
-
-    defaultLanguage: finalDefaultLanguage,
-    languages,
-    wikiData,
-    urls,
-    urlSpec,
-
-    cachebust: '?' + CACHEBUST,
-    developersComment,
-    getSizeOfAdditionalFile,
-    getSizeOfImageFile,
-    niceShowAggregate,
-  });
+  stepStatusSummary.performBuild.status = STATUS_STARTED_NOT_DONE;
+
+  let buildModeResult;
+
+  try {
+    buildModeResult = await selectedBuildMode.go({
+      cliOptions,
+      dataPath,
+      mediaPath,
+      queueSize,
+      srcRootPath: __dirname,
+
+      defaultLanguage: finalDefaultLanguage,
+      languages,
+      missingImagePaths,
+      thumbsCache,
+      urls,
+      urlSpec,
+      wikiData,
+
+      cachebust: '?' + CACHEBUST,
+      developersComment,
+      getSizeOfAdditionalFile,
+      getSizeOfImagePath,
+      niceShowAggregate,
+    });
+  } catch (error) {
+    console.error(error);
+
+    logError`There was a JavaScript error performing the build.`;
+    fileIssue();
+
+    Object.assign(stepStatusSummary.performBuild, {
+      status: STATUS_FATAL_ERROR,
+      message: `javascript error - view log for details`,
+    });
+
+    return false;
+  }
+
+  if (buildModeResult !== true) {
+    Object.assign(stepStatusSummary.performBuild, {
+      status: STATUS_HAS_WARNINGS,
+      message: `may not have completed - view log for details`,
+    });
+
+    return false;
+  }
+
+  stepStatusSummary.performBuild.status = STATUS_DONE_CLEAN;
+
+  return true;
 }
 
 // TODO: isMain detection isn't consistent across platforms here
@@ -787,6 +1103,65 @@ if (true || isMain(import.meta.url) || path.basename(process.argv[1]) === 'hsmus
       }
     }
 
+    if (showStepStatusSummary) {
+      console.error(colors.bright(`Step summary:`));
+
+      const longestNameLength =
+        Math.max(...
+          Object.values(stepStatusSummary)
+            .map(({name}) => name.length));
+
+      const anyStepsNotClean =
+        Object.values(stepStatusSummary)
+          .some(({status}) =>
+            status === STATUS_HAS_WARNINGS ||
+            status === STATUS_FATAL_ERROR ||
+            status === STATUS_STARTED_NOT_DONE);
+
+      for (const {name, status, annotation} of Object.values(stepStatusSummary)) {
+        let message = `${(name + ': ').padEnd(longestNameLength + 4, '.')} ${status}`;
+        if (annotation) {
+          message += ` (${annotation})`;
+        }
+
+        switch (status) {
+          case STATUS_DONE_CLEAN:
+            console.error(colors.green(message));
+            break;
+
+          case STATUS_NOT_STARTED:
+          case STATUS_NOT_APPLICABLE:
+            console.error(colors.dim(message));
+            break;
+
+          case STATUS_HAS_WARNINGS:
+          case STATUS_STARTED_NOT_DONE:
+            console.error(colors.yellow(message));
+            break;
+
+          case STATUS_FATAL_ERROR:
+            console.error(colors.red(message));
+            break;
+
+          default:
+            console.error(message);
+            break;
+        }
+      }
+
+      if (result === true) {
+        if (anyStepsNotClean) {
+          console.error(colors.bright(`Final output is true, but some steps aren't clean.`));
+          process.exit(1);
+          return;
+        } else {
+          console.error(colors.bright(`Final output is true and all steps are clean.`));
+        }
+      } else {
+        console.error(colors.bright(`Final output is not true (${result}).`));
+      }
+    }
+
     if (result !== true) {
       process.exit(1);
       return;
diff --git a/src/url-spec.js b/src/url-spec.js
index 4d10313..2ff0fa5 100644
--- a/src/url-spec.js
+++ b/src/url-spec.js
@@ -37,6 +37,8 @@ const urlSpec = {
       flashIndex: 'flash/',
       flash: 'flash/<>/',
 
+      flashActGallery: 'flash-act/<>/',
+
       groupInfo: 'group/<>/',
       groupGallery: 'group/<>/gallery/',
 
diff --git a/src/util/cli.js b/src/util/cli.js
index f83c806..4c08c08 100644
--- a/src/util/cli.js
+++ b/src/util/cli.js
@@ -17,7 +17,7 @@ export const ENABLE_COLOR =
 const C = (n) =>
   ENABLE_COLOR ? (text) => `\x1b[${n}m${text}\x1b[0m` : (text) => text;
 
-export const color = {
+export const colors = {
   bright: C('1'),
   dim: C('2'),
   normal: C('22'),
@@ -334,7 +334,9 @@ export function progressCallAll(msgOrMsgFn, array) {
 export function fileIssue({
   topMessage = `This shouldn't happen.`,
 } = {}) {
-  console.error(color.red(`${topMessage} Please let the HSMusic developers know:`));
-  console.error(color.red(`- https://hsmusic.wiki/feedback/`));
-  console.error(color.red(`- https://github.com/hsmusic/hsmusic-wiki/issues/`));
+  if (topMessage) {
+    console.error(colors.red(`${topMessage} Please let the HSMusic developers know:`));
+  }
+  console.error(colors.red(`- https://hsmusic.wiki/feedback/`));
+  console.error(colors.red(`- https://github.com/hsmusic/hsmusic-wiki/issues/`));
 }
diff --git a/src/util/html.js b/src/util/html.js
index a311bbb..282a52d 100644
--- a/src/util/html.js
+++ b/src/util/html.js
@@ -2,7 +2,7 @@
 
 import {inspect} from 'node:util';
 
-import {empty} from '#sugar';
+import {empty, typeAppearance} from '#sugar';
 import * as commonValidators from '#validators';
 
 // COMPREHENSIVE!
@@ -242,7 +242,7 @@ export class Tag {
       this.selfClosing &&
       !(value === null ||
         value === undefined ||
-        !Boolean(value) ||
+        !value ||
         Array.isArray(value) && value.filter(Boolean).length === 0)
     ) {
       throw new Error(`Tag <${this.tagName}> is self-closing but got content`);
@@ -633,7 +633,7 @@ export class Template {
 
   static validateDescription(description) {
     if (typeof description !== 'object') {
-      throw new TypeError(`Expected object, got ${typeof description}`);
+      throw new TypeError(`Expected object, got ${typeAppearance(description)}`);
     }
 
     if (description === null) {
@@ -806,24 +806,43 @@ export class Template {
     }
 
     // Null is always an acceptable slot value.
-    if (value !== null) {
-      if ('validate' in description) {
-        description.validate({
-          ...commonValidators,
-          ...validators,
-        })(value);
-      }
+    if (value === null) {
+      return true;
+    }
+
+    if ('validate' in description) {
+      description.validate({
+        ...commonValidators,
+        ...validators,
+      })(value);
+    }
 
-      if ('type' in description) {
-        const {type} = description;
-        if (type === 'html') {
-          if (!isHTML(value)) {
+    if ('type' in description) {
+      switch (description.type) {
+        case 'html': {
+          if (!isHTML(value))
             throw new TypeError(`Slot expects html (tag, template or blank), got ${typeof value}`);
-          }
-        } else {
-          if (typeof value !== type) {
-            throw new TypeError(`Slot expects ${type}, got ${typeof value}`);
-          }
+
+          return true;
+        }
+
+        case 'string': {
+          // Tags and templates are valid in string arguments - they'll be
+          // stringified when exposed to the description's .content() function.
+          if (isTag(value) || isTemplate(value))
+            return true;
+
+          if (typeof value !== 'string')
+            throw new TypeError(`Slot expects string, got ${typeof value}`);
+
+          return true;
+        }
+
+        default: {
+          if (typeof value !== description.type)
+            throw new TypeError(`Slot expects ${description.type}, got ${typeof value}`);
+
+          return true;
         }
       }
     }
@@ -847,6 +866,12 @@ export class Template {
       return providedValue;
     }
 
+    if (description.type === 'string') {
+      if (isTag(providedValue) || isTemplate(providedValue)) {
+        return providedValue.toString();
+      }
+    }
+
     if (providedValue !== null) {
       return providedValue;
     }
diff --git a/src/util/replacer.js b/src/util/replacer.js
index c5289cc..095ee06 100644
--- a/src/util/replacer.js
+++ b/src/util/replacer.js
@@ -5,9 +5,8 @@
 // function, which converts nodes parsed here into actual HTML, links, etc
 // for embedding in a wiki webpage.
 
-import {logError, logWarn} from '#cli';
 import * as html from '#html';
-import {escapeRegex} from '#sugar';
+import {escapeRegex, typeAppearance} from '#sugar';
 
 // Syntax literals.
 const tagBeginning = '[[';
@@ -408,7 +407,7 @@ export function postprocessHeadings(inputNodes) {
 
 export function parseInput(input) {
   if (typeof input !== 'string') {
-    throw new TypeError(`Expected input to be string, got ${input}`);
+    throw new TypeError(`Expected input to be string, got ${typeAppearance(input)}`);
   }
 
   try {
diff --git a/src/util/sugar.js b/src/util/sugar.js
index 487c093..3e39e98 100644
--- a/src/util/sugar.js
+++ b/src/util/sugar.js
@@ -6,7 +6,7 @@
 // It will likely only do exactly what I want it to, and only in the cases I
 // decided were relevant enough to 8other handling.
 
-import {color} from './cli.js';
+import {colors} from './cli.js';
 
 // Apparently JavaScript doesn't come with a function to split an array into
 // chunks! Weird. Anyway, this is an awesome place to use a generator, even
@@ -82,7 +82,7 @@ export function stitchArrays(keyToArray) {
   for (const [key, value] of Object.entries(keyToArray)) {
     if (value === null) continue;
     if (Array.isArray(value)) continue;
-    errors.push(new TypeError(`(${key}) Expected array or null, got ${value}`));
+    errors.push(new TypeError(`(${key}) Expected array or null, got ${typeAppearance(value)}`));
   }
 
   if (!empty(errors)) {
@@ -168,12 +168,34 @@ export function setIntersection(set1, set2) {
   return intersection;
 }
 
-export function filterProperties(obj, properties) {
-  const set = new Set(properties);
-  return Object.fromEntries(
-    Object
-      .entries(obj)
-      .filter(([key]) => set.has(key)));
+export function filterProperties(object, properties, {
+  preserveOriginalOrder = false,
+} = {}) {
+  if (typeof object !== 'object' || object === null) {
+    throw new TypeError(`Expected object to be an object, got ${typeAppearance(object)}`);
+  }
+
+  if (!Array.isArray(properties)) {
+    throw new TypeError(`Expected properties to be an array, got ${typeAppearance(properties)}`);
+  }
+
+  const filteredObject = {};
+
+  if (preserveOriginalOrder) {
+    for (const property of Object.keys(object)) {
+      if (properties.includes(property)) {
+        filteredObject[property] = object[property];
+      }
+    }
+  } else {
+    for (const property of properties) {
+      if (Object.hasOwn(object, property)) {
+        filteredObject[property] = object[property];
+      }
+    }
+  }
+
+  return filteredObject;
 }
 
 export function queue(array, max = 50) {
@@ -218,6 +240,16 @@ export function escapeRegex(string) {
   return string.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
 }
 
+// Gets the "look" of some arbitrary value. It's like typeof, but smarter.
+// Don't use this for actually validating types - it's only suitable for
+// inclusion in error messages.
+export function typeAppearance(value) {
+  if (value === null) return 'null';
+  if (value === undefined) return 'undefined';
+  if (Array.isArray(value)) return 'array';
+  return typeof value;
+}
+
 // Binds default values for arguments in a {key: value} type function argument
 // (typically the second argument, but may be overridden by providing a
 // [bindOpts.bindIndex] argument). Typically useful for preparing a function for
@@ -532,15 +564,17 @@ export function showAggregate(topError, {
   print = true,
 } = {}) {
   const recursive = (error, {level}) => {
-    let header = showTraces
+    let headerPart = showTraces
       ? `[${error.constructor.name || 'unnamed'}] ${
           error.message || '(no message)'
         }`
       : error instanceof AggregateError
       ? `[${error.message || '(no message)'}]`
       : error.message || '(no message)';
+
     if (showTraces) {
       const stackLines = error.stack?.split('\n');
+
       const stackLine = stackLines?.find(
         (line) =>
           line.trim().startsWith('at') &&
@@ -548,30 +582,41 @@ export function showAggregate(topError, {
           !line.includes('node:') &&
           !line.includes('<anonymous>')
       );
+
       const tracePart = stackLine
         ? '- ' +
           stackLine
             .trim()
             .replace(/file:\/\/.*\.js/, (match) => pathToFileURL(match))
         : '(no stack trace)';
-      header += ` ${color.dim(tracePart)}`;
-    }
-    const bar = level % 2 === 0 ? '\u2502' : color.dim('\u254e');
-    const head = level % 2 === 0 ? '\u257f' : color.dim('\u257f');
-
-    if (error instanceof AggregateError) {
-      return (
-        header +
-        '\n' +
-        error.errors
-          .map((error) => recursive(error, {level: level + 1}))
-          .flatMap((str) => str.split('\n'))
-          .map((line, i) => i === 0 ? ` ${head} ${line}` : ` ${bar} ${line}`)
-          .join('\n')
-      );
-    } else {
-      return header;
+
+      headerPart += ` ${colors.dim(tracePart)}`;
     }
+
+    const head1 = level % 2 === 0 ? '\u21aa' : colors.dim('\u21aa');
+    const bar1 = ' ';
+
+    const causePart =
+      (error.cause
+        ? recursive(error.cause, {level: level + 1})
+            .split('\n')
+            .map((line, i) => i === 0 ? ` ${head1} ${line}` : ` ${bar1} ${line}`)
+            .join('\n')
+        : '');
+
+    const head2 = level % 2 === 0 ? '\u257f' : colors.dim('\u257f');
+    const bar2 = level % 2 === 0 ? '\u2502' : colors.dim('\u254e');
+
+    const aggregatePart =
+      (error instanceof AggregateError
+        ? error.errors
+            .map(error => recursive(error, {level: level + 1}))
+            .flatMap(str => str.split('\n'))
+            .map((line, i) => i === 0 ? ` ${head2} ${line}` : ` ${bar2} ${line}`)
+            .join('\n')
+        : '');
+
+    return [headerPart, causePart, aggregatePart].filter(Boolean).join('\n');
   };
 
   const message = recursive(topError, {level: 0});
@@ -588,7 +633,8 @@ export function decorateErrorWithIndex(fn) {
     try {
       return fn(x, index, array);
     } catch (error) {
-      error.message = `(${color.yellow(`#${index + 1}`)}) ${error.message}`;
+      error.message = `(${colors.yellow(`#${index + 1}`)}) ${error.message}`;
+      error[Symbol.for('hsmusic.decorate.indexInSourceArray')] = index;
       throw error;
     }
   };
diff --git a/src/util/urls.js b/src/util/urls.js
index d2b303e..11b9b8b 100644
--- a/src/util/urls.js
+++ b/src/util/urls.js
@@ -237,27 +237,6 @@ export function getPagePathname({
     : to('localized.' + pagePath[0], ...pagePath.slice(1)));
 }
 
-export function getPagePathnameAcrossLanguages({
-  defaultLanguage,
-  languages,
-  pagePath,
-  urls,
-}) {
-  return withEntries(languages, entries => entries
-    .filter(([key, language]) => key !== 'default' && !language.hidden)
-    .map(([_key, language]) => [
-      language.code,
-      getPagePathname({
-        baseDirectory:
-          (language === defaultLanguage
-            ? ''
-            : language.code),
-        pagePath,
-        urls,
-      }),
-    ]));
-}
-
 // Needed for the rare path arguments which themselves contains one or more
 // slashes, e.g. for listings, with arguments like 'albums/by-name'.
 export function getPageSubdirectoryPrefix({
diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js
index ad2f82f..0790ae9 100644
--- a/src/util/wiki-data.js
+++ b/src/util/wiki-data.js
@@ -1,6 +1,6 @@
 // Utility functions for interacting with wiki data.
 
-import {accumulateSum, empty, stitchArrays, unique} from './sugar.js';
+import {accumulateSum, empty, unique} from './sugar.js';
 
 // Generic value operations
 
@@ -610,20 +610,9 @@ export function sortFlashesChronologically(data, {
   latestFirst = false,
   getDate,
 } = {}) {
-  // Flash acts don't actually have any identifying properties because they
-  // don't have dedicated pages (yet), so don't have a directory. Make up a
-  // fake key identifying them so flashes can be grouped together.
-  const flashActs = new Set(data.map(flash => flash.act));
-  const flashActIdentifiers = new Map();
-
-  let counter = 0;
-  for (const act of flashActs) {
-    flashActIdentifiers.set(act, ++counter);
-  }
-
   // Group flashes by act...
-  data.sort((a, b) => {
-    return flashActIdentifiers.get(a.act) - flashActIdentifiers.get(b.act);
+  sortByDirectory(data, {
+    getDirectory: flash => flash.act.directory,
   });
 
   // Sort flashes by position in act...
@@ -874,3 +863,71 @@ export function filterItemsForCarousel(items) {
     .filter(item => item.artTags.every(tag => !tag.isContentWarning))
     .slice(0, maxCarouselLayoutItems + 1);
 }
+
+// Ridiculous caching support nonsense
+
+export class TupleMap {
+  static maxNestedTupleLength = 25;
+
+  #store = [undefined, null, null, null];
+
+  #lifetime(value) {
+    if (Array.isArray(value) && value.length <= TupleMap.maxNestedTupleLength) {
+      return 'tuple';
+    } else if (
+      typeof value === 'object' && value !== null ||
+      typeof value === 'function'
+    ) {
+      return 'weak';
+    } else {
+      return 'strong';
+    }
+  }
+
+  #getSubstoreShallow(value, store) {
+    const lifetime = this.#lifetime(value);
+    const mapIndex = {weak: 1, strong: 2, tuple: 3}[lifetime];
+
+    let map = store[mapIndex];
+    if (map === null) {
+      map = store[mapIndex] =
+        (lifetime === 'weak' ? new WeakMap()
+       : lifetime === 'strong' ? new Map()
+       : lifetime === 'tuple' ? new TupleMap()
+       : null);
+    }
+
+    if (map.has(value)) {
+      return map.get(value);
+    } else {
+      const substore = [undefined, null, null, null];
+      map.set(value, substore);
+      return substore;
+    }
+  }
+
+  #getSubstoreDeep(tuple, store = this.#store) {
+    if (tuple.length === 0) {
+      return store;
+    } else {
+      const [first, ...rest] = tuple;
+      return this.#getSubstoreDeep(rest, this.#getSubstoreShallow(first, store));
+    }
+  }
+
+  get(tuple) {
+    const store = this.#getSubstoreDeep(tuple);
+    return store[0];
+  }
+
+  has(tuple) {
+    const store = this.#getSubstoreDeep(tuple);
+    return store[0] !== undefined;
+  }
+
+  set(tuple, value) {
+    const store = this.#getSubstoreDeep(tuple);
+    store[0] = value;
+    return value;
+  }
+}
diff --git a/src/write/bind-utilities.js b/src/write/bind-utilities.js
index 8e2adea..3d4ecc7 100644
--- a/src/write/bind-utilities.js
+++ b/src/write/bind-utilities.js
@@ -10,15 +10,24 @@ import * as html from '#html';
 import {bindOpts} from '#sugar';
 import {thumb} from '#urls';
 
+import {
+  checkIfImagePathHasCachedThumbnails,
+  getDimensionsOfImagePath,
+  getThumbnailEqualOrSmaller,
+  getThumbnailsAvailableForDimensions,
+} from '#thumbs';
+
 export function bindUtilities({
   absoluteTo,
   cachebust,
   defaultLanguage,
   getSizeOfAdditionalFile,
-  getSizeOfImageFile,
+  getSizeOfImagePath,
   language,
   languages,
+  missingImagePaths,
   pagePath,
+  thumbsCache,
   to,
   urls,
   wikiData,
@@ -30,10 +39,12 @@ export function bindUtilities({
     cachebust,
     defaultLanguage,
     getSizeOfAdditionalFile,
-    getSizeOfImageFile,
+    getSizeOfImagePath,
+    getThumbnailsAvailableForDimensions,
     html,
     language,
     languages,
+    missingImagePaths,
     pagePath,
     thumb,
     to,
@@ -46,5 +57,17 @@ export function bindUtilities({
 
   bound.find = bindFind(wikiData, {mode: 'warn'});
 
+  bound.checkIfImagePathHasCachedThumbnails =
+    (imagePath) =>
+      checkIfImagePathHasCachedThumbnails(imagePath, thumbsCache);
+
+  bound.getDimensionsOfImagePath =
+    (imagePath) =>
+      getDimensionsOfImagePath(imagePath, thumbsCache);
+
+  bound.getThumbnailEqualOrSmaller =
+    (preferred, imagePath) =>
+      getThumbnailEqualOrSmaller(preferred, imagePath, thumbsCache);
+
   return bound;
 }
diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js
index 2767a02..1339c32 100644
--- a/src/write/build-modes/live-dev-server.js
+++ b/src/write/build-modes/live-dev-server.js
@@ -1,8 +1,6 @@
 import * as http from 'node:http';
-import {createReadStream} from 'node:fs';
-import {stat} from 'node:fs/promises';
+import {readFile, stat} from 'node:fs/promises';
 import * as path from 'node:path';
-import {pipeline} from 'node:stream/promises'
 
 import {logInfo, logWarn, progressCallAll} from '#cli';
 import {watchContentDependencies} from '#content-dependencies';
@@ -10,11 +8,9 @@ import {quickEvaluate} from '#content-function';
 import * as html from '#html';
 import * as pageSpecs from '#page-specs';
 import {serializeThings} from '#serialize';
-import {empty} from '#sugar';
 
 import {
   getPagePathname,
-  getPagePathnameAcrossLanguages,
   getURLsFrom,
   getURLsFromRoot,
 } from '#urls';
@@ -44,8 +40,8 @@ export function getCLIOptions() {
       },
     },
 
-    'quiet-responses': {
-      help: `Disables outputting [200] and [404] responses in the server log`,
+    'loud-responses': {
+      help: `Enables outputting [200] and [404] responses in the server log, which are suppressed by default`,
       type: 'flag',
     },
   };
@@ -58,14 +54,16 @@ export async function go({
 
   defaultLanguage,
   languages,
+  missingImagePaths,
   srcRootPath,
+  thumbsCache,
   urls,
   wikiData,
 
   cachebust,
   developersComment,
   getSizeOfAdditionalFile,
-  getSizeOfImageFile,
+  getSizeOfImagePath,
   niceShowAggregate,
 }) {
   const showError = (error) => {
@@ -78,7 +76,7 @@ export async function go({
 
   const host = cliOptions['host'] ?? defaultHost;
   const port = parseInt(cliOptions['port'] ?? defaultPort);
-  const quietResponses = cliOptions['quiet-responses'] ?? false;
+  const loudResponses = cliOptions['loud-responses'] ?? false;
 
   const contentDependenciesWatcher = await watchContentDependencies();
   const {contentDependencies} = contentDependenciesWatcher;
@@ -160,10 +158,10 @@ export async function go({
         });
         response.writeHead(200, contentTypeJSON);
         response.end(json);
-        if (!quietResponses) console.log(`${requestHead} [200] /data.json`);
+        if (loudResponses) console.log(`${requestHead} [200] /data.json`);
       } catch (error) {
         response.writeHead(500, contentTypeJSON);
-        response.end({error: `Internal error serializing wiki JSON`});
+        response.end(`Internal error serializing wiki JSON`);
         console.error(`${requestHead} [500] /data.json`);
         showError(error);
       }
@@ -224,7 +222,7 @@ export async function go({
         'gif': 'image/gif',
         'ico': 'image/vnd.microsoft.icon',
         'jpg': 'image/jpeg',
-        'jpeg:': 'image/jpeg',
+        'jpeg': 'image/jpeg',
         'js': 'text/javascript',
         'mjs': 'text/javascript',
         'mp3': 'audio/mpeg',
@@ -249,14 +247,13 @@ export async function go({
 
       try {
         const {size} = await stat(filePath);
+        const buffer = await readFile(filePath)
         response.writeHead(200, contentType ? {
           'Content-Type': contentType,
           'Content-Length': size,
         } : {});
-        await pipeline(
-          createReadStream(filePath),
-          response);
-        if (!quietResponses) console.log(`${requestHead} [200] ${pathname}`);
+        response.end(buffer);
+        if (loudResponses) console.log(`${requestHead} [200] ${pathname}`);
       } catch (error) {
         response.writeHead(500, contentTypePlain);
         response.end(`Failed during file-to-response pipeline`);
@@ -274,7 +271,7 @@ export async function go({
     if (!Object.hasOwn(urlToPageMap, pathnameKey)) {
       response.writeHead(404, contentTypePlain);
       response.end(`No page found for: ${pathnameKey}\n`);
-      if (!quietResponses) console.log(`${requestHead} [404] ${pathname}`);
+      if (loudResponses) console.log(`${requestHead} [404] ${pathname}`);
       return;
     }
 
@@ -331,22 +328,17 @@ export async function go({
         return;
       }
 
-      const localizedPathnames = getPagePathnameAcrossLanguages({
-        defaultLanguage,
-        languages,
-        pagePath: servePath,
-        urls,
-      });
-
       const bound = bindUtilities({
         absoluteTo,
         cachebust,
         defaultLanguage,
         getSizeOfAdditionalFile,
-        getSizeOfImageFile,
+        getSizeOfImagePath,
         language,
         languages,
+        missingImagePaths,
         pagePath: servePath,
+        thumbsCache,
         to,
         urls,
         wikiData,
@@ -363,14 +355,14 @@ export async function go({
 
       const {pageHTML} = html.resolve(topLevelResult);
 
-      if (!quietResponses) console.log(`${requestHead} [200] ${pathname}`);
+      if (loudResponses) console.log(`${requestHead} [200] ${pathname}`);
       response.writeHead(200, contentTypeHTML);
       response.end(pageHTML);
     } catch (error) {
-      response.writeHead(500, contentTypePlain);
-      response.end(`Error generating page, view server log for details\n`);
       console.error(`${requestHead} [500] ${pathname}`);
       showError(error);
+      response.writeHead(500, contentTypePlain);
+      response.end(`Error generating page, view server log for details\n`);
     }
   });
 
@@ -393,8 +385,11 @@ export async function go({
   server.on('listening', () => {
     logInfo`${'All done!'} Listening at: ${address}`;
     logInfo`Press ^C here (control+C) to stop the server and exit.`;
-    if (quietResponses) {
-      logInfo`Suppressing [200] and [404] response logging.`;
+    if (loudResponses) {
+      logInfo`Printing [200] and [404] responses.`
+    } else {
+      logInfo`Suppressing [200] and [404] response logging.`
+      logInfo`(Pass --loud-responses to show these.)`;
     }
   });
 
diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js
index 2210dfe..0931699 100644
--- a/src/write/build-modes/static-build.js
+++ b/src/write/build-modes/static-build.js
@@ -17,16 +17,15 @@ import {serializeThings} from '#serialize';
 import {empty, queue, withEntries} from '#sugar';
 
 import {
+  fileIssue,
   logError,
   logInfo,
   logWarn,
-  progressCallAll,
   progressPromiseAll,
 } from '#cli';
 
 import {
   getPagePathname,
-  getPagePathnameAcrossLanguages,
   getURLsFrom,
   getURLsFromRoot,
 } from '#urls';
@@ -89,15 +88,17 @@ export async function go({
 
   defaultLanguage,
   languages,
+  missingImagePaths,
   srcRootPath,
+  thumbsCache,
   urls,
-  urlSpec,
   wikiData,
 
   cachebust,
   developersComment,
   getSizeOfAdditionalFile,
-  getSizeOfImageFile,
+  getSizeOfImagePath,
+  niceShowAggregate,
 }) {
   const outputPath = cliOptions['out-path'] || process.env.HSMUSIC_OUT;
   const appendIndexHTML = cliOptions['append-index-html'] ?? false;
@@ -253,6 +254,8 @@ export async function go({
   ));
   */
 
+  let errored = false;
+
   const contentDependencies = await quickLoadContentDependencies();
 
   const perLanguageFn = async (language, i, entries) => {
@@ -265,13 +268,6 @@ export async function go({
       ...pageWrites.map(page => () => {
         const pagePath = page.path;
 
-        const localizedPathnames = getPagePathnameAcrossLanguages({
-          defaultLanguage,
-          languages,
-          pagePath,
-          urls,
-        });
-
         const pathname = getPagePathname({
           baseDirectory,
           pagePath,
@@ -294,23 +290,33 @@ export async function go({
           cachebust,
           defaultLanguage,
           getSizeOfAdditionalFile,
-          getSizeOfImageFile,
+          getSizeOfImagePath,
           language,
           languages,
+          missingImagePaths,
           pagePath,
+          thumbsCache,
           to,
           urls,
           wikiData,
         });
 
-        const topLevelResult =
-          quickEvaluate({
-            contentDependencies,
-            extraDependencies: {...bound, appendIndexHTML},
-
-            name: page.contentFunction.name,
-            args: page.contentFunction.args ?? [],
-          });
+        let topLevelResult;
+        try {
+          topLevelResult =
+            quickEvaluate({
+              contentDependencies,
+              extraDependencies: {...bound, appendIndexHTML},
+
+              name: page.contentFunction.name,
+              args: page.contentFunction.args ?? [],
+            });
+        } catch (error) {
+          logError`\rError generating page: ${pathname}`;
+          niceShowAggregate(error);
+          errored = true;
+          return;
+        }
 
         const {pageHTML, oEmbedJSON} = html.resolve(topLevelResult);
 
@@ -358,6 +364,16 @@ export async function go({
 
   // The single most important step.
   logInfo`Written!`;
+
+  if (errored) {
+    logWarn`The code generating content for some pages ended up erroring.`;
+    logWarn`These pages were skipped, so if you ran a build previously and`;
+    logWarn`they didn't error that time, then the old version is still`;
+    logWarn`available - albeit possibly outdated! Please scroll up and send`;
+    logWarn`the HSMusic developers a copy of the errors:`;
+    fileIssue({topMessage: null});
+  }
+
   return true;
 }
 
@@ -454,14 +470,9 @@ async function writeFavicon({
 }
 
 async function writeSharedFilesAndPages({
-  language,
   outputPath,
-  urls,
-  wikiData,
   wikiDataJSON,
 }) {
-  const {groupData, wikiInfo} = wikiData;
-
   return progressPromiseAll(`Writing files & pages shared across languages.`, [
     wikiDataJSON &&
       writeFile(
diff --git a/tap-snapshots/test/snapshot/generateAdditionalFilesList.js.test.cjs b/tap-snapshots/test/snapshot/generateAdditionalFilesList.js.test.cjs
index 5ca6348..42a409a 100644
--- a/tap-snapshots/test/snapshot/generateAdditionalFilesList.js.test.cjs
+++ b/tap-snapshots/test/snapshot/generateAdditionalFilesList.js.test.cjs
@@ -5,7 +5,7 @@
  * Make sure to inspect the output below.  Do not ignore changes!
  */
 'use strict'
-exports[`test/snapshot/generateAdditionalFilesList.js TAP generateAdditionalFilesList (snapshot) > basic behavior 1`] = `
+exports[`test/snapshot/generateAdditionalFilesList.js > TAP > generateAdditionalFilesList (snapshot) > basic behavior 1`] = `
 <dl>
     <dt>SBURB Wallpaper</dt>
     <dd>
@@ -24,6 +24,6 @@ exports[`test/snapshot/generateAdditionalFilesList.js TAP generateAdditionalFile
 </dl>
 `
 
-exports[`test/snapshot/generateAdditionalFilesList.js TAP generateAdditionalFilesList (snapshot) > no additional files 1`] = `
+exports[`test/snapshot/generateAdditionalFilesList.js > TAP > generateAdditionalFilesList (snapshot) > no additional files 1`] = `
 
 `
diff --git a/tap-snapshots/test/snapshot/generateAdditionalFilesShortcut.js.test.cjs b/tap-snapshots/test/snapshot/generateAdditionalFilesShortcut.js.test.cjs
index c94371c..e166140 100644
--- a/tap-snapshots/test/snapshot/generateAdditionalFilesShortcut.js.test.cjs
+++ b/tap-snapshots/test/snapshot/generateAdditionalFilesShortcut.js.test.cjs
@@ -5,10 +5,10 @@
  * Make sure to inspect the output below.  Do not ignore changes!
  */
 'use strict'
-exports[`test/snapshot/generateAdditionalFilesShortcut.js TAP generateAdditionalFilesShortcut (snapshot) > basic behavior 1`] = `
+exports[`test/snapshot/generateAdditionalFilesShortcut.js > TAP > generateAdditionalFilesShortcut (snapshot) > basic behavior 1`] = `
 View <a href="#additional-files">additional files</a>: SBURB Wallpaper, Alternate Covers
 `
 
-exports[`test/snapshot/generateAdditionalFilesShortcut.js TAP generateAdditionalFilesShortcut (snapshot) > no additional files 1`] = `
+exports[`test/snapshot/generateAdditionalFilesShortcut.js > TAP > generateAdditionalFilesShortcut (snapshot) > no additional files 1`] = `
 
 `
diff --git a/tap-snapshots/test/snapshot/generateAlbumBanner.js.test.cjs b/tap-snapshots/test/snapshot/generateAlbumBanner.js.test.cjs
index 28eaf6d..c900eb4 100644
--- a/tap-snapshots/test/snapshot/generateAlbumBanner.js.test.cjs
+++ b/tap-snapshots/test/snapshot/generateAlbumBanner.js.test.cjs
@@ -5,14 +5,14 @@
  * Make sure to inspect the output below.  Do not ignore changes!
  */
 'use strict'
-exports[`test/snapshot/generateAlbumBanner.js TAP generateAlbumBanner (snapshot) > basic behavior 1`] = `
+exports[`test/snapshot/generateAlbumBanner.js > TAP > generateAlbumBanner (snapshot) > basic behavior 1`] = `
 <div id="banner"><img src="media/album-art/cool-album/banner.png" alt="album banner" width="800" height="200"></div>
 `
 
-exports[`test/snapshot/generateAlbumBanner.js TAP generateAlbumBanner (snapshot) > no banner 1`] = `
+exports[`test/snapshot/generateAlbumBanner.js > TAP > generateAlbumBanner (snapshot) > no banner 1`] = `
 
 `
 
-exports[`test/snapshot/generateAlbumBanner.js TAP generateAlbumBanner (snapshot) > no dimensions 1`] = `
+exports[`test/snapshot/generateAlbumBanner.js > TAP > generateAlbumBanner (snapshot) > no dimensions 1`] = `
 <div id="banner"><img src="media/album-art/cool-album/banner.png" alt="album banner" width="1100" height="200"></div>
 `
diff --git a/tap-snapshots/test/snapshot/generateAlbumCoverArtwork.js.test.cjs b/tap-snapshots/test/snapshot/generateAlbumCoverArtwork.js.test.cjs
index d787df6..2c679fc 100644
--- a/tap-snapshots/test/snapshot/generateAlbumCoverArtwork.js.test.cjs
+++ b/tap-snapshots/test/snapshot/generateAlbumCoverArtwork.js.test.cjs
@@ -5,28 +5,31 @@
  * Make sure to inspect the output below.  Do not ignore changes!
  */
 'use strict'
-exports[`test/snapshot/generateAlbumCoverArtwork.js TAP generateAlbumCoverArtwork (snapshot) > display: primary 1`] = `
+exports[`test/snapshot/generateAlbumCoverArtwork.js > TAP > generateAlbumCoverArtwork (snapshot) > display: primary 1`] = `
 <div id="cover-art-container">
-    <a id="cover-art" class="box image-link" href="media/album-art/bee-forus-seatbelt-safebee/cover.png">
-        <div class="square">
-            <div class="square-content">
-                <div class="reveal">
-                    <div class="image-container"><div class="image-inner-area"><img data-original-size="0" src="media/album-art/bee-forus-seatbelt-safebee/cover.medium.jpg"></div></div>
-                    <span class="reveal-text-container">
-                        <span class="reveal-text">
-                            cw: creepy crawlies
-                            <br>
-                            <span class="reveal-interaction">click to show</span>
-                        </span>
-                    </span>
-                </div>
-            </div>
-        </div>
-    </a>
+    [mocked: image
+     args: [
+       [
+         { name: 'Damara', directory: 'damara', isContentWarning: false },
+         { name: 'Cronus', directory: 'cronus', isContentWarning: false },
+         { name: 'Bees', directory: 'bees', isContentWarning: false },
+         { name: 'creepy crawlies', isContentWarning: true }
+       ]
+     ]
+     slots: { path: [ 'media.albumCover', 'bee-forus-seatbelt-safebee', 'png' ], thumb: 'medium', id: 'cover-art', reveal: true, link: true, square: true }]
     <p>Tags: <a href="tag/damara/">Damara</a>, <a href="tag/cronus/">Cronus</a>, <a href="tag/bees/">Bees</a></p>
 </div>
 `
 
-exports[`test/snapshot/generateAlbumCoverArtwork.js TAP generateAlbumCoverArtwork (snapshot) > display: thumbnail 1`] = `
-<div class="square"><div class="square-content"><div class="image-container"><div class="image-inner-area"><img src="media/album-art/bee-forus-seatbelt-safebee/cover.small.jpg"></div></div></div></div>
+exports[`test/snapshot/generateAlbumCoverArtwork.js > TAP > generateAlbumCoverArtwork (snapshot) > display: thumbnail 1`] = `
+[mocked: image
+ args: [
+   [
+     { name: 'Damara', directory: 'damara', isContentWarning: false },
+     { name: 'Cronus', directory: 'cronus', isContentWarning: false },
+     { name: 'Bees', directory: 'bees', isContentWarning: false },
+     { name: 'creepy crawlies', isContentWarning: true }
+   ]
+ ]
+ slots: { path: [ 'media.albumCover', 'bee-forus-seatbelt-safebee', 'png' ], thumb: 'small', reveal: false, link: false, square: true }]
 `
diff --git a/tap-snapshots/test/snapshot/generateAlbumReleaseInfo.js.test.cjs b/tap-snapshots/test/snapshot/generateAlbumReleaseInfo.js.test.cjs
index e769026..9702cad 100644
--- a/tap-snapshots/test/snapshot/generateAlbumReleaseInfo.js.test.cjs
+++ b/tap-snapshots/test/snapshot/generateAlbumReleaseInfo.js.test.cjs
@@ -5,11 +5,7 @@
  * Make sure to inspect the output below.  Do not ignore changes!
  */
 'use strict'
-exports[`test/snapshot/generateAlbumReleaseInfo.js TAP generateAlbumReleaseInfo (snapshot) > URLs only 1`] = `
-<p>Listen on <a href="https://homestuck.bandcamp.com/foo" class="nowrap">Bandcamp</a> or <a href="https://soundcloud.com/bar" class="nowrap">SoundCloud</a>.</p>
-`
-
-exports[`test/snapshot/generateAlbumReleaseInfo.js TAP generateAlbumReleaseInfo (snapshot) > basic behavior 1`] = `
+exports[`test/snapshot/generateAlbumReleaseInfo.js > TAP > generateAlbumReleaseInfo (snapshot) > basic behavior 1`] = `
 <p>
     By <span class="nowrap"><a href="artist/toby-fox/">Toby Fox</a> (music probably)</span> and <span class="nowrap"><a href="artist/tensei/">Tensei</a> (hot jams) (<span class="icons"><a href="https://tenseimusic.bandcamp.com/" class="icon">
                 <svg>
@@ -33,10 +29,14 @@ exports[`test/snapshot/generateAlbumReleaseInfo.js TAP generateAlbumReleaseInfo
 <p>Listen on <a href="https://homestuck.bandcamp.com/album/alterniabound-with-alternia" class="nowrap">Bandcamp</a>, <a href="https://www.youtube.com/playlist?list=PLnVpmehyaOFZWO9QOZmD6A3TIK0wZ6xE2" class="nowrap">YouTube (playlist)</a>, or <a href="https://www.youtube.com/watch?v=HO5V2uogkYc" class="nowrap">YouTube (full album)</a>.</p>
 `
 
-exports[`test/snapshot/generateAlbumReleaseInfo.js TAP generateAlbumReleaseInfo (snapshot) > equal cover art date 1`] = `
+exports[`test/snapshot/generateAlbumReleaseInfo.js > TAP > generateAlbumReleaseInfo (snapshot) > equal cover art date 1`] = `
 <p>Released 4/12/2020.</p>
 `
 
-exports[`test/snapshot/generateAlbumReleaseInfo.js TAP generateAlbumReleaseInfo (snapshot) > reduced details 1`] = `
+exports[`test/snapshot/generateAlbumReleaseInfo.js > TAP > generateAlbumReleaseInfo (snapshot) > reduced details 1`] = `
 
 `
+
+exports[`test/snapshot/generateAlbumReleaseInfo.js > TAP > generateAlbumReleaseInfo (snapshot) > URLs only 1`] = `
+<p>Listen on <a href="https://homestuck.bandcamp.com/foo" class="nowrap">Bandcamp</a> or <a href="https://soundcloud.com/bar" class="nowrap">SoundCloud</a>.</p>
+`
diff --git a/tap-snapshots/test/snapshot/generateAlbumSecondaryNav.js.test.cjs b/tap-snapshots/test/snapshot/generateAlbumSecondaryNav.js.test.cjs
index f84827a..29d0628 100644
--- a/tap-snapshots/test/snapshot/generateAlbumSecondaryNav.js.test.cjs
+++ b/tap-snapshots/test/snapshot/generateAlbumSecondaryNav.js.test.cjs
@@ -5,29 +5,29 @@
  * Make sure to inspect the output below.  Do not ignore changes!
  */
 'use strict'
-exports[`test/snapshot/generateAlbumSecondaryNav.js TAP generateAlbumSecondaryNav (snapshot) > basic behavior, mode: album 1`] = `
+exports[`test/snapshot/generateAlbumSecondaryNav.js > TAP > generateAlbumSecondaryNav (snapshot) > basic behavior, mode: album 1`] = `
 <nav id="secondary-nav" class="nav-links-groups">
-    <span>
+    <span style="--primary-color: #abcdef; --dark-color: #21272e; --dim-color: #818181; --dim-ghost-color: #818181cc; --bg-color: #161616cc; --bg-black-color: #06090bcc; --shadow-color: #0d0d0dcc">
         <a href="group/vcg/">VCG</a>
-        (<a href="album/first/">Previous</a>, <a href="album/last/">Next</a>)
+        (<a href="album/first/" title="First">Previous</a>, <a href="album/last/" title="Last">Next</a>)
     </span>
-    <span>
+    <span style="--primary-color: #123456; --dark-color: #0e2842; --dim-color: #000000; --dim-ghost-color: #000000cc; --bg-color: #161616cc; --bg-black-color: #000913cc; --shadow-color: #0d0d0dcc">
         <a href="group/bepis/">Bepis</a>
-        (<a href="album/second/">Next</a>)
+        (<a href="album/second/" title="Second">Next</a>)
     </span>
 </nav>
 `
 
-exports[`test/snapshot/generateAlbumSecondaryNav.js TAP generateAlbumSecondaryNav (snapshot) > basic behavior, mode: track 1`] = `
+exports[`test/snapshot/generateAlbumSecondaryNav.js > TAP > generateAlbumSecondaryNav (snapshot) > basic behavior, mode: track 1`] = `
 <nav id="secondary-nav" class="nav-links-groups">
-    <a href="group/vcg/">VCG</a>
-    <a href="group/bepis/">Bepis</a>
+    <a href="group/vcg/" style="--primary-color: #abcdef; --dim-color: #818181">VCG</a>
+    <a href="group/bepis/" style="--primary-color: #123456; --dim-color: #000000">Bepis</a>
 </nav>
 `
 
-exports[`test/snapshot/generateAlbumSecondaryNav.js TAP generateAlbumSecondaryNav (snapshot) > dateless album in mixed group 1`] = `
+exports[`test/snapshot/generateAlbumSecondaryNav.js > TAP > generateAlbumSecondaryNav (snapshot) > dateless album in mixed group 1`] = `
 <nav id="secondary-nav" class="nav-links-groups">
-    <a href="group/vcg/">VCG</a>
-    <a href="group/bepis/">Bepis</a>
+    <a href="group/vcg/" style="--primary-color: #abcdef; --dim-color: #818181">VCG</a>
+    <a href="group/bepis/" style="--primary-color: #123456; --dim-color: #000000">Bepis</a>
 </nav>
 `
diff --git a/tap-snapshots/test/snapshot/generateAlbumSidebarGroupBox.js.test.cjs b/tap-snapshots/test/snapshot/generateAlbumSidebarGroupBox.js.test.cjs
index 3be8496..cd820cd 100644
--- a/tap-snapshots/test/snapshot/generateAlbumSidebarGroupBox.js.test.cjs
+++ b/tap-snapshots/test/snapshot/generateAlbumSidebarGroupBox.js.test.cjs
@@ -5,7 +5,7 @@
  * Make sure to inspect the output below.  Do not ignore changes!
  */
 'use strict'
-exports[`test/snapshot/generateAlbumSidebarGroupBox.js TAP generateAlbumSidebarGroupBox (snapshot) > basic behavior, mode: album 1`] = `
+exports[`test/snapshot/generateAlbumSidebarGroupBox.js > TAP > generateAlbumSidebarGroupBox (snapshot) > basic behavior, mode: album 1`] = `
 <h1><a href="group/vcg/">VCG</a></h1>
 Very cool group.
 <p>Visit on <a href="https://vcg.bandcamp.com/" class="nowrap">Bandcamp</a> or <a href="https://youtube.com/@vcg" class="nowrap">YouTube</a>.</p>
@@ -13,12 +13,12 @@ Very cool group.
 <p class="group-chronology-link">Previous: <a href="album/first/">First</a></p>
 `
 
-exports[`test/snapshot/generateAlbumSidebarGroupBox.js TAP generateAlbumSidebarGroupBox (snapshot) > basic behavior, mode: track 1`] = `
+exports[`test/snapshot/generateAlbumSidebarGroupBox.js > TAP > generateAlbumSidebarGroupBox (snapshot) > basic behavior, mode: track 1`] = `
 <h1><a href="group/vcg/">VCG</a></h1>
 <p>Visit on <a href="https://vcg.bandcamp.com/" class="nowrap">Bandcamp</a> or <a href="https://youtube.com/@vcg" class="nowrap">YouTube</a>.</p>
 `
 
-exports[`test/snapshot/generateAlbumSidebarGroupBox.js TAP generateAlbumSidebarGroupBox (snapshot) > dateless album in mixed group 1`] = `
+exports[`test/snapshot/generateAlbumSidebarGroupBox.js > TAP > generateAlbumSidebarGroupBox (snapshot) > dateless album in mixed group 1`] = `
 <h1><a href="group/vcg/">VCG</a></h1>
 Very cool group.
 <p>Visit on <a href="https://vcg.bandcamp.com/" class="nowrap">Bandcamp</a> or <a href="https://youtube.com/@vcg" class="nowrap">YouTube</a>.</p>
diff --git a/tap-snapshots/test/snapshot/generateAlbumTrackList.js.test.cjs b/tap-snapshots/test/snapshot/generateAlbumTrackList.js.test.cjs
index 59eb445..304717d 100644
--- a/tap-snapshots/test/snapshot/generateAlbumTrackList.js.test.cjs
+++ b/tap-snapshots/test/snapshot/generateAlbumTrackList.js.test.cjs
@@ -5,7 +5,7 @@
  * Make sure to inspect the output below.  Do not ignore changes!
  */
 'use strict'
-exports[`test/snapshot/generateAlbumTrackList.js TAP generateAlbumTrackList (snapshot) > basic behavior, default track section 1`] = `
+exports[`test/snapshot/generateAlbumTrackList.js > TAP > generateAlbumTrackList (snapshot) > basic behavior, default track section 1`] = `
 <ul>
     <li>(0:20) <a href="track/t1/">Track 1</a></li>
     <li>(0:30) <a href="track/t2/">Track 2</a></li>
@@ -14,7 +14,7 @@ exports[`test/snapshot/generateAlbumTrackList.js TAP generateAlbumTrackList (sna
 </ul>
 `
 
-exports[`test/snapshot/generateAlbumTrackList.js TAP generateAlbumTrackList (snapshot) > basic behavior, with track sections 1`] = `
+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">First section (~1:30):</dt>
     <dd>
diff --git a/tap-snapshots/test/snapshot/generateBanner.js.test.cjs b/tap-snapshots/test/snapshot/generateBanner.js.test.cjs
index 24e4960..bf2c03c 100644
--- a/tap-snapshots/test/snapshot/generateBanner.js.test.cjs
+++ b/tap-snapshots/test/snapshot/generateBanner.js.test.cjs
@@ -5,10 +5,10 @@
  * Make sure to inspect the output below.  Do not ignore changes!
  */
 'use strict'
-exports[`test/snapshot/generateBanner.js TAP generateBanner (snapshot) > basic behavior 1`] = `
+exports[`test/snapshot/generateBanner.js > TAP > generateBanner (snapshot) > basic behavior 1`] = `
 <div id="banner"><img src="media/album-art/cool-album/banner.png" alt="Very cool banner art." width="800" height="200"></div>
 `
 
-exports[`test/snapshot/generateBanner.js TAP generateBanner (snapshot) > no dimensions 1`] = `
+exports[`test/snapshot/generateBanner.js > TAP > generateBanner (snapshot) > no dimensions 1`] = `
 <div id="banner"><img src="media/album-art/cool-album/banner.png" width="1100" height="200"></div>
 `
diff --git a/tap-snapshots/test/snapshot/generateCoverArtwork.js.test.cjs b/tap-snapshots/test/snapshot/generateCoverArtwork.js.test.cjs
index 88be76e..bc1432d 100644
--- a/tap-snapshots/test/snapshot/generateCoverArtwork.js.test.cjs
+++ b/tap-snapshots/test/snapshot/generateCoverArtwork.js.test.cjs
@@ -5,28 +5,31 @@
  * Make sure to inspect the output below.  Do not ignore changes!
  */
 'use strict'
-exports[`test/snapshot/generateCoverArtwork.js TAP generateCoverArtwork (snapshot) > display: primary 1`] = `
+exports[`test/snapshot/generateCoverArtwork.js > TAP > generateCoverArtwork (snapshot) > display: primary 1`] = `
 <div id="cover-art-container">
-    <a id="cover-art" class="box image-link" href="media/album-art/bee-forus-seatbelt-safebee/cover.png">
-        <div class="square">
-            <div class="square-content">
-                <div class="reveal">
-                    <div class="image-container"><div class="image-inner-area"><img data-original-size="0" src="media/album-art/bee-forus-seatbelt-safebee/cover.medium.jpg"></div></div>
-                    <span class="reveal-text-container">
-                        <span class="reveal-text">
-                            cw: creepy crawlies
-                            <br>
-                            <span class="reveal-interaction">click to show</span>
-                        </span>
-                    </span>
-                </div>
-            </div>
-        </div>
-    </a>
+    [mocked: image
+     args: [
+       [
+         { name: 'Damara', directory: 'damara', isContentWarning: false },
+         { name: 'Cronus', directory: 'cronus', isContentWarning: false },
+         { name: 'Bees', directory: 'bees', isContentWarning: false },
+         { name: 'creepy crawlies', isContentWarning: true }
+       ]
+     ]
+     slots: { path: [ 'media.albumCover', 'bee-forus-seatbelt-safebee', 'png' ], thumb: 'medium', id: 'cover-art', reveal: true, link: true, square: true }]
     <p>Tags: <a href="tag/damara/">Damara</a>, <a href="tag/cronus/">Cronus</a>, <a href="tag/bees/">Bees</a></p>
 </div>
 `
 
-exports[`test/snapshot/generateCoverArtwork.js TAP generateCoverArtwork (snapshot) > display: thumbnail 1`] = `
-<div class="square"><div class="square-content"><div class="image-container"><div class="image-inner-area"><img src="media/album-art/bee-forus-seatbelt-safebee/cover.small.jpg"></div></div></div></div>
+exports[`test/snapshot/generateCoverArtwork.js > TAP > generateCoverArtwork (snapshot) > display: thumbnail 1`] = `
+[mocked: image
+ args: [
+   [
+     { name: 'Damara', directory: 'damara', isContentWarning: false },
+     { name: 'Cronus', directory: 'cronus', isContentWarning: false },
+     { name: 'Bees', directory: 'bees', isContentWarning: false },
+     { name: 'creepy crawlies', isContentWarning: true }
+   ]
+ ]
+ slots: { path: [ 'media.albumCover', 'bee-forus-seatbelt-safebee', 'png' ], thumb: 'small', reveal: false, link: false, square: true }]
 `
diff --git a/tap-snapshots/test/snapshot/generatePreviousNextLinks.js.test.cjs b/tap-snapshots/test/snapshot/generatePreviousNextLinks.js.test.cjs
index 0726858..ed7b882 100644
--- a/tap-snapshots/test/snapshot/generatePreviousNextLinks.js.test.cjs
+++ b/tap-snapshots/test/snapshot/generatePreviousNextLinks.js.test.cjs
@@ -5,24 +5,24 @@
  * Make sure to inspect the output below.  Do not ignore changes!
  */
 'use strict'
-exports[`test/snapshot/generatePreviousNextLinks.js TAP generatePreviousNextLinks (snapshot) > basic behavior 1`] = `
-previous: {"tooltip":true,"color":false,"attributes":{"id":"previous-button"},"content":"Previous"}
-next: {"tooltip":true,"color":false,"attributes":{"id":"next-button"},"content":"Next"}
+exports[`test/snapshot/generatePreviousNextLinks.js > TAP > generatePreviousNextLinks (snapshot) > basic behavior 1`] = `
+previous: { tooltip: true, color: false, attributes: { id: 'previous-button' }, content: Tag (no name, 1 items) }
+next: { tooltip: true, color: false, attributes: { id: 'next-button' }, content: Tag (no name, 1 items) }
 `
 
-exports[`test/snapshot/generatePreviousNextLinks.js TAP generatePreviousNextLinks (snapshot) > disable id 1`] = `
-previous: {"tooltip":true,"color":false,"attributes":{"id":false},"content":"Previous"}
-next: {"tooltip":true,"color":false,"attributes":{"id":false},"content":"Next"}
+exports[`test/snapshot/generatePreviousNextLinks.js > TAP > generatePreviousNextLinks (snapshot) > disable id 1`] = `
+previous: { tooltip: true, color: false, attributes: { id: false }, content: Tag (no name, 1 items) }
+next: { tooltip: true, color: false, attributes: { id: false }, content: Tag (no name, 1 items) }
 `
 
-exports[`test/snapshot/generatePreviousNextLinks.js TAP generatePreviousNextLinks (snapshot) > neither link present 1`] = `
+exports[`test/snapshot/generatePreviousNextLinks.js > TAP > generatePreviousNextLinks (snapshot) > neither link present 1`] = `
 
 `
 
-exports[`test/snapshot/generatePreviousNextLinks.js TAP generatePreviousNextLinks (snapshot) > next missing 1`] = `
-previous: {"tooltip":true,"color":false,"attributes":{"id":"previous-button"},"content":"Previous"}
+exports[`test/snapshot/generatePreviousNextLinks.js > TAP > generatePreviousNextLinks (snapshot) > next missing 1`] = `
+previous: { tooltip: true, color: false, attributes: { id: 'previous-button' }, content: Tag (no name, 1 items) }
 `
 
-exports[`test/snapshot/generatePreviousNextLinks.js TAP generatePreviousNextLinks (snapshot) > previous missing 1`] = `
-next: {"tooltip":true,"color":false,"attributes":{"id":"next-button"},"content":"Next"}
+exports[`test/snapshot/generatePreviousNextLinks.js > TAP > generatePreviousNextLinks (snapshot) > previous missing 1`] = `
+next: { tooltip: true, color: false, attributes: { id: 'next-button' }, content: Tag (no name, 1 items) }
 `
diff --git a/tap-snapshots/test/snapshot/generateTrackCoverArtwork.js.test.cjs b/tap-snapshots/test/snapshot/generateTrackCoverArtwork.js.test.cjs
index 92216a8..78063c4 100644
--- a/tap-snapshots/test/snapshot/generateTrackCoverArtwork.js.test.cjs
+++ b/tap-snapshots/test/snapshot/generateTrackCoverArtwork.js.test.cjs
@@ -5,39 +5,46 @@
  * Make sure to inspect the output below.  Do not ignore changes!
  */
 'use strict'
-exports[`test/snapshot/generateTrackCoverArtwork.js TAP generateTrackCoverArtwork (snapshot) > display: primary - no unique art 1`] = `
+exports[`test/snapshot/generateTrackCoverArtwork.js > TAP > generateTrackCoverArtwork (snapshot) > display: primary - no unique art 1`] = `
 <div id="cover-art-container">
-    <a id="cover-art" class="box image-link" href="media/album-art/bee-forus-seatbelt-safebee/cover.png">
-        <div class="square">
-            <div class="square-content">
-                <div class="reveal">
-                    <div class="image-container"><div class="image-inner-area"><img data-original-size="0" src="media/album-art/bee-forus-seatbelt-safebee/cover.medium.jpg"></div></div>
-                    <span class="reveal-text-container">
-                        <span class="reveal-text">
-                            cw: creepy crawlies
-                            <br>
-                            <span class="reveal-interaction">click to show</span>
-                        </span>
-                    </span>
-                </div>
-            </div>
-        </div>
-    </a>
+    [mocked: image
+     args: [
+       [
+         { name: 'Damara', directory: 'damara', isContentWarning: false },
+         { name: 'Cronus', directory: 'cronus', isContentWarning: false },
+         { name: 'Bees', directory: 'bees', isContentWarning: false },
+         { name: 'creepy crawlies', isContentWarning: true }
+       ]
+     ]
+     slots: { path: [ 'media.albumCover', 'bee-forus-seatbelt-safebee', 'png' ], thumb: 'medium', id: 'cover-art', reveal: true, link: true, square: true }]
     <p>Tags: <a href="tag/damara/">Damara</a>, <a href="tag/cronus/">Cronus</a>, <a href="tag/bees/">Bees</a></p>
 </div>
 `
 
-exports[`test/snapshot/generateTrackCoverArtwork.js TAP generateTrackCoverArtwork (snapshot) > display: primary - unique art 1`] = `
+exports[`test/snapshot/generateTrackCoverArtwork.js > TAP > generateTrackCoverArtwork (snapshot) > display: primary - unique art 1`] = `
 <div id="cover-art-container">
-    <a id="cover-art" class="box image-link" href="media/album-art/bee-forus-seatbelt-safebee/beesmp3.jpg"><div class="square"><div class="square-content"><div class="image-container"><div class="image-inner-area"><img data-original-size="0" src="media/album-art/bee-forus-seatbelt-safebee/beesmp3.medium.jpg"></div></div></div></div></a>
+    [mocked: image
+     args: [ [ { name: 'Bees', directory: 'bees', isContentWarning: false } ] ]
+     slots: { path: [ 'media.trackCover', 'bee-forus-seatbelt-safebee', 'beesmp3', 'jpg' ], thumb: 'medium', id: 'cover-art', reveal: true, link: true, square: true }]
     <p>Tags: <a href="tag/bees/">Bees</a></p>
 </div>
 `
 
-exports[`test/snapshot/generateTrackCoverArtwork.js TAP generateTrackCoverArtwork (snapshot) > display: thumbnail - no unique art 1`] = `
-<div class="square"><div class="square-content"><div class="image-container"><div class="image-inner-area"><img src="media/album-art/bee-forus-seatbelt-safebee/cover.small.jpg"></div></div></div></div>
+exports[`test/snapshot/generateTrackCoverArtwork.js > TAP > generateTrackCoverArtwork (snapshot) > display: thumbnail - no unique art 1`] = `
+[mocked: image
+ args: [
+   [
+     { name: 'Damara', directory: 'damara', isContentWarning: false },
+     { name: 'Cronus', directory: 'cronus', isContentWarning: false },
+     { name: 'Bees', directory: 'bees', isContentWarning: false },
+     { name: 'creepy crawlies', isContentWarning: true }
+   ]
+ ]
+ slots: { path: [ 'media.albumCover', 'bee-forus-seatbelt-safebee', 'png' ], thumb: 'small', reveal: false, link: false, square: true }]
 `
 
-exports[`test/snapshot/generateTrackCoverArtwork.js TAP generateTrackCoverArtwork (snapshot) > display: thumbnail - unique art 1`] = `
-<div class="square"><div class="square-content"><div class="image-container"><div class="image-inner-area"><img src="media/album-art/bee-forus-seatbelt-safebee/beesmp3.small.jpg"></div></div></div></div>
+exports[`test/snapshot/generateTrackCoverArtwork.js > TAP > generateTrackCoverArtwork (snapshot) > display: thumbnail - unique art 1`] = `
+[mocked: image
+ args: [ [ { name: 'Bees', directory: 'bees', isContentWarning: false } ] ]
+ slots: { path: [ 'media.trackCover', 'bee-forus-seatbelt-safebee', 'beesmp3', 'jpg' ], thumb: 'small', reveal: false, link: false, square: true }]
 `
diff --git a/tap-snapshots/test/snapshot/generateTrackReleaseInfo.js.test.cjs b/tap-snapshots/test/snapshot/generateTrackReleaseInfo.js.test.cjs
index e94ed82..2add28e 100644
--- a/tap-snapshots/test/snapshot/generateTrackReleaseInfo.js.test.cjs
+++ b/tap-snapshots/test/snapshot/generateTrackReleaseInfo.js.test.cjs
@@ -5,7 +5,7 @@
  * Make sure to inspect the output below.  Do not ignore changes!
  */
 'use strict'
-exports[`test/snapshot/generateTrackReleaseInfo.js TAP generateTrackReleaseInfo (snapshot) > basic behavior 1`] = `
+exports[`test/snapshot/generateTrackReleaseInfo.js > TAP > generateTrackReleaseInfo (snapshot) > basic behavior 1`] = `
 <p>
     By <a href="artist/toby-fox/">Toby Fox</a>.
     <br>
@@ -16,21 +16,21 @@ exports[`test/snapshot/generateTrackReleaseInfo.js TAP generateTrackReleaseInfo
 <p>Listen on <a href="https://soundcloud.com/foo" class="nowrap">SoundCloud</a> or <a href="https://youtube.com/watch?v=bar" class="nowrap">YouTube</a>.</p>
 `
 
-exports[`test/snapshot/generateTrackReleaseInfo.js TAP generateTrackReleaseInfo (snapshot) > cover artist contribs, non-unique 1`] = `
+exports[`test/snapshot/generateTrackReleaseInfo.js > TAP > generateTrackReleaseInfo (snapshot) > cover artist contribs, non-unique 1`] = `
 <p>By <a href="artist/toby-fox/">Toby Fox</a>.</p>
-<p>This wiki doesn&apos;t have any listening links for <i>Suspicious Track</i>.</p>
+<p>This wiki doesn't have any listening links for <i>Suspicious Track</i>.</p>
 `
 
-exports[`test/snapshot/generateTrackReleaseInfo.js TAP generateTrackReleaseInfo (snapshot) > cover artist contribs, unique 1`] = `
+exports[`test/snapshot/generateTrackReleaseInfo.js > TAP > generateTrackReleaseInfo (snapshot) > cover artist contribs, unique 1`] = `
 <p>
     By <a href="artist/toby-fox/">Toby Fox</a>.
     <br>
-    Cover art by <span class="nowrap"><a href="artist/alpaca/">Alpaca</a> (🔥)</span>.
+    Cover art by <span class="nowrap"><a href="artist/alpaca/">Alpaca</a> (&#x1F525;)</span>.
 </p>
-<p>This wiki doesn&apos;t have any listening links for <i>Suspicious Track</i>.</p>
+<p>This wiki doesn't have any listening links for <i>Suspicious Track</i>.</p>
 `
 
-exports[`test/snapshot/generateTrackReleaseInfo.js TAP generateTrackReleaseInfo (snapshot) > reduced details 1`] = `
+exports[`test/snapshot/generateTrackReleaseInfo.js > TAP > generateTrackReleaseInfo (snapshot) > reduced details 1`] = `
 <p>By <a href="artist/toby-fox/">Toby Fox</a>.</p>
-<p>This wiki doesn&apos;t have any listening links for <i>Suspicious Track</i>.</p>
+<p>This wiki doesn't have any listening links for <i>Suspicious Track</i>.</p>
 `
diff --git a/tap-snapshots/test/snapshot/image.js.test.cjs b/tap-snapshots/test/snapshot/image.js.test.cjs
index c2f6aea..f88141d 100644
--- a/tap-snapshots/test/snapshot/image.js.test.cjs
+++ b/tap-snapshots/test/snapshot/image.js.test.cjs
@@ -5,7 +5,7 @@
  * Make sure to inspect the output below.  Do not ignore changes!
  */
 'use strict'
-exports[`test/snapshot/image.js TAP image (snapshot) > content warnings via tags 1`] = `
+exports[`test/snapshot/image.js > TAP > image (snapshot) > content warnings via tags 1`] = `
 <div class="reveal">
     <div class="image-container"><div class="image-inner-area"><img src="media/album-art/beyond-canon/cover.png"></div></div>
     <span class="reveal-text-container">
@@ -18,43 +18,59 @@ exports[`test/snapshot/image.js TAP image (snapshot) > content warnings via tags
 </div>
 `
 
-exports[`test/snapshot/image.js TAP image (snapshot) > id with link 1`] = `
+exports[`test/snapshot/image.js > TAP > image (snapshot) > id with link 1`] = `
 <a id="banana" class="box image-link" href="foobar"><div class="image-container"><div class="image-inner-area"><img src="foobar"></div></div></a>
 `
 
-exports[`test/snapshot/image.js TAP image (snapshot) > id with square 1`] = `
+exports[`test/snapshot/image.js > TAP > image (snapshot) > id with square 1`] = `
 <div class="square"><div class="square-content"><div class="image-container"><div class="image-inner-area"><img id="banana" src="foobar"></div></div></div></div>
 `
 
-exports[`test/snapshot/image.js TAP image (snapshot) > id without link 1`] = `
+exports[`test/snapshot/image.js > TAP > image (snapshot) > id without link 1`] = `
 <div class="image-container"><div class="image-inner-area"><img id="banana" src="foobar"></div></div>
 `
 
-exports[`test/snapshot/image.js TAP image (snapshot) > lazy with square 1`] = `
+exports[`test/snapshot/image.js > TAP > image (snapshot) > lazy with square 1`] = `
 <noscript><div class="square"><div class="square-content"><div class="image-container"><div class="image-inner-area"><img src="foobar"></div></div></div></div></noscript>
 <div class="square js-hide"><div class="square-content"><div class="image-container"><div class="image-inner-area"><img class="lazy" data-original="foobar"></div></div></div></div>
 `
 
-exports[`test/snapshot/image.js TAP image (snapshot) > link with file size 1`] = `
-<a class="box image-link" href="media/album-art/pingas/cover.png"><div class="image-container"><div class="image-inner-area"><img data-original-size="1000000" src="media/album-art/pingas/cover.png"></div></div></a>
+exports[`test/snapshot/image.js > TAP > image (snapshot) > link with file size 1`] = `
+<a class="box image-link" href="media/album-art/pingas/cover.png"><div class="image-container"><div class="image-inner-area"><img src="media/album-art/pingas/cover.png"></div></div></a>
 `
 
-exports[`test/snapshot/image.js TAP image (snapshot) > source missing 1`] = `
+exports[`test/snapshot/image.js > TAP > image (snapshot) > missing image path 1`] = `
+<div class="image-container"><div class="image-inner-area"><div class="image-text-area">(This image file is missing)</div></div></div>
+`
+
+exports[`test/snapshot/image.js > TAP > image (snapshot) > missing image path w/ missingSourceContent 1`] = `
+<div class="image-container"><div class="image-inner-area"><div class="image-text-area">Cover's missing, whoops</div></div></div>
+`
+
+exports[`test/snapshot/image.js > TAP > image (snapshot) > source missing 1`] = `
 <div class="image-container placeholder-image"><div class="image-inner-area"><div class="image-text-area">Example of missing source message.</div></div></div>
 `
 
-exports[`test/snapshot/image.js TAP image (snapshot) > source via path 1`] = `
+exports[`test/snapshot/image.js > TAP > image (snapshot) > source via path 1`] = `
 <div class="image-container"><div class="image-inner-area"><img src="media/album-art/beyond-canon/cover.png"></div></div>
 `
 
-exports[`test/snapshot/image.js TAP image (snapshot) > source via src 1`] = `
+exports[`test/snapshot/image.js > TAP > image (snapshot) > source via src 1`] = `
 <div class="image-container"><div class="image-inner-area"><img src="https://example.com/bananas.gif"></div></div>
 `
 
-exports[`test/snapshot/image.js TAP image (snapshot) > square 1`] = `
+exports[`test/snapshot/image.js > TAP > image (snapshot) > square 1`] = `
 <div class="square"><div class="square-content"><div class="image-container"><div class="image-inner-area"><img src="foobar"></div></div></div></div>
 `
 
-exports[`test/snapshot/image.js TAP image (snapshot) > width & height 1`] = `
+exports[`test/snapshot/image.js > TAP > image (snapshot) > thumb requested but source is gif 1`] = `
+<div class="image-container"><div class="image-inner-area"><img src="media/flash-art/5426.gif"></div></div>
+`
+
+exports[`test/snapshot/image.js > TAP > image (snapshot) > thumbnail details 1`] = `
+<div class="image-container"><div class="image-inner-area"><img data-original-length="1200" data-thumbs="voluminous:1200 middling:900 petite:20" src="media/album-art/beyond-canon/cover.voluminous.jpg"></div></div>
+`
+
+exports[`test/snapshot/image.js > TAP > image (snapshot) > width & height 1`] = `
 <div class="image-container"><div class="image-inner-area"><img width="600" height="400" src="foobar"></div></div>
 `
diff --git a/tap-snapshots/test/snapshot/linkArtist.js.test.cjs b/tap-snapshots/test/snapshot/linkArtist.js.test.cjs
index 77516f3..6b532ae 100644
--- a/tap-snapshots/test/snapshot/linkArtist.js.test.cjs
+++ b/tap-snapshots/test/snapshot/linkArtist.js.test.cjs
@@ -5,10 +5,10 @@
  * Make sure to inspect the output below.  Do not ignore changes!
  */
 'use strict'
-exports[`test/snapshot/linkArtist.js TAP linkArtist (snapshot) > basic behavior 1`] = `
+exports[`test/snapshot/linkArtist.js > TAP > linkArtist (snapshot) > basic behavior 1`] = `
 <a href="artist/toby-fox/">Toby Fox</a>
 `
 
-exports[`test/snapshot/linkArtist.js TAP linkArtist (snapshot) > prefer short name 1`] = `
+exports[`test/snapshot/linkArtist.js > TAP > linkArtist (snapshot) > prefer short name 1`] = `
 <a href="artist/55gore/">55gore</a>
 `
diff --git a/tap-snapshots/test/snapshot/linkContribution.js.test.cjs b/tap-snapshots/test/snapshot/linkContribution.js.test.cjs
index 7124ad1..75b9d27 100644
--- a/tap-snapshots/test/snapshot/linkContribution.js.test.cjs
+++ b/tap-snapshots/test/snapshot/linkContribution.js.test.cjs
@@ -5,7 +5,7 @@
  * Make sure to inspect the output below.  Do not ignore changes!
  */
 'use strict'
-exports[`test/snapshot/linkContribution.js TAP linkContribution (snapshot) > loads of links 1`] = `
+exports[`test/snapshot/linkContribution.js > TAP > linkContribution (snapshot) > loads of links 1`] = `
 <span class="nowrap"><a href="artist/lorem-ipsum-lover/">Lorem Ipsum Lover</a> (<span class="icons"><a href="https://loremipsum.io" class="icon">
             <svg>
                 <title>External (loremipsum.io)</title>
@@ -29,20 +29,20 @@ exports[`test/snapshot/linkContribution.js TAP linkContribution (snapshot) > loa
         </a></span>)</span>
 `
 
-exports[`test/snapshot/linkContribution.js TAP linkContribution (snapshot) > no accents 1`] = `
+exports[`test/snapshot/linkContribution.js > TAP > linkContribution (snapshot) > no accents 1`] = `
 <a href="artist/clark-powell/">Clark Powell</a>
-<a href="artist/the-big-baddies/">Grounder & Scratch</a>
+<a href="artist/the-big-baddies/">Grounder &amp; Scratch</a>
 <a href="artist/toby-fox/">Toby Fox</a>
 `
 
-exports[`test/snapshot/linkContribution.js TAP linkContribution (snapshot) > no preventWrapping 1`] = `
+exports[`test/snapshot/linkContribution.js > TAP > linkContribution (snapshot) > no preventWrapping 1`] = `
 <a href="artist/clark-powell/">Clark Powell</a> (<span class="icons"><a href="https://soundcloud.com/plazmataz" class="icon">
         <svg>
             <title>SoundCloud</title>
             <use href="static/icons.svg#icon-soundcloud"></use>
         </svg>
     </a></span>)
-<a href="artist/the-big-baddies/">Grounder & Scratch</a> (Snooping)
+<a href="artist/the-big-baddies/">Grounder &amp; Scratch</a> (Snooping)
 <a href="artist/toby-fox/">Toby Fox</a> (Arrangement) (<span class="icons"><a href="https://tobyfox.bandcamp.com/" class="icon">
         <svg>
             <title>Bandcamp</title>
@@ -56,20 +56,20 @@ exports[`test/snapshot/linkContribution.js TAP linkContribution (snapshot) > no
     </a></span>)
 `
 
-exports[`test/snapshot/linkContribution.js TAP linkContribution (snapshot) > only showContribution 1`] = `
+exports[`test/snapshot/linkContribution.js > TAP > linkContribution (snapshot) > only showContribution 1`] = `
 <a href="artist/clark-powell/">Clark Powell</a>
-<span class="nowrap"><a href="artist/the-big-baddies/">Grounder & Scratch</a> (Snooping)</span>
+<span class="nowrap"><a href="artist/the-big-baddies/">Grounder &amp; Scratch</a> (Snooping)</span>
 <span class="nowrap"><a href="artist/toby-fox/">Toby Fox</a> (Arrangement)</span>
 `
 
-exports[`test/snapshot/linkContribution.js TAP linkContribution (snapshot) > only showIcons 1`] = `
+exports[`test/snapshot/linkContribution.js > TAP > linkContribution (snapshot) > only showIcons 1`] = `
 <span class="nowrap"><a href="artist/clark-powell/">Clark Powell</a> (<span class="icons"><a href="https://soundcloud.com/plazmataz" class="icon">
             <svg>
                 <title>SoundCloud</title>
                 <use href="static/icons.svg#icon-soundcloud"></use>
             </svg>
         </a></span>)</span>
-<a href="artist/the-big-baddies/">Grounder & Scratch</a>
+<a href="artist/the-big-baddies/">Grounder &amp; Scratch</a>
 <span class="nowrap"><a href="artist/toby-fox/">Toby Fox</a> (<span class="icons"><a href="https://tobyfox.bandcamp.com/" class="icon">
             <svg>
                 <title>Bandcamp</title>
@@ -83,14 +83,14 @@ exports[`test/snapshot/linkContribution.js TAP linkContribution (snapshot) > onl
         </a></span>)</span>
 `
 
-exports[`test/snapshot/linkContribution.js TAP linkContribution (snapshot) > showContribution & showIcons 1`] = `
+exports[`test/snapshot/linkContribution.js > TAP > linkContribution (snapshot) > showContribution & showIcons 1`] = `
 <span class="nowrap"><a href="artist/clark-powell/">Clark Powell</a> (<span class="icons"><a href="https://soundcloud.com/plazmataz" class="icon">
             <svg>
                 <title>SoundCloud</title>
                 <use href="static/icons.svg#icon-soundcloud"></use>
             </svg>
         </a></span>)</span>
-<span class="nowrap"><a href="artist/the-big-baddies/">Grounder & Scratch</a> (Snooping)</span>
+<span class="nowrap"><a href="artist/the-big-baddies/">Grounder &amp; Scratch</a> (Snooping)</span>
 <span class="nowrap"><a href="artist/toby-fox/">Toby Fox</a> (Arrangement) (<span class="icons"><a href="https://tobyfox.bandcamp.com/" class="icon">
             <svg>
                 <title>Bandcamp</title>
diff --git a/tap-snapshots/test/snapshot/linkExternal.js.test.cjs b/tap-snapshots/test/snapshot/linkExternal.js.test.cjs
index 156b7f9..cd6dca7 100644
--- a/tap-snapshots/test/snapshot/linkExternal.js.test.cjs
+++ b/tap-snapshots/test/snapshot/linkExternal.js.test.cjs
@@ -5,7 +5,7 @@
  * Make sure to inspect the output below.  Do not ignore changes!
  */
 'use strict'
-exports[`test/snapshot/linkExternal.js TAP linkExternal (snapshot) > basic domain matches 1`] = `
+exports[`test/snapshot/linkExternal.js > TAP > linkExternal (snapshot) > basic domain matches 1`] = `
 <a href="https://homestuck.bandcamp.com/" class="nowrap">Bandcamp</a>
 <a href="https://soundcloud.com/plazmataz" class="nowrap">SoundCloud</a>
 <a href="https://aeritus.tumblr.com/" class="nowrap">Tumblr</a>
@@ -19,21 +19,21 @@ exports[`test/snapshot/linkExternal.js TAP linkExternal (snapshot) > basic domai
 <a href="https://buzinkai.newgrounds.com/" class="nowrap">Newgrounds</a>
 `
 
-exports[`test/snapshot/linkExternal.js TAP linkExternal (snapshot) > custom domains for common platforms 1`] = `
+exports[`test/snapshot/linkExternal.js > TAP > linkExternal (snapshot) > custom domains for common platforms 1`] = `
 <a href="https://music.solatrus.com/" class="nowrap">music.solatrus.com</a>
 <a href="https://types.pl/" class="nowrap">Mastodon (types.pl)</a>
 `
 
-exports[`test/snapshot/linkExternal.js TAP linkExternal (snapshot) > custom matches - album 1`] = `
+exports[`test/snapshot/linkExternal.js > TAP > linkExternal (snapshot) > custom matches - album 1`] = `
 <a href="https://youtu.be/abc" class="nowrap">YouTube (full album)</a>
 <a href="https://youtube.com/watch?v=abc" class="nowrap">YouTube (full album)</a>
 <a href="https://youtube.com/Playlist?list=kweh" class="nowrap">YouTube (playlist)</a>
 `
 
-exports[`test/snapshot/linkExternal.js TAP linkExternal (snapshot) > missing domain (arbitrary local path) 1`] = `
+exports[`test/snapshot/linkExternal.js > TAP > linkExternal (snapshot) > missing domain (arbitrary local path) 1`] = `
 <a href="/foo/bar/baz.mp3" class="nowrap">Wiki Archive (local upload)</a>
 `
 
-exports[`test/snapshot/linkExternal.js TAP linkExternal (snapshot) > unknown domain (arbitrary world wide web path) 1`] = `
+exports[`test/snapshot/linkExternal.js > TAP > linkExternal (snapshot) > unknown domain (arbitrary world wide web path) 1`] = `
 <a href="https://snoo.ping.as/usual/i/see/" class="nowrap">snoo.ping.as</a>
 `
diff --git a/tap-snapshots/test/snapshot/linkExternalFlash.js.test.cjs b/tap-snapshots/test/snapshot/linkExternalFlash.js.test.cjs
index d7f6c1c..d29d0dd 100644
--- a/tap-snapshots/test/snapshot/linkExternalFlash.js.test.cjs
+++ b/tap-snapshots/test/snapshot/linkExternalFlash.js.test.cjs
@@ -5,14 +5,14 @@
  * Make sure to inspect the output below.  Do not ignore changes!
  */
 'use strict'
-exports[`test/snapshot/linkExternalFlash.js TAP linkExternalFlash (snapshot) > basic behavior 1`] = `
+exports[`test/snapshot/linkExternalFlash.js > TAP > linkExternalFlash (snapshot) > basic behavior 1`] = `
 <span class="nowrap"><a href="https://homestuck.com/story/4109/" class="nowrap">homestuck.com</a> (page 4109)</span>
 <span class="nowrap"><a href="https://youtu.be/FDt-SLyEcjI" class="nowrap">YouTube</a> (on any device)</span>
 <span class="nowrap"><a href="https://www.bgreco.net/hsflash/006009.html" class="nowrap">www.bgreco.net</a> (HQ Audio)</span>
 <span class="nowrap"><a href="https://www.newgrounds.com/portal/view/582345" class="nowrap">Newgrounds</a></span>
 `
 
-exports[`test/snapshot/linkExternalFlash.js TAP linkExternalFlash (snapshot) > secret page 1`] = `
+exports[`test/snapshot/linkExternalFlash.js > TAP > linkExternalFlash (snapshot) > secret page 1`] = `
 <span class="nowrap"><a href="https://homestuck.com/story/pony/" class="nowrap">homestuck.com</a> (secret page)</span>
 <span class="nowrap"><a href="https://youtu.be/USB1pj6hAjU" class="nowrap">YouTube</a> (on any device)</span>
 `
diff --git a/tap-snapshots/test/snapshot/linkTemplate.js.test.cjs b/tap-snapshots/test/snapshot/linkTemplate.js.test.cjs
index 45a9e61..0d9ef77 100644
--- a/tap-snapshots/test/snapshot/linkTemplate.js.test.cjs
+++ b/tap-snapshots/test/snapshot/linkTemplate.js.test.cjs
@@ -5,15 +5,15 @@
  * Make sure to inspect the output below.  Do not ignore changes!
  */
 'use strict'
-exports[`test/snapshot/linkTemplate.js TAP linkTemplate (snapshot) > fill many slots 1`] = `
+exports[`test/snapshot/linkTemplate.js > TAP > linkTemplate (snapshot) > fill many slots 1`] = `
 <a class="dog" id="cat1" href="https://hsmusic.wiki/media/cool%20file.pdf#fooey" style="--primary-color: #123456ff; --dim-color: #12345677">My Cool Link</a>
 `
 
-exports[`test/snapshot/linkTemplate.js TAP linkTemplate (snapshot) > fill path slot & provide appendIndexHTML 1`] = `
+exports[`test/snapshot/linkTemplate.js > TAP > linkTemplate (snapshot) > fill path slot & provide appendIndexHTML 1`] = `
 <a href="/c*lzone/myCoolPath/ham/pineapple/tomato/index.html">delish</a>
 `
 
-exports[`test/snapshot/linkTemplate.js TAP linkTemplate (snapshot) > link in content 1`] = `
+exports[`test/snapshot/linkTemplate.js > TAP > linkTemplate (snapshot) > link in content 1`] = `
 <a href="#the-more-ye-know">
     Oh geez oh heck
     There's a link in here!!
@@ -23,10 +23,10 @@ exports[`test/snapshot/linkTemplate.js TAP linkTemplate (snapshot) > link in con
 </a>
 `
 
-exports[`test/snapshot/linkTemplate.js TAP linkTemplate (snapshot) > missing content 1`] = `
+exports[`test/snapshot/linkTemplate.js > TAP > linkTemplate (snapshot) > missing content 1`] = `
 <a href="banana">(Missing link content)</a>
 `
 
-exports[`test/snapshot/linkTemplate.js TAP linkTemplate (snapshot) > special characters in path argument 1`] = `
+exports[`test/snapshot/linkTemplate.js > TAP > linkTemplate (snapshot) > special characters in path argument 1`] = `
 <a href="media/album-additional/homestuck-vol-1/Showtime%20(Piano%20Refrain)%20-%20%23xXxAwesomeSheetMusick%3FrxXx%23.pdf">Damn, that's some good sheet music</a>
 `
diff --git a/tap-snapshots/test/snapshot/linkThing.js.test.cjs b/tap-snapshots/test/snapshot/linkThing.js.test.cjs
new file mode 100644
index 0000000..5a5b251
--- /dev/null
+++ b/tap-snapshots/test/snapshot/linkThing.js.test.cjs
@@ -0,0 +1,39 @@
+/* IMPORTANT
+ * This snapshot file is auto-generated, but designed for humans.
+ * It should be checked into source control and tracked carefully.
+ * Re-generate by setting TAP_SNAPSHOT=1 and running tests.
+ * Make sure to inspect the output below.  Do not ignore changes!
+ */
+'use strict'
+exports[`test/snapshot/linkThing.js > TAP > linkThing (snapshot) > basic behavior 1`] = `
+<a href="track/foo/" style="--primary-color: #abcdef; --dim-color: #818181">Cool track!</a>
+`
+
+exports[`test/snapshot/linkThing.js > TAP > linkThing (snapshot) > color 1`] = `
+<a href="track/showtime-piano-refrain/">Showtime (Piano Refrain)</a>
+<a href="track/showtime-piano-refrain/" style="--primary-color: #38f43d; --dim-color: #389f33">Showtime (Piano Refrain)</a>
+<a href="track/showtime-piano-refrain/" style="--primary-color: #aaccff; --dim-color: #828282">Showtime (Piano Refrain)</a>
+`
+
+exports[`test/snapshot/linkThing.js > TAP > linkThing (snapshot) > nested links in content stripped 1`] = `
+<a href="foo/"><b>Oooo! Very spooky.</b></a>
+`
+
+exports[`test/snapshot/linkThing.js > TAP > linkThing (snapshot) > preferShortName 1`] = `
+<a href="tag/five-oceanfalls/">Five</a>
+`
+
+exports[`test/snapshot/linkThing.js > TAP > linkThing (snapshot) > tags in name escaped 1`] = `
+<a href="track/foo/">&lt;a href=&quot;SNOOPING&quot;&gt;AS USUAL&lt;/a&gt; I SEE</a>
+<a href="track/bar/">&lt;b&gt;boldface&lt;/b&gt;</a>
+<a href="album/exile/">&gt;Exile&lt;</a>
+<a href="track/heart/">&lt;3</a>
+`
+
+exports[`test/snapshot/linkThing.js > TAP > linkThing (snapshot) > tooltip & content 1`] = `
+<a href="album/beyond-canon/">Beyond Canon</a>
+<a href="album/beyond-canon/" title="Beyond Canon">Beyond Canon</a>
+<a href="album/beyond-canon/" title="Beyond Canon">Next</a>
+<a href="album/beyond-canon/" title="Apple">Banana</a>
+<a href="album/beyond-canon/">Banana</a>
+`
diff --git a/tap-snapshots/test/snapshot/transformContent.js.test.cjs b/tap-snapshots/test/snapshot/transformContent.js.test.cjs
index d144cf1..85ee740 100644
--- a/tap-snapshots/test/snapshot/transformContent.js.test.cjs
+++ b/tap-snapshots/test/snapshot/transformContent.js.test.cjs
@@ -5,12 +5,12 @@
  * Make sure to inspect the output below.  Do not ignore changes!
  */
 'use strict'
-exports[`test/snapshot/transformContent.js TAP transformContent (snapshot) > dates 1`] = `
+exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > dates 1`] = `
 <p><time datetime="Thu, 13 Apr 2023 00:00:00 GMT">4/12/2023</time> Yep!</p>
 <p>Very nice: <time datetime="Fri, 25 Oct 2413 03:00:00 GMT">10/25/2413</time></p>
 `
 
-exports[`test/snapshot/transformContent.js TAP transformContent (snapshot) > inline images 1`] = `
+exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > inline images 1`] = `
 <p><img src="snooping.png"> as USUAL...</p>
 <p>What do you know? <img src="cowabunga.png" width="24" height="32"></p>
 <p><a href="to-localized.album/cool-album" style="--primary-color: #123456; --dim-color: #000000">I'm on the left.</a><img src="im-on-the-right.jpg"></p>
@@ -20,12 +20,12 @@ exports[`test/snapshot/transformContent.js TAP transformContent (snapshot) > inl
 <p>And... all done! <img src="end-of-source.png"></p>
 `
 
-exports[`test/snapshot/transformContent.js TAP transformContent (snapshot) > links to a thing 1`] = `
+exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > links to a thing 1`] = `
 <p>This is <a href="to-localized.album/cool-album" style="--primary-color: #123456; --dim-color: #000000">my favorite album</a>.</p>
 <p>That&#39;s right, <a href="to-localized.album/cool-album" style="--primary-color: #123456; --dim-color: #000000">Cool Album</a>!</p>
 `
 
-exports[`test/snapshot/transformContent.js TAP transformContent (snapshot) > lyrics - basic line breaks 1`] = `
+exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > lyrics - basic line breaks 1`] = `
 <p>Hey, ho<br>
 And away we go<br>
 Truly, music</p>
@@ -33,7 +33,7 @@ Truly, music</p>
 (That&#39;s right)</p>
 `
 
-exports[`test/snapshot/transformContent.js TAP transformContent (snapshot) > lyrics - line breaks around tags 1`] = `
+exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > lyrics - line breaks around tags 1`] = `
 <p>The date be <time datetime="Tue, 13 Apr 2004 03:00:00 GMT">4/13/2004</time><br>
 I say, the date be <time datetime="Tue, 13 Apr 2004 03:00:00 GMT">4/13/2004</time><br>
 <time datetime="Tue, 13 Apr 2004 03:00:00 GMT">4/13/2004</time><br>
@@ -46,31 +46,31 @@ I say, the date be <time datetime="Tue, 13 Apr 2004 03:00:00 GMT">4/13/2004</tim
 <time datetime="Tue, 13 Apr 2004 03:00:00 GMT">4/13/2004</time>, and don&#39;t ye forget it</p>
 `
 
-exports[`test/snapshot/transformContent.js TAP transformContent (snapshot) > lyrics - repeated and edge line breaks 1`] = `
+exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > lyrics - repeated and edge line breaks 1`] = `
 <p>Well, you know<br>
 How it goes</p>
 <p>Yessiree</p>
 `
 
-exports[`test/snapshot/transformContent.js TAP transformContent (snapshot) > non-inline image #1 1`] = `
-<div class="content-image"><a class="box image-link" href="spark.png"><div class="image-container"><div class="image-inner-area"><img src="spark.large.jpg"></div></div></a></div>
+exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > non-inline image #1 1`] = `
+<div class="content-image">[mocked: image - slots: { src: 'spark.png', link: true, thumb: 'large' }]</div>
 `
 
-exports[`test/snapshot/transformContent.js TAP transformContent (snapshot) > non-inline image #2 1`] = `
+exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > non-inline image #2 1`] = `
 <p>Rad.</p>
-<div class="content-image"><a class="box image-link" href="spark.png"><div class="image-container"><div class="image-inner-area"><img src="spark.large.jpg"></div></div></a></div>
+<div class="content-image">[mocked: image - slots: { src: 'spark.png', link: true, thumb: 'large' }]</div>
 `
 
-exports[`test/snapshot/transformContent.js TAP transformContent (snapshot) > non-inline image #3 1`] = `
-<div class="content-image"><a class="box image-link" href="spark.png"><div class="image-container"><div class="image-inner-area"><img src="spark.large.jpg"></div></div></a></div>
+exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > non-inline image #3 1`] = `
+<div class="content-image">[mocked: image - slots: { src: 'spark.png', link: true, thumb: 'large' }]</div>
 <p>Baller.</p>
 `
 
-exports[`test/snapshot/transformContent.js TAP transformContent (snapshot) > super basic string 1`] = `
+exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > super basic string 1`] = `
 <p>Neat listing: Albums - by Date</p>
 `
 
-exports[`test/snapshot/transformContent.js TAP transformContent (snapshot) > two text paragraphs 1`] = `
+exports[`test/snapshot/transformContent.js > TAP > transformContent (snapshot) > two text paragraphs 1`] = `
 <p>Hello, world!</p>
 <p>Wow, this is very cool.</p>
 `
diff --git a/test/lib/content-function.js b/test/lib/content-function.js
index bb12be8..5cb499b 100644
--- a/test/lib/content-function.js
+++ b/test/lib/content-function.js
@@ -1,5 +1,6 @@
 import * as path from 'node:path';
 import {fileURLToPath} from 'node:url';
+import {inspect} from 'node:util';
 
 import chroma from 'chroma-js';
 
@@ -90,27 +91,92 @@ export function testContentFunctions(t, message, fn) {
       t.matchSnapshot(result, description);
     };
 
-    evaluate.stubTemplate = name => {
+    evaluate.stubTemplate = name =>
       // Creates a particularly permissable template, allowing any slot values
       // to be stored and just outputting the contents of those slots as-are.
+      _stubTemplate(name, false);
 
-      return new (class extends html.Template {
-        #slotValues = {};
+    evaluate.stubContentFunction = name =>
+      // Like stubTemplate, but instead of a template directly, returns
+      // an object describing a content function - suitable for passing
+      // into evaluate.mock.
+      _stubTemplate(name, true);
 
-        constructor() {
-          super({
-            content: () => `${name}: ${JSON.stringify(this.#slotValues)}`,
-          });
-        }
+    const _stubTemplate = (name, mockContentFunction) => {
+      const inspectNicely = (value, opts = {}) =>
+        inspect(value, {
+          ...opts,
+          colors: false,
+          sort: true,
+        });
 
-        setSlots(slotNamesToValues) {
-          Object.assign(this.#slotValues, slotNamesToValues);
-        }
+      const makeTemplate = formatContentFn =>
+        new (class extends html.Template {
+          #slotValues = {};
 
-        setSlot(slotName, slotValue) {
-          this.#slotValues[slotName] = slotValue;
-        }
-      });
+          constructor() {
+            super({
+              content: () => this.#getContent(formatContentFn),
+            });
+          }
+
+          setSlots(slotNamesToValues) {
+            Object.assign(this.#slotValues, slotNamesToValues);
+          }
+
+          setSlot(slotName, slotValue) {
+            this.#slotValues[slotName] = slotValue;
+          }
+
+          #getContent(formatContentFn) {
+            const toInspect =
+              Object.fromEntries(
+                Object.entries(this.#slotValues)
+                  .filter(([key, value]) => value !== null));
+
+            const inspected =
+              inspectNicely(toInspect, {
+                breakLength: Infinity,
+                compact: true,
+                depth: Infinity,
+              });
+
+            return formatContentFn(inspected); `${name}: ${inspected}`;
+          }
+        });
+
+      if (mockContentFunction) {
+        return {
+          data: (...args) => ({args}),
+          generate: (data) =>
+            makeTemplate(slots => {
+              const argsLines =
+                (empty(data.args)
+                  ? []
+                  : inspectNicely(data.args, {depth: Infinity})
+                      .split('\n'));
+
+              return (`[mocked: ${name}` +
+
+                (empty(data.args)
+                  ? ``
+               : argsLines.length === 1
+                  ? `\n args: ${argsLines[0]}`
+                  : `\n args: ${argsLines[0]}\n` +
+                    argsLines.slice(1).join('\n').replace(/^/gm, ' ')) +
+
+                (!empty(data.args)
+                  ? `\n `
+                  : ` - `) +
+
+                (slots
+                  ? `slots: ${slots}]`
+                  : `slots: none]`));
+            }),
+        };
+      } else {
+        return makeTemplate(slots => `${name}: ${slots}`);
+      }
     };
 
     evaluate.mock = (...opts) => {
diff --git a/test/lib/index.js b/test/lib/index.js
index b9cc82f..5fb5bf7 100644
--- a/test/lib/index.js
+++ b/test/lib/index.js
@@ -1,3 +1,6 @@
+Error.stackTraceLimit = Infinity;
+
 export * from './content-function.js';
 export * from './generic-mock.js';
+export * from './wiki-data.js';
 export * from './strict-match-error.js';
diff --git a/test/lib/wiki-data.js b/test/lib/wiki-data.js
new file mode 100644
index 0000000..c4083a5
--- /dev/null
+++ b/test/lib/wiki-data.js
@@ -0,0 +1,24 @@
+import {linkWikiDataArrays} from '#yaml';
+
+export function linkAndBindWikiData(wikiData) {
+  linkWikiDataArrays(wikiData);
+
+  return {
+    // Mutate to make the below functions aware of new data objects, or of
+    // reordering the existing ones. Don't mutate arrays such as trackData
+    // in-place; assign completely new arrays to this wikiData object instead.
+    wikiData,
+
+    // Use this after you've mutated wikiData to assign new data arrays.
+    // It'll automatically relink everything on wikiData so all the objects
+    // are caught up to date.
+    linkWikiDataArrays:
+      linkWikiDataArrays.bind(null, wikiData),
+
+    // Use this if you HAVEN'T mutated wikiData and just need to decache
+    // indirect dependencies on exposed properties of other data objects.
+    // See documentation on linkWikiDataArarys (in yaml.js) for more info.
+    XXX_decacheWikiData:
+      linkWikiDataArrays.bind(null, wikiData, {XXX_decacheWikiData: true}),
+  };
+}
diff --git a/test/snapshot/generateAlbumCoverArtwork.js b/test/snapshot/generateAlbumCoverArtwork.js
index 98632d3..b1c7885 100644
--- a/test/snapshot/generateAlbumCoverArtwork.js
+++ b/test/snapshot/generateAlbumCoverArtwork.js
@@ -1,12 +1,14 @@
 import t from 'tap';
+
+import contentFunction from '#content-function';
 import {testContentFunctions} from '#test-lib';
 
 testContentFunctions(t, 'generateAlbumCoverArtwork (snapshot)', async (t, evaluate) => {
-  await evaluate.load();
-
-  const extraDependencies = {
-    getSizeOfImageFile: () => 0,
-  };
+  await evaluate.load({
+    mock: {
+      image: evaluate.stubContentFunction('image'),
+    },
+  });
 
   const album = {
     directory: 'bee-forus-seatbelt-safebee',
@@ -23,13 +25,11 @@ testContentFunctions(t, 'generateAlbumCoverArtwork (snapshot)', async (t, evalua
     name: 'generateAlbumCoverArtwork',
     args: [album],
     slots: {mode: 'primary'},
-    extraDependencies,
   });
 
   evaluate.snapshot('display: thumbnail', {
     name: 'generateAlbumCoverArtwork',
     args: [album],
     slots: {mode: 'thumbnail'},
-    extraDependencies,
   });
 });
diff --git a/test/snapshot/generateAlbumSecondaryNav.js b/test/snapshot/generateAlbumSecondaryNav.js
index a5cb2e9..709b062 100644
--- a/test/snapshot/generateAlbumSecondaryNav.js
+++ b/test/snapshot/generateAlbumSecondaryNav.js
@@ -6,8 +6,8 @@ testContentFunctions(t, 'generateAlbumSecondaryNav (snapshot)', async (t, evalua
 
   let album, group1, group2;
 
-  group1 = {name: 'VCG', directory: 'vcg'};
-  group2 = {name: 'Bepis', directory: 'bepis'};
+  group1 = {name: 'VCG', directory: 'vcg', color: '#abcdef'};
+  group2 = {name: 'Bepis', directory: 'bepis', color: '#123456'};
 
   album = {
     date: new Date('2010-04-13'),
diff --git a/test/snapshot/generateCoverArtwork.js b/test/snapshot/generateCoverArtwork.js
index 21c9145..e35dd8d 100644
--- a/test/snapshot/generateCoverArtwork.js
+++ b/test/snapshot/generateCoverArtwork.js
@@ -2,11 +2,11 @@ import t from 'tap';
 import {testContentFunctions} from '#test-lib';
 
 testContentFunctions(t, 'generateCoverArtwork (snapshot)', async (t, evaluate) => {
-  await evaluate.load();
-
-  const extraDependencies = {
-    getSizeOfImageFile: () => 0,
-  };
+  await evaluate.load({
+    mock: {
+      image: evaluate.stubContentFunction('image', {mock: true}),
+    },
+  });
 
   const artTags = [
     {name: 'Damara', directory: 'damara', isContentWarning: false},
@@ -21,13 +21,11 @@ testContentFunctions(t, 'generateCoverArtwork (snapshot)', async (t, evaluate) =
     name: 'generateCoverArtwork',
     args: [artTags],
     slots: {path, mode: 'primary'},
-    extraDependencies,
   });
 
   evaluate.snapshot('display: thumbnail', {
     name: 'generateCoverArtwork',
     args: [artTags],
     slots: {path, mode: 'thumbnail'},
-    extraDependencies,
   });
 });
diff --git a/test/snapshot/generateTrackCoverArtwork.js b/test/snapshot/generateTrackCoverArtwork.js
index 9e15470..03a181e 100644
--- a/test/snapshot/generateTrackCoverArtwork.js
+++ b/test/snapshot/generateTrackCoverArtwork.js
@@ -2,11 +2,11 @@ import t from 'tap';
 import {testContentFunctions} from '#test-lib';
 
 testContentFunctions(t, 'generateTrackCoverArtwork (snapshot)', async (t, evaluate) => {
-  await evaluate.load();
-
-  const extraDependencies = {
-    getSizeOfImageFile: () => 0,
-  };
+  await evaluate.load({
+    mock: {
+      image: evaluate.stubContentFunction('image'),
+    },
+  });
 
   const album = {
     directory: 'bee-forus-seatbelt-safebee',
@@ -37,27 +37,23 @@ testContentFunctions(t, 'generateTrackCoverArtwork (snapshot)', async (t, evalua
     name: 'generateTrackCoverArtwork',
     args: [track1],
     slots: {mode: 'primary'},
-    extraDependencies,
   });
 
   evaluate.snapshot('display: thumbnail - unique art', {
     name: 'generateTrackCoverArtwork',
     args: [track1],
     slots: {mode: 'thumbnail'},
-    extraDependencies,
   });
 
   evaluate.snapshot('display: primary - no unique art', {
     name: 'generateTrackCoverArtwork',
     args: [track2],
     slots: {mode: 'primary'},
-    extraDependencies,
   });
 
   evaluate.snapshot('display: thumbnail - no unique art', {
     name: 'generateTrackCoverArtwork',
     args: [track2],
     slots: {mode: 'thumbnail'},
-    extraDependencies,
   });
 });
diff --git a/test/snapshot/image.js b/test/snapshot/image.js
index 6bec1cc..2a1e980 100644
--- a/test/snapshot/image.js
+++ b/test/snapshot/image.js
@@ -4,11 +4,18 @@ import {testContentFunctions} from '#test-lib';
 testContentFunctions(t, 'image (snapshot)', async (t, evaluate) => {
   await evaluate.load();
 
-  const quickSnapshot = (message, opts) =>
+  const quickSnapshot = (message, {extraDependencies, ...opts}) =>
     evaluate.snapshot(message, {
       name: 'image',
       extraDependencies: {
-        getSizeOfImageFile: () => 0,
+        checkIfImagePathHasCachedThumbnails: path => !path.endsWith('.gif'),
+        getSizeOfImagePath: () => 0,
+        getDimensionsOfImagePath: () => [600, 600],
+        getThumbnailEqualOrSmaller: () => 'medium',
+        getThumbnailsAvailableForDimensions: () =>
+          [['large', 800], ['medium', 400], ['small', 250]],
+        missingImagePaths: ['album-art/missing/cover.png'],
+        ...extraDependencies,
       },
       ...opts,
     });
@@ -79,7 +86,7 @@ testContentFunctions(t, 'image (snapshot)', async (t, evaluate) => {
 
   quickSnapshot('link with file size', {
     extraDependencies: {
-      getSizeOfImageFile: () => 10 ** 6,
+      getSizeOfImagePath: () => 10 ** 6,
     },
     slots: {
       path: ['media.albumCover', 'pingas', 'png'],
@@ -98,4 +105,44 @@ testContentFunctions(t, 'image (snapshot)', async (t, evaluate) => {
       path: ['media.albumCover', 'beyond-canon', 'png'],
     },
   });
+
+  evaluate.snapshot('thumbnail details', {
+    name: 'image',
+    extraDependencies: {
+      checkIfImagePathHasCachedThumbnails: () => true,
+      getSizeOfImagePath: () => 0,
+      getDimensionsOfImagePath: () => [900, 1200],
+      getThumbnailsAvailableForDimensions: () =>
+        [['voluminous', 1200], ['middling', 900], ['petite', 20]],
+      getThumbnailEqualOrSmaller: () => 'voluminous',
+      missingImagePaths: [],
+    },
+    slots: {
+      thumb: 'gargantuan',
+      path: ['media.albumCover', 'beyond-canon', 'png'],
+    },
+  });
+
+  quickSnapshot('thumb requested but source is gif', {
+    slots: {
+      thumb: 'medium',
+      path: ['media.flashArt', '5426', 'gif'],
+    },
+  });
+
+  quickSnapshot('missing image path', {
+    slots: {
+      thumb: 'medium',
+      path: ['media.albumCover', 'missing', 'png'],
+      link: true,
+    },
+  });
+
+  quickSnapshot('missing image path w/ missingSourceContent', {
+    slots: {
+      thumb: 'medium',
+      path: ['media.albumCover', 'missing', 'png'],
+      missingSourceContent: `Cover's missing, whoops`,
+    },
+  });
 });
diff --git a/test/snapshot/linkThing.js b/test/snapshot/linkThing.js
new file mode 100644
index 0000000..195d8c0
--- /dev/null
+++ b/test/snapshot/linkThing.js
@@ -0,0 +1,87 @@
+import t from 'tap';
+import * as html from '#html';
+import {testContentFunctions} from '#test-lib';
+
+testContentFunctions(t, 'linkThing (snapshot)', async (t, evaluate) => {
+  await evaluate.load();
+
+  const quickSnapshot = (message, oneOrMultiple) =>
+    evaluate.snapshot(message,
+      (Array.isArray(oneOrMultiple)
+        ? {name: 'linkThing', multiple: oneOrMultiple}
+        : {name: 'linkThing', ...oneOrMultiple}));
+
+  quickSnapshot('basic behavior', {
+    args: ['localized.track', {
+      directory: 'foo',
+      color: '#abcdef',
+      name: `Cool track!`,
+    }],
+  });
+
+  quickSnapshot('preferShortName', {
+    args: ['localized.tag', {
+      directory: 'five-oceanfalls',
+      name: 'Five (Oceanfalls)',
+      nameShort: 'Five',
+    }],
+    slots: {preferShortName: true},
+  });
+
+  quickSnapshot('tooltip & content', {
+    args: ['localized.album', {
+      directory: 'beyond-canon',
+      name: 'Beyond Canon',
+    }],
+    multiple: [
+      {slots: {tooltip: false}},
+      {slots: {tooltip: true}},
+      {slots: {tooltip: true, content: 'Next'}},
+      {slots: {tooltip: 'Apple', content: 'Banana'}},
+      {slots: {content: 'Banana'}},
+    ],
+  });
+
+  quickSnapshot('color', {
+    args: ['localized.track', {
+      directory: 'showtime-piano-refrain',
+      name: 'Showtime (Piano Refrain)',
+      color: '#38f43d',
+    }],
+    multiple: [
+      {slots: {color: false}},
+      {slots: {color: true}},
+      {slots: {color: '#aaccff'}},
+    ],
+  });
+
+  quickSnapshot('tags in name escaped', [
+    {args: ['localized.track', {
+      directory: 'foo',
+      name: `<a href="SNOOPING">AS USUAL</a> I SEE`,
+    }]},
+    {args: ['localized.track', {
+      directory: 'bar',
+      name: `<b>boldface</b>`,
+    }]},
+    {args: ['localized.album', {
+      directory: 'exile',
+      name: '>Exile<',
+    }]},
+    {args: ['localized.track', {
+      directory: 'heart',
+      name: '<3',
+    }]},
+  ]);
+
+  quickSnapshot('nested links in content stripped', {
+    args: ['localized.staticPage', {directory: 'foo', name: 'Foo'}],
+    slots: {
+      content:
+        html.tag('b', {[html.joinChildren]: ''}, [
+          html.tag('a', {href: 'bar'}, `Oooo!`),
+          ` Very spooky.`,
+        ]),
+    },
+  });
+});
diff --git a/test/snapshot/transformContent.js b/test/snapshot/transformContent.js
index 2595285..b05beac 100644
--- a/test/snapshot/transformContent.js
+++ b/test/snapshot/transformContent.js
@@ -2,7 +2,11 @@ import t from 'tap';
 import {testContentFunctions} from '#test-lib';
 
 testContentFunctions(t, 'transformContent (snapshot)', async (t, evaluate) => {
-  await evaluate.load();
+  await evaluate.load({
+    mock: {
+      image: evaluate.stubContentFunction('image'),
+    },
+  });
 
   const extraDependencies = {
     wikiData: {
@@ -11,8 +15,6 @@ testContentFunctions(t, 'transformContent (snapshot)', async (t, evaluate) => {
       ],
     },
 
-    getSizeOfImageFile: () => 0,
-
     to: (key, ...args) => `to-${key}/${args.join('/')}`,
   };
 
@@ -50,15 +52,18 @@ testContentFunctions(t, 'transformContent (snapshot)', async (t, evaluate) => {
 
   quickSnapshot(
     'non-inline image #2',
-      `Rad.\n<img src="spark.png">`);
+      `Rad.\n` +
+      `<img src="spark.png">`);
 
   quickSnapshot(
     'non-inline image #3',
-      `<img src="spark.png">\nBaller.`);
+      `<img src="spark.png">\n` +
+      `Baller.`);
 
   quickSnapshot(
     'dates',
-      `[[date:2023-04-13]] Yep!\nVery nice: [[date:25 October 2413]]`);
+      `[[date:2023-04-13]] Yep!\n` +
+      `Very nice: [[date:25 October 2413]]`);
 
   quickSnapshot(
     'super basic string',
diff --git a/test/unit/data/things/cacheable-object.js b/test/unit/data/cacheable-object.js
index 2e82af0..57e562d 100644
--- a/test/unit/data/things/cacheable-object.js
+++ b/test/unit/data/cacheable-object.js
@@ -195,13 +195,10 @@ t.test(`CacheableObject validate on update`, t => {
   obj.directory = 'megalovania';
   t.equal(obj.directory, 'megalovania');
 
-  try {
-    obj.directory = 25;
-  } catch (err) {
-    thrownError = err;
-  }
+  t.throws(
+    () => { obj.directory = 25; },
+    {cause: mockError});
 
-  t.equal(thrownError, mockError);
   t.equal(obj.directory, 'megalovania');
 
   const date = new Date(`25 December 2009`);
@@ -209,13 +206,10 @@ t.test(`CacheableObject validate on update`, t => {
   obj.date = date;
   t.equal(obj.date, date);
 
-  try {
-    obj.date = `TWELFTH PERIGEE'S EVE`;
-  } catch (err) {
-    thrownError = err;
-  }
+  t.throws(
+    () => { obj.date = `TWELFTH PERIGEE'S EVE`; },
+    {cause: TypeError});
 
-  t.equal(thrownError?.constructor, TypeError);
   t.equal(obj.date, date);
 });
 
@@ -244,8 +238,8 @@ t.test(`CacheableObject default property throws if invalid`, t => {
 
   let thrownError;
 
-  try {
-    newCacheableObject({
+  t.throws(
+    () => newCacheableObject({
       string: {
         flags: {
           update: true
@@ -261,10 +255,6 @@ t.test(`CacheableObject default property throws if invalid`, t => {
           }
         }
       }
-    });
-  } catch (err) {
-    thrownError = err;
-  }
-
-  t.equal(thrownError, mockError);
+    }),
+    {cause: mockError});
 });
diff --git a/test/unit/data/composite/control-flow/exposeConstant.js b/test/unit/data/composite/control-flow/exposeConstant.js
new file mode 100644
index 0000000..0c75894
--- /dev/null
+++ b/test/unit/data/composite/control-flow/exposeConstant.js
@@ -0,0 +1,42 @@
+import t from 'tap';
+
+import {compositeFrom, continuationSymbol, input} from '#composite';
+import {exposeConstant} from '#composite/control-flow';
+
+t.test(`exposeConstant: basic behavior`, t => {
+  t.plan(2);
+
+  const composite1 = compositeFrom({
+    compose: false,
+
+    steps: [
+      exposeConstant({
+        value: input.value('foo'),
+      }),
+    ],
+  });
+
+  t.match(composite1, {
+    expose: {
+      dependencies: [],
+    },
+  });
+
+  t.equal(composite1.expose.compute(), 'foo');
+});
+
+t.test(`exposeConstant: validate inputs`, t => {
+  t.plan(2);
+
+  t.throws(
+    () => exposeConstant({}),
+    {message: `Errors in input options passed to exposeConstant`, errors: [
+      {message: `Required these inputs: value`},
+    ]});
+
+  t.throws(
+    () => exposeConstant({value: 'some dependency'}),
+    {message: `Errors in input options passed to exposeConstant`, errors: [
+      {message: `value: Expected input.value() call, got dependency name`},
+    ]});
+});
diff --git a/test/unit/data/composite/control-flow/exposeDependency.js b/test/unit/data/composite/control-flow/exposeDependency.js
new file mode 100644
index 0000000..8f6bfd0
--- /dev/null
+++ b/test/unit/data/composite/control-flow/exposeDependency.js
@@ -0,0 +1,64 @@
+import t from 'tap';
+
+import {compositeFrom, continuationSymbol, input} from '#composite';
+import {exposeDependency} from '#composite/control-flow';
+
+t.test(`exposeDependency: basic behavior`, t => {
+  t.plan(4);
+
+  const composite1 = compositeFrom({
+    compose: false,
+
+    steps: [
+      exposeDependency({dependency: 'foo'}),
+    ],
+  });
+
+  t.match(composite1, {
+    expose: {
+      dependencies: ['foo'],
+    },
+  });
+
+  t.equal(composite1.expose.compute({foo: 'bar'}), 'bar');
+
+  const composite2 = compositeFrom({
+    compose: false,
+
+    steps: [
+      {
+        dependencies: ['foo'],
+        compute: (continuation, {foo}) =>
+          continuation({'#bar': foo.toUpperCase()}),
+      },
+
+      exposeDependency({dependency: '#bar'}),
+    ],
+  });
+
+  t.match(composite2, {
+    expose: {
+      dependencies: ['foo'],
+    },
+  });
+
+  t.equal(composite2.expose.compute({foo: 'bar'}), 'BAR');
+});
+
+t.test(`exposeDependency: validate inputs`, t => {
+  t.plan(2);
+
+  t.throws(
+    () => exposeDependency({}),
+    {message: `Errors in input options passed to exposeDependency`, errors: [
+      {message: `Required these inputs: dependency`},
+    ]});
+
+  t.throws(
+    () => exposeDependency({
+      dependency: input.value('some static value'),
+    }),
+    {message: `Errors in input options passed to exposeDependency`, errors: [
+      {message: `dependency: Expected dependency name, got input.value() call`},
+    ]});
+});
diff --git a/test/unit/data/composite/control-flow/withResultOfAvailabilityCheck.js b/test/unit/data/composite/control-flow/withResultOfAvailabilityCheck.js
new file mode 100644
index 0000000..2bcabb4
--- /dev/null
+++ b/test/unit/data/composite/control-flow/withResultOfAvailabilityCheck.js
@@ -0,0 +1,195 @@
+import t from 'tap';
+
+import {compositeFrom, continuationSymbol, input} from '#composite';
+import {withResultOfAvailabilityCheck} from '#composite/control-flow';
+
+const composite = compositeFrom({
+  compose: false,
+
+  steps: [
+    withResultOfAvailabilityCheck({
+      from: 'from',
+      mode: 'mode',
+    }).outputs({
+      ['#availability']: '#result',
+    }),
+
+    {
+      dependencies: ['#result'],
+      compute: ({'#result': result}) => result,
+    },
+  ],
+});
+
+t.test(`withResultOfAvailabilityCheck: basic behavior`, t => {
+  t.plan(1);
+
+  t.match(composite, {
+    expose: {
+      dependencies: ['from', 'mode'],
+    },
+  });
+});
+
+const quickCompare = (t, expect, {from, mode}) =>
+  t.equal(composite.expose.compute({from, mode}), expect);
+
+const quickThrows = (t, {from, mode}) =>
+  t.throws(() => composite.expose.compute({from, mode}));
+
+t.test(`withResultOfAvailabilityCheck: mode = null`, t => {
+  t.plan(11);
+
+  quickCompare(t, true,  {mode: 'null', from: 'truthy string'});
+  quickCompare(t, true,  {mode: 'null', from: 123});
+  quickCompare(t, true,  {mode: 'null', from: true});
+
+  quickCompare(t, true,  {mode: 'null', from: ''});
+  quickCompare(t, true,  {mode: 'null', from: 0});
+  quickCompare(t, true,  {mode: 'null', from: -1});
+  quickCompare(t, true,  {mode: 'null', from: false});
+
+  quickCompare(t, true,  {mode: 'null', from: [1, 2, 3]});
+  quickCompare(t, true,  {mode: 'null', from: []});
+
+  quickCompare(t, false, {mode: 'null', from: null});
+  quickCompare(t, false, {mode: 'null', from: undefined});
+});
+
+t.test(`withResultOfAvailabilityCheck: mode = empty`, t => {
+  t.plan(11);
+
+  quickThrows(t, {mode: 'empty', from: 'truthy string'});
+  quickThrows(t, {mode: 'empty', from: 123});
+  quickThrows(t, {mode: 'empty', from: true});
+
+  quickThrows(t, {mode: 'empty', from: ''});
+  quickThrows(t, {mode: 'empty', from: 0});
+  quickThrows(t, {mode: 'empty', from: -1});
+  quickThrows(t, {mode: 'empty', from: false});
+
+  quickCompare(t, true,  {mode: 'empty', from: [1, 2, 3]});
+  quickCompare(t, false, {mode: 'empty', from: []});
+
+  quickCompare(t, false, {mode: 'empty', from: null});
+  quickCompare(t, false, {mode: 'empty', from: undefined});
+});
+
+t.test(`withResultOfAvailabilityCheck: mode = falsy`, t => {
+  t.plan(11);
+
+  quickCompare(t, true,  {mode: 'falsy', from: 'truthy string'});
+  quickCompare(t, true,  {mode: 'falsy', from: 123});
+  quickCompare(t, true,  {mode: 'falsy', from: true});
+
+  quickCompare(t, false, {mode: 'falsy', from: ''});
+  quickCompare(t, false, {mode: 'falsy', from: 0});
+  quickCompare(t, true,  {mode: 'falsy', from: -1});
+  quickCompare(t, false, {mode: 'falsy', from: false});
+
+  quickCompare(t, true,  {mode: 'falsy', from: [1, 2, 3]});
+  quickCompare(t, false, {mode: 'falsy', from: []});
+
+  quickCompare(t, false, {mode: 'falsy', from: null});
+  quickCompare(t, false, {mode: 'falsy', from: undefined});
+});
+
+t.test(`withResultOfAvailabilityCheck: mode = index`, t => {
+  t.plan(11);
+
+  quickCompare(t, false, {mode: 'index', from: 'truthy string'});
+  quickCompare(t, true,  {mode: 'index', from: 123});
+  quickCompare(t, false, {mode: 'index', from: true});
+
+  quickCompare(t, false, {mode: 'index', from: ''});
+  quickCompare(t, true,  {mode: 'index', from: 0});
+  quickCompare(t, false, {mode: 'index', from: -1});
+  quickCompare(t, false, {mode: 'index', from: false});
+
+  quickCompare(t, false, {mode: 'index', from: [1, 2, 3]});
+  quickCompare(t, false, {mode: 'index', from: []});
+
+  quickCompare(t, false, {mode: 'index', from: null});
+  quickCompare(t, false, {mode: 'index', from: undefined});
+});
+
+t.test(`withResultOfAvailabilityCheck: default mode`, t => {
+  t.plan(1);
+
+  const template = withResultOfAvailabilityCheck({
+    from: 'foo',
+  });
+
+  t.match(template.toDescription(), {
+    inputMapping: {
+      from: input.dependency('foo'),
+      mode: input.value('null'),
+    },
+  });
+});
+
+t.test(`withResultOfAvailabilityCheck: validate static inputs`, t => {
+  t.plan(5);
+
+  t.throws(
+    () => withResultOfAvailabilityCheck({}),
+    {message: `Errors in input options passed to withResultOfAvailabilityCheck`, errors: [
+      {message: `Required these inputs: from`},
+    ]});
+
+  t.doesNotThrow(() =>
+    withResultOfAvailabilityCheck({
+      from: 'dependency1',
+      mode: 'dependency2',
+    }));
+
+  t.doesNotThrow(() =>
+    withResultOfAvailabilityCheck({
+      from: input.value('some static value'),
+      mode: input.value('null'),
+    }));
+
+  t.throws(
+    () => withResultOfAvailabilityCheck({
+      from: 'foo',
+      mode: input.value('invalid'),
+    }),
+    {message: `Errors in input options passed to withResultOfAvailabilityCheck`, errors: [
+      {message: `mode: Expected one of null empty falsy index, got invalid`},
+    ]});
+
+  t.throws(() =>
+    withResultOfAvailabilityCheck({
+      from: input.value(null),
+      mode: input.value(null),
+    }),
+    {message: `Errors in input options passed to withResultOfAvailabilityCheck`, errors: [
+      {message: `mode: Expected a value, got null`},
+    ]});
+});
+
+t.test(`withResultOfAvailabilityCheck: validate dynamic inputs`, t => {
+  t.plan(2);
+
+  t.throws(
+    () => composite.expose.compute({
+      from: 'apple',
+      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`},
+        ]}}});
+
+  t.throws(
+    () => composite.expose.compute({
+      from: null,
+      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`},
+        ]}}});
+});
diff --git a/test/unit/data/composite/data/withPropertiesFromObject.js b/test/unit/data/composite/data/withPropertiesFromObject.js
new file mode 100644
index 0000000..ead1b9b
--- /dev/null
+++ b/test/unit/data/composite/data/withPropertiesFromObject.js
@@ -0,0 +1,248 @@
+import t from 'tap';
+
+import {compositeFrom, input} from '#composite';
+import {exposeDependency} from '#composite/control-flow';
+import {withPropertiesFromObject} from '#composite/data';
+
+const composite = compositeFrom({
+  compose: false,
+
+  steps: [
+    withPropertiesFromObject({
+      object: 'object',
+      properties: 'properties',
+    }),
+
+    exposeDependency({dependency: '#object'}),
+  ],
+});
+
+t.test(`withPropertiesFromObject: basic behavior`, t => {
+  t.plan(4);
+
+  t.match(composite, {
+    expose: {
+      dependencies: ['object', 'properties'],
+    },
+  });
+
+  t.same(
+    composite.expose.compute({
+      object: {foo: 'bar', bim: 'BOOM', bam: 'baz'},
+      properties: ['foo', 'bim'],
+    }),
+    {foo: 'bar', bim: 'BOOM'});
+
+  t.same(
+    composite.expose.compute({
+      object: {value1: 'uwah', value2: 'arah'},
+      properties: ['value1', 'value3'],
+    }),
+    {value1: 'uwah', value3: null});
+
+  t.same(
+    composite.expose.compute({
+      object: null,
+      properties: ['ohMe', 'ohMy', 'ohDear'],
+    }),
+    {ohMe: null, ohMy: null, ohDear: null});
+});
+
+t.test(`withPropertiesFromObject: output shapes & values`, t => {
+  t.plan(2 * 2 * 3 ** 2);
+
+  const dependencies = {
+    ['object_dependency']:
+      {foo: 'apple', bar: 'banana', baz: 'orange'},
+    [input('object_neither')]:
+      {foo: 'koala', bar: 'okapi', baz: 'mongoose'},
+    ['properties_dependency']:
+      ['foo', 'bar', 'missing1'],
+    [input('properties_neither')]:
+      ['foo', 'baz', 'missing3'],
+  };
+
+  const mapLevel1 = [
+    [input.value('prefix_value'), [
+      ['object_dependency', [
+        ['properties_dependency', {
+          '#object': {foo: 'apple', bar: 'banana', missing1: null},
+        }],
+        [input.value(['bar', 'baz', 'missing2']), {
+          '#prefix_value.bar': 'banana',
+          '#prefix_value.baz': 'orange',
+          '#prefix_value.missing2': null,
+        }],
+        [input('properties_neither'), {
+          '#object': {foo: 'apple', baz: 'orange', missing3: null},
+        }]]],
+
+      [input.value({foo: 'ouh', bar: 'rah', baz: 'nyu'}), [
+        ['properties_dependency', {
+          '#object': {foo: 'ouh', bar: 'rah', missing1: null},
+        }],
+        [input.value(['bar', 'baz', 'missing2']), {
+          '#prefix_value.bar': 'rah',
+          '#prefix_value.baz': 'nyu',
+          '#prefix_value.missing2': null,
+        }],
+        [input('properties_neither'), {
+          '#object': {foo: 'ouh', baz: 'nyu', missing3: null},
+        }]]],
+
+      [input('object_neither'), [
+        ['properties_dependency', {
+          '#object': {foo: 'koala', bar: 'okapi', missing1: null},
+        }],
+        [input.value(['bar', 'baz', 'missing2']), {
+          '#prefix_value.bar': 'okapi',
+          '#prefix_value.baz': 'mongoose',
+          '#prefix_value.missing2': null,
+        }],
+        [input('properties_neither'), {
+          '#object': {foo: 'koala', baz: 'mongoose', missing3: null},
+        }]]]]],
+
+    [input.value(null), [
+      ['object_dependency', [
+        ['properties_dependency', {
+          '#object': {foo: 'apple', bar: 'banana', missing1: null},
+        }],
+        [input.value(['bar', 'baz', 'missing2']), {
+          '#object_dependency.bar': 'banana',
+          '#object_dependency.baz': 'orange',
+          '#object_dependency.missing2': null,
+        }],
+        [input('properties_neither'), {
+          '#object': {foo: 'apple', baz: 'orange', missing3: null},
+        }]]],
+
+      [input.value({foo: 'ouh', bar: 'rah', baz: 'nyu'}), [
+        ['properties_dependency', {
+          '#object': {foo: 'ouh', bar: 'rah', missing1: null},
+        }],
+        [input.value(['bar', 'baz', 'missing2']), {
+          '#object.bar': 'rah',
+          '#object.baz': 'nyu',
+          '#object.missing2': null,
+        }],
+        [input('properties_neither'), {
+          '#object': {foo: 'ouh', baz: 'nyu', missing3: null},
+        }]]],
+
+      [input('object_neither'), [
+        ['properties_dependency', {
+          '#object': {foo: 'koala', bar: 'okapi', missing1: null},
+        }],
+        [input.value(['bar', 'baz', 'missing2']), {
+          '#object.bar': 'okapi',
+          '#object.baz': 'mongoose',
+          '#object.missing2': null,
+        }],
+        [input('properties_neither'), {
+          '#object': {foo: 'koala', baz: 'mongoose', missing3: null},
+        }]]]]],
+  ];
+
+  for (const [prefixInput, mapLevel2] of mapLevel1) {
+    for (const [objectInput, mapLevel3] of mapLevel2) {
+      for (const [propertiesInput, outputDict] of mapLevel3) {
+        const step = withPropertiesFromObject({
+          prefix: prefixInput,
+          object: objectInput,
+          properties: propertiesInput,
+        });
+
+        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);
+  }
+});
+
+t.test(`withPropertiesFromObject: validate static inputs`, t => {
+  t.plan(3);
+
+  t.throws(
+    () => withPropertiesFromObject({}),
+    {message: `Errors in input options passed to withPropertiesFromObject`, errors: [
+      {message: `Required these inputs: object, properties`},
+    ]});
+
+  t.throws(
+    () => withPropertiesFromObject({
+      object: input.value('intriguing'),
+      properties: input.value('very'),
+      prefix: input.value({yes: 'yup'}),
+    }),
+    {message: `Errors in input options passed to withPropertiesFromObject`, errors: [
+      {message: `object: Expected an object, got string`},
+      {message: `properties: Expected an array, got string`},
+      {message: `prefix: Expected a string, got object`},
+    ]});
+
+  t.throws(
+    () => withPropertiesFromObject({
+      object: input.value([['abc', 1], ['def', 2], [123, 3]]),
+      properties: input.value(['abc', 'def', 123]),
+    }),
+    {message: `Errors in input options passed to withPropertiesFromObject`, errors: [
+      {message: `object: Expected an object, got array`},
+      {message: `properties: Errors validating array items`, errors: [
+        {
+          [Symbol.for('hsmusic.decorate.indexInSourceArray')]: 2,
+          message: /Expected a string, got number/,
+        },
+      ]},
+    ]});
+});
+
+t.test(`withPropertiesFromObject: validate dynamic inputs`, t => {
+  t.plan(2);
+
+  t.throws(
+    () => composite.expose.compute({
+      object: 'intriguing',
+      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`},
+        ]}}});
+
+  t.throws(
+    () => composite.expose.compute({
+      object: [['abc', 1], ['def', 2], [123, 3]],
+      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.decorate.indexInSourceArray')]: 2,
+              message: /Expected a string, got number/,
+            },
+          ]},
+        ]}}});
+});
diff --git a/test/unit/data/composite/data/withPropertyFromObject.js b/test/unit/data/composite/data/withPropertyFromObject.js
new file mode 100644
index 0000000..6a772c3
--- /dev/null
+++ b/test/unit/data/composite/data/withPropertyFromObject.js
@@ -0,0 +1,122 @@
+import t from 'tap';
+
+import {compositeFrom, input} from '#composite';
+import {exposeDependency} from '#composite/control-flow';
+import {withPropertyFromObject} from '#composite/data';
+
+t.test(`withPropertyFromObject: basic behavior`, t => {
+  t.plan(4);
+
+  const composite = compositeFrom({
+    compose: false,
+
+    steps: [
+      withPropertyFromObject({
+        object: 'object',
+        property: 'property',
+      }),
+
+      exposeDependency({dependency: '#value'}),
+    ],
+  });
+
+  t.match(composite, {
+    expose: {
+      dependencies: ['object', 'property'],
+    },
+  });
+
+  t.equal(composite.expose.compute({
+    object: {foo: 'bar', bim: 'BOOM'},
+    property: 'bim',
+  }), 'BOOM');
+
+  t.equal(composite.expose.compute({
+    object: {value1: 'uwah'},
+    property: 'value2',
+  }), null);
+
+  t.equal(composite.expose.compute({
+    object: null,
+    property: 'oml where did me object go',
+  }), null);
+});
+
+t.test(`withPropertyFromObject: output shapes & values`, t => {
+  t.plan(2 * 3 ** 2);
+
+  const dependencies = {
+    ['object_dependency']:
+      {foo: 'apple', bar: 'banana', baz: 'orange'},
+    [input('object_neither')]:
+      {foo: 'koala', bar: 'okapi', baz: 'mongoose'},
+    ['property_dependency']:
+      'foo',
+    [input('property_neither')]:
+      'baz',
+  };
+
+  const mapLevel1 = [
+    ['object_dependency', [
+      ['property_dependency', {
+        '#value': 'apple',
+      }],
+      [input.value('bar'), {
+        '#object_dependency.bar': 'banana',
+      }],
+      [input('property_neither'), {
+        '#value': 'orange',
+      }]]],
+
+    [input.value({foo: 'ouh', bar: 'rah', baz: 'nyu'}), [
+      ['property_dependency', {
+        '#value': 'ouh',
+      }],
+      [input.value('bar'), {
+        '#value': 'rah',
+      }],
+      [input('property_neither'), {
+        '#value': 'nyu',
+      }]]],
+
+    [input('object_neither'), [
+      ['property_dependency', {
+        '#value': 'koala',
+      }],
+      [input.value('bar'), {
+        '#value': 'okapi',
+      }],
+      [input('property_neither'), {
+        '#value': 'mongoose',
+      }]]],
+  ];
+
+  for (const [objectInput, mapLevel2] of mapLevel1) {
+    for (const [propertyInput, outputDict] of mapLevel2) {
+      const step = withPropertyFromObject({
+        object: objectInput,
+        property: propertyInput,
+      });
+
+      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);
+  }
+});
diff --git a/test/unit/data/composite/things/track/withAlbum.js b/test/unit/data/composite/things/track/withAlbum.js
new file mode 100644
index 0000000..30f8cc5
--- /dev/null
+++ b/test/unit/data/composite/things/track/withAlbum.js
@@ -0,0 +1,144 @@
+import t from 'tap';
+
+import {compositeFrom, input} from '#composite';
+import {exposeConstant, exposeDependency} from '#composite/control-flow';
+import {withAlbum} from '#composite/things/track';
+
+t.test(`withAlbum: basic behavior`, t => {
+  t.plan(3);
+
+  const composite = compositeFrom({
+    compose: false,
+    steps: [
+      withAlbum(),
+      exposeDependency({dependency: '#album'}),
+    ],
+  });
+
+  t.match(composite, {
+    expose: {
+      dependencies: ['albumData', 'this'],
+    },
+  });
+
+  const fakeTrack1 = {directory: 'foo'};
+  const fakeTrack2 = {directory: 'bar'};
+  const fakeAlbum = {directory: 'baz', tracks: [fakeTrack1]};
+
+  t.equal(
+    composite.expose.compute({
+      albumData: [fakeAlbum],
+      this: fakeTrack1,
+    }),
+    fakeAlbum);
+
+  t.equal(
+    composite.expose.compute({
+      albumData: [fakeAlbum],
+      this: fakeTrack2,
+    }),
+    null);
+});
+
+t.test(`withAlbum: early exit conditions (notFoundMode: null)`, t => {
+  t.plan(4);
+
+  const composite = compositeFrom({
+    compose: false,
+    steps: [
+      withAlbum(),
+      exposeConstant({
+        value: input.value('bimbam'),
+      }),
+    ],
+  });
+
+  const fakeTrack1 = {directory: 'foo'};
+  const fakeTrack2 = {directory: 'bar'};
+  const fakeAlbum = {directory: 'baz', tracks: [fakeTrack1]};
+
+  t.equal(
+    composite.expose.compute({
+      albumData: [fakeAlbum],
+      this: fakeTrack1,
+    }),
+    'bimbam',
+    `does not early exit if albumData is present and contains the track`);
+
+  t.equal(
+    composite.expose.compute({
+      albumData: [fakeAlbum],
+      this: fakeTrack2,
+    }),
+    'bimbam',
+    `does not early exit if albumData is present and does not contain the track`);
+
+  t.equal(
+    composite.expose.compute({
+      albumData: [],
+      this: fakeTrack1,
+    }),
+    'bimbam',
+    `does not early exit if albumData is empty array`);
+
+  t.equal(
+    composite.expose.compute({
+      albumData: null,
+      this: fakeTrack1,
+    }),
+    null,
+    `early exits if albumData is null`);
+});
+
+t.test(`withAlbum: early exit conditions (notFoundMode: exit)`, t => {
+  t.plan(4);
+
+  const composite = compositeFrom({
+    compose: false,
+    steps: [
+      withAlbum({
+        notFoundMode: input.value('exit'),
+      }),
+
+      exposeConstant({
+        value: input.value('bimbam'),
+      }),
+    ],
+  });
+
+  const fakeTrack1 = {directory: 'foo'};
+  const fakeTrack2 = {directory: 'bar'};
+  const fakeAlbum = {directory: 'baz', tracks: [fakeTrack1]};
+
+  t.equal(
+    composite.expose.compute({
+      albumData: [fakeAlbum],
+      this: fakeTrack1,
+    }),
+    'bimbam',
+    `does not early exit if albumData is present and contains the track`);
+
+  t.equal(
+    composite.expose.compute({
+      albumData: [fakeAlbum],
+      this: fakeTrack2,
+    }),
+    null,
+    `early exits if albumData is present and does not contain the track`);
+
+  t.equal(
+    composite.expose.compute({
+      albumData: [],
+      this: fakeTrack1,
+    }),
+    null,
+    `early exits if albumData is empty array`);
+
+  t.equal(
+    composite.expose.compute({
+      albumData: null,
+      this: fakeTrack1,
+    }),
+    null,
+    `early exits if albumData is null`);
+});
diff --git a/test/unit/data/compositeFrom.js b/test/unit/data/compositeFrom.js
new file mode 100644
index 0000000..0029667
--- /dev/null
+++ b/test/unit/data/compositeFrom.js
@@ -0,0 +1,345 @@
+import t from 'tap';
+
+import {compositeFrom, continuationSymbol, input} from '#composite';
+import {isString} from '#validators';
+
+t.test(`compositeFrom: basic behavior`, t => {
+  t.plan(2);
+
+  const composite = compositeFrom({
+    annotation: `myComposite`,
+    compose: false,
+
+    steps: [
+      {
+        dependencies: ['foo'],
+        compute: (continuation, {foo}) =>
+          continuation({'#bar': foo * 2}),
+      },
+
+      {
+        dependencies: ['#bar', 'baz', 'suffix'],
+        compute: ({'#bar': bar, baz, suffix}) =>
+          baz.repeat(bar) + suffix,
+      },
+    ],
+  });
+
+  t.match(composite, {
+    annotation: `myComposite`,
+
+    flags: {expose: true, compose: false, update: false},
+
+    expose: {
+      dependencies: ['foo', 'baz', 'suffix'],
+      compute: Function,
+      transform: null,
+    },
+
+    update: null,
+  });
+
+  t.equal(
+    composite.expose.compute({
+      foo: 3,
+      baz: 'ba',
+      suffix: 'BOOM',
+    }),
+    'babababababaBOOM');
+});
+
+t.test(`compositeFrom: input-shaped step dependencies`, t => {
+  t.plan(2);
+
+  const composite = compositeFrom({
+    compose: false,
+    steps: [
+      {
+        dependencies: [
+          input.myself(),
+          input.updateValue(),
+        ],
+
+        transform: (updateValue1, {
+          [input.myself()]: me,
+          [input.updateValue()]: updateValue2,
+        }) => ({me, updateValue1, updateValue2}),
+      },
+    ],
+  });
+
+  t.match(composite, {
+    expose: {
+      dependencies: ['this'],
+      transform: Function,
+      compute: null,
+    },
+  });
+
+  const myself = {foo: 'bar'};
+
+  t.same(
+    composite.expose.transform('banana', {
+      this: myself,
+      pomelo: 'delicious',
+    }),
+    {
+      me: myself,
+      updateValue1: 'banana',
+      updateValue2: 'banana',
+    });
+});
+
+t.test(`compositeFrom: dependencies from inputs`, t => {
+  t.plan(3);
+
+  const composite = compositeFrom({
+    annotation: `myComposite`,
+
+    compose: true,
+
+    inputMapping: {
+      foo:      input('bar'),
+      pomelo:   input.value('delicious'),
+      humorous: input.dependency('#mammal'),
+      data:     input.dependency('albumData'),
+      ref:      input.updateValue(),
+    },
+
+    inputDescriptions: {
+      foo:      input(),
+      pomelo:   input(),
+      humorous: input(),
+      data:     input(),
+      ref:      input(),
+    },
+
+    steps: [
+      {
+        dependencies: [
+          input('foo'),
+          input('pomelo'),
+          input('humorous'),
+          input('data'),
+          input('ref'),
+        ],
+
+        compute: (continuation, {
+          [input('foo')]: foo,
+          [input('pomelo')]: pomelo,
+          [input('humorous')]: humorous,
+          [input('data')]: data,
+          [input('ref')]: ref,
+        }) => continuation.exit({foo, pomelo, humorous, data, ref}),
+      },
+    ],
+  });
+
+  t.match(composite, {
+    expose: {
+      dependencies: [
+        input('bar'),
+        '#mammal',
+        'albumData',
+      ],
+
+      transform: Function,
+      compute: null,
+    },
+  });
+
+  const exitData = {};
+  const continuation = {
+    exit(value) {
+      Object.assign(exitData, value);
+      return continuationSymbol;
+    },
+  };
+
+  t.equal(
+    composite.expose.transform('album:bepis', continuation, {
+      [input('bar')]: 'squid time',
+      '#mammal': 'fox',
+      'albumData': ['album1', 'album2'],
+    }),
+    continuationSymbol);
+
+  t.same(exitData, {
+    foo: 'squid time',
+    pomelo: 'delicious',
+    humorous: 'fox',
+    data: ['album1', 'album2'],
+    ref: 'album:bepis',
+  });
+});
+
+t.test(`compositeFrom: update from various sources`, t => {
+  t.plan(3);
+
+  const match = {
+    flags: {update: true, expose: true, compose: false},
+
+    update: {
+      validate: isString,
+      default: 'foo',
+    },
+
+    expose: {
+      transform: Function,
+      compute: null,
+    },
+  };
+
+  t.test(`compositeFrom: update from composition description`, t => {
+    t.plan(2);
+
+    const composite = compositeFrom({
+      compose: false,
+
+      update: {
+        validate: isString,
+        default: 'foo',
+      },
+
+      steps: [
+        {transform: (value, continuation) => continuation(value.repeat(2))},
+        {transform: (value) => `Xx_${value}_xX`},
+      ],
+    });
+
+    t.match(composite, match);
+    t.equal(composite.expose.transform('foo'), `Xx_foofoo_xX`);
+  });
+
+  t.test(`compositeFrom: update from step dependencies`, t => {
+    t.plan(2);
+
+    const composite = compositeFrom({
+      compose: false,
+
+      steps: [
+        {
+          dependencies: [
+            input.updateValue({
+              validate: isString,
+              default: 'foo',
+            }),
+          ],
+
+          compute: ({
+            [input.updateValue()]: value,
+          }) => `Xx_${value.repeat(2)}_xX`,
+        },
+      ],
+    });
+
+    t.match(composite, match);
+    t.equal(composite.expose.transform('foo'), 'Xx_foofoo_xX');
+  });
+
+  t.test(`compositeFrom: update from inputs`, t => {
+    t.plan(3);
+
+    const composite = compositeFrom({
+      inputMapping: {
+        myInput: input.updateValue({
+          validate: isString,
+          default: 'foo',
+        }),
+      },
+
+      inputDescriptions: {
+        myInput: input(),
+      },
+
+      steps: [
+        {
+          dependencies: [input('myInput')],
+          compute: (continuation, {
+            [input('myInput')]: value,
+          }) => continuation({
+            '#value': `Xx_${value.repeat(2)}_xX`,
+          }),
+        },
+
+        {
+          dependencies: ['#value'],
+          transform: (_value, continuation, {'#value': value}) =>
+            continuation(value),
+        },
+      ],
+    });
+
+    let continuationValue = null;
+    const continuation = value => {
+      continuationValue = value;
+      return continuationSymbol;
+    };
+
+    t.match(composite, {
+      ...match,
+
+      flags: {update: true, expose: true, compose: true},
+    });
+
+    t.equal(
+      composite.expose.transform('foo', continuation),
+      continuationSymbol);
+
+    t.equal(continuationValue, 'Xx_foofoo_xX');
+  });
+});
+
+t.test(`compositeFrom: dynamic input validation from type`, t => {
+  t.plan(2);
+
+  const composite = compositeFrom({
+    inputMapping: {
+      string:   input('string'),
+      number:   input('number'),
+      boolean:  input('boolean'),
+      function: input('function'),
+      object:   input('object'),
+      array:    input('array'),
+    },
+
+    inputDescriptions: {
+      string:   input({null: true, type: 'string'}),
+      number:   input({null: true, type: 'number'}),
+      boolean:  input({null: true, type: 'boolean'}),
+      function: input({null: true, type: 'function'}),
+      object:   input({null: true, type: 'object'}),
+      array:    input({null: true, type: 'array'}),
+    },
+
+    outputs: {'#result': '#result'},
+
+    steps: [
+      {compute: continuation => continuation({'#result': 'OK'})},
+    ],
+  });
+
+  const notCalledSymbol = Symbol('continuation not called');
+
+  let continuationValue;
+  const continuation = value => {
+    continuationValue = value;
+    return continuationSymbol;
+  };
+
+  let thrownError;
+
+  try {
+    continuationValue = notCalledSymbol;
+    thrownError = null;
+    composite.expose.compute(continuation, {
+      [input('string')]: 123,
+    });
+  } catch (error) {
+    thrownError = error;
+  }
+
+  t.equal(continuationValue, notCalledSymbol);
+  t.match(thrownError, {
+  });
+});
diff --git a/test/unit/data/templateCompositeFrom.js b/test/unit/data/templateCompositeFrom.js
new file mode 100644
index 0000000..2de1873
--- /dev/null
+++ b/test/unit/data/templateCompositeFrom.js
@@ -0,0 +1,209 @@
+import t from 'tap';
+
+import {isString} from '#validators';
+
+import {
+  compositeFrom,
+  continuationSymbol,
+  input,
+  templateCompositeFrom,
+} from '#composite';
+
+t.test(`templateCompositeFrom: basic behavior`, t => {
+  t.plan(1);
+
+  const myCoolUtility = templateCompositeFrom({
+    annotation: `myCoolUtility`,
+
+    inputs: {
+      foo: input(),
+    },
+
+    outputs: ['#bar'],
+
+    steps: () => [
+      {
+        dependencies: [input('foo')],
+        compute: (continuation, {
+          [input('foo')]: foo,
+        }) => continuation({
+          ['#bar']: (typeof foo).toUpperCase()
+        }),
+      },
+    ],
+  });
+
+  const instantiatedTemplate = myCoolUtility({
+    foo: 'color',
+  });
+
+  t.match(instantiatedTemplate.toDescription(), {
+    annotation: `myCoolUtility`,
+
+    inputMapping: {
+      foo: input.dependency('color'),
+    },
+
+    inputDescriptions: {
+      foo: input(),
+    },
+
+    outputs: {
+      '#bar': '#bar',
+    },
+
+    steps: Function,
+  });
+});
+
+t.test(`templateCompositeFrom: validate static input values`, t => {
+  t.plan(3);
+
+  const stub = {
+    annotation: 'stubComposite',
+    outputs: ['#result'],
+    steps: () => [{compute: continuation => continuation({'#result': 'OK'})}],
+  };
+
+  const quickThrows = (t, composite, inputOptions, ...errorMessages) =>
+    t.throws(
+      () => composite(inputOptions),
+      {
+        message: `Errors in input options passed to stubComposite`,
+        errors: errorMessages.map(message => ({message})),
+      });
+
+  t.test(`templateCompositeFrom: validate input token shapes`, t => {
+    t.plan(15);
+
+    const template1 = templateCompositeFrom({
+      ...stub, inputs: {
+        foo: input(),
+      },
+    });
+
+    t.doesNotThrow(
+      () => template1({foo: 'dependency'}));
+
+    t.doesNotThrow(
+      () => template1({foo: input.dependency('dependency')}));
+
+    t.doesNotThrow(
+      () => template1({foo: input.value('static value')}));
+
+    t.doesNotThrow(
+      () => template1({foo: input('outerInput')}));
+
+    t.doesNotThrow(
+      () => template1({foo: input.updateValue()}));
+
+    t.doesNotThrow(
+      () => template1({foo: input.myself()}));
+
+    quickThrows(t, template1,
+      {foo: input.staticValue()},
+      `foo: Expected dependency name or value-providing input() call, got input.staticValue`);
+
+    quickThrows(t, template1,
+      {foo: input.staticDependency()},
+      `foo: Expected dependency name or value-providing input() call, got input.staticDependency`);
+
+    const template2 = templateCompositeFrom({
+      ...stub, inputs: {
+        bar: input.staticDependency(),
+      },
+    });
+
+    t.doesNotThrow(
+      () => template2({bar: 'dependency'}));
+
+    t.doesNotThrow(
+      () => template2({bar: input.dependency('dependency')}));
+
+    quickThrows(t, template2,
+      {bar: input.value(123)},
+      `bar: Expected dependency name, got input.value`);
+
+    quickThrows(t, template2,
+      {bar: input('outOfPlace')},
+      `bar: Expected dependency name, got input`);
+
+    const template3 = templateCompositeFrom({
+      ...stub, inputs: {
+        baz: input.staticValue(),
+      },
+    });
+
+    t.doesNotThrow(
+      () => template3({baz: input.value(1025)}));
+
+    quickThrows(t, template3,
+      {baz: 'dependency'},
+      `baz: Expected input.value() call, got dependency name`);
+
+    quickThrows(t, template3,
+      {baz: input('outOfPlace')},
+      `baz: Expected input.value() call, got input() call`);
+  });
+
+  t.test(`templateCompositeFrom: validate missing / misplaced inputs`, t => {
+    t.plan(1);
+
+    const template = templateCompositeFrom({
+      ...stub, inputs: {
+        foo: input(),
+        bar: input(),
+      },
+    });
+
+    t.throws(
+      () => template({
+        baz: 'aeiou',
+        raz: input.value(123),
+      }),
+      {message: `Errors in input options passed to stubComposite`, errors: [
+        {message: `Unexpected input names: baz, raz`},
+        {message: `Required these inputs: foo, bar`},
+      ]});
+  });
+
+  t.test(`templateCompositeFrom: validate acceptsNull / defaultValue: null`, t => {
+    t.plan(3);
+
+    const template1 = templateCompositeFrom({
+      ...stub, inputs: {
+        foo: input(),
+      },
+    });
+
+    t.throws(
+      () => template1({}),
+      {message: `Errors in input options passed to stubComposite`, errors: [
+        {message: `Required these inputs: foo`},
+      ]},
+      `throws if input missing and not marked specially`);
+
+    const template2 = templateCompositeFrom({
+      ...stub, inputs: {
+        bar: input({acceptsNull: true}),
+      },
+    });
+
+    t.throws(
+      () => template2({}),
+      {message: `Errors in input options passed to stubComposite`, errors: [
+        {message: `Required these inputs: bar`},
+      ]},
+      `throws if input missing even if marked {acceptsNull}`);
+
+    const template3 = templateCompositeFrom({
+      ...stub, inputs: {
+        baz: input({defaultValue: null}),
+      },
+    });
+
+    t.doesNotThrow(
+      () => template3({}),
+      `does not throw if input missing if marked {defaultValue: null}`);
+  });
+});
diff --git a/test/unit/data/things/album.js b/test/unit/data/things/album.js
new file mode 100644
index 0000000..76a2b90
--- /dev/null
+++ b/test/unit/data/things/album.js
@@ -0,0 +1,411 @@
+import t from 'tap';
+
+import {linkAndBindWikiData} from '#test-lib';
+import thingConstructors from '#things';
+
+const {
+  Album,
+  Artist,
+  Track,
+} = thingConstructors;
+
+function stubArtistAndContribs() {
+  const artist = new Artist();
+  artist.name = `Test Artist`;
+
+  const contribs = [{who: `Test Artist`, what: null}];
+  const badContribs = [{who: `Figment of Your Imagination`, what: null}];
+
+  return {artist, contribs, badContribs};
+}
+
+function stubTrack(directory = 'foo') {
+  const track = new Track();
+  track.directory = directory;
+
+  return track;
+}
+
+t.test(`Album.bannerDimensions`, t => {
+  t.plan(4);
+
+  const album = new Album();
+  const {artist, contribs, badContribs} = stubArtistAndContribs();
+
+  linkAndBindWikiData({
+    albumData: [album],
+    artistData: [artist],
+  });
+
+  t.equal(album.bannerDimensions, null,
+    `Album.bannerDimensions #1: defaults to null`);
+
+  album.bannerDimensions = [1200, 275];
+
+  t.equal(album.bannerDimensions, null,
+    `Album.bannerDimensions #2: is null if bannerArtistContribs empty`);
+
+  album.bannerArtistContribs = badContribs;
+
+  t.equal(album.bannerDimensions, null,
+    `Album.bannerDimensions #3: is null if bannerArtistContribs resolves empty`);
+
+  album.bannerArtistContribs = contribs;
+
+  t.same(album.bannerDimensions, [1200, 275],
+    `Album.bannerDimensions #4: is own value`);
+});
+
+t.test(`Album.bannerFileExtension`, t => {
+  t.plan(5);
+
+  const album = new Album();
+  const {artist, contribs, badContribs} = stubArtistAndContribs();
+
+  linkAndBindWikiData({
+    albumData: [album],
+    artistData: [artist],
+  });
+
+  t.equal(album.bannerFileExtension, null,
+    `Album.bannerFileExtension #1: defaults to null`);
+
+  album.bannerFileExtension = 'png';
+
+  t.equal(album.bannerFileExtension, null,
+    `Album.bannerFileExtension #2: is null if bannerArtistContribs empty`);
+
+  album.bannerArtistContribs = badContribs;
+
+  t.equal(album.bannerFileExtension, null,
+    `Album.bannerFileExtension #3: is null if bannerArtistContribs resolves empty`);
+
+  album.bannerArtistContribs = contribs;
+
+  t.equal(album.bannerFileExtension, 'png',
+    `Album.bannerFileExtension #4: is own value`);
+
+  album.bannerFileExtension = null;
+
+  t.equal(album.bannerFileExtension, 'jpg',
+    `Album.bannerFileExtension #5: defaults to jpg`);
+});
+
+t.test(`Album.bannerStyle`, t => {
+  t.plan(4);
+
+  const album = new Album();
+  const {artist, contribs, badContribs} = stubArtistAndContribs();
+
+  linkAndBindWikiData({
+    albumData: [album],
+    artistData: [artist],
+  });
+
+  t.equal(album.bannerStyle, null,
+    `Album.bannerStyle #1: defaults to null`);
+
+  album.bannerStyle = `opacity: 0.5`;
+
+  t.equal(album.bannerStyle, null,
+    `Album.bannerStyle #2: is null if bannerArtistContribs empty`);
+
+  album.bannerArtistContribs = badContribs;
+
+  t.equal(album.bannerStyle, null,
+    `Album.bannerStyle #3: is null if bannerArtistContribs resolves empty`);
+
+  album.bannerArtistContribs = contribs;
+
+  t.equal(album.bannerStyle, `opacity: 0.5`,
+    `Album.bannerStyle #4: is own value`);
+});
+
+t.test(`Album.coverArtDate`, t => {
+  t.plan(6);
+
+  const album = new Album();
+  const {artist, contribs, badContribs} = stubArtistAndContribs();
+
+  linkAndBindWikiData({
+    albumData: [album],
+    artistData: [artist],
+  });
+
+  t.equal(album.coverArtDate, null,
+    `Album.coverArtDate #1: defaults to null`);
+
+  album.date = new Date('2012-10-25');
+
+  t.equal(album.coverArtDate, null,
+    `Album.coverArtDate #2: is null if coverArtistContribs empty (1/2)`);
+
+  album.coverArtDate = new Date('2011-04-13');
+
+  t.equal(album.coverArtDate, null,
+    `Album.coverArtDate #3: is null if coverArtistContribs empty (2/2)`);
+
+  album.coverArtistContribs = contribs;
+
+  t.same(album.coverArtDate, new Date('2011-04-13'),
+    `Album.coverArtDate #4: is own value`);
+
+  album.coverArtDate = null;
+
+  t.same(album.coverArtDate, new Date(`2012-10-25`),
+    `Album.coverArtDate #5: inherits album release date`);
+
+  album.coverArtistContribs = badContribs;
+
+  t.equal(album.coverArtDate, null,
+    `Album.coverArtDate #6: is null if coverArtistContribs resolves empty`);
+});
+
+t.test(`Album.coverArtFileExtension`, t => {
+  t.plan(5);
+
+  const album = new Album();
+  const {artist, contribs, badContribs} = stubArtistAndContribs();
+
+  linkAndBindWikiData({
+    albumData: [album],
+    artistData: [artist],
+  });
+
+  t.equal(album.coverArtFileExtension, null,
+    `Album.coverArtFileExtension #1: is null if coverArtistContribs empty (1/2)`);
+
+  album.coverArtFileExtension = 'png';
+
+  t.equal(album.coverArtFileExtension, null,
+    `Album.coverArtFileExtension #2: is null if coverArtistContribs empty (2/2)`);
+
+  album.coverArtFileExtension = null;
+  album.coverArtistContribs = contribs;
+
+  t.equal(album.coverArtFileExtension, 'jpg',
+    `Album.coverArtFileExtension #3: defaults to jpg`);
+
+  album.coverArtFileExtension = 'png';
+
+  t.equal(album.coverArtFileExtension, 'png',
+    `Album.coverArtFileExtension #4: is own value`);
+
+  album.coverArtistContribs = badContribs;
+
+  t.equal(album.coverArtFileExtension, null,
+    `Album.coverArtFileExtension #5: is null if coverArtistContribs resolves empty`);
+});
+
+t.test(`Album.tracks`, t => {
+  t.plan(5);
+
+  const album = new Album();
+  const track1 = stubTrack('track1');
+  const track2 = stubTrack('track2');
+  const track3 = stubTrack('track3');
+
+  linkAndBindWikiData({
+    albumData: [album],
+    trackData: [track1, track2, track3],
+  });
+
+  t.same(album.tracks, [],
+    `Album.tracks #1: defaults to empty array`);
+
+  album.trackSections = [
+    {tracks: ['track:track1', 'track:track2', 'track:track3']},
+  ];
+
+  t.same(album.tracks, [track1, track2, track3],
+    `Album.tracks #2: pulls tracks from one track section`);
+
+  album.trackSections = [
+    {tracks: ['track:track1']},
+    {tracks: ['track:track2', 'track:track3']},
+  ];
+
+  t.same(album.tracks, [track1, track2, track3],
+    `Album.tracks #3: pulls tracks from multiple track sections`);
+
+  album.trackSections = [
+    {tracks: ['track:track1', 'track:does-not-exist']},
+    {tracks: ['track:this-one-neither', 'track:track2']},
+    {tracks: ['track:effectively-empty-section']},
+    {tracks: ['track:track3']},
+  ];
+
+  t.same(album.tracks, [track1, track2, track3],
+    `Album.tracks #4: filters out references without matches`);
+
+  album.trackSections = [
+    {tracks: ['track:track1']},
+    {},
+    {tracks: ['track:track2']},
+    {},
+    {},
+    {tracks: ['track:track3']},
+  ];
+
+  t.same(album.tracks, [track1, track2, track3],
+    `Album.tracks #5: skips missing tracks property`);
+});
+
+t.test(`Album.trackSections`, t => {
+  t.plan(7);
+
+  const album = new Album();
+  const track1 = stubTrack('track1');
+  const track2 = stubTrack('track2');
+  const track3 = stubTrack('track3');
+  const track4 = stubTrack('track4');
+
+  linkAndBindWikiData({
+    albumData: [album],
+    trackData: [track1, track2, track3, track4],
+  });
+
+  album.trackSections = [
+    {tracks: ['track:track1', 'track:track2']},
+    {tracks: ['track:track3', 'track:track4']},
+  ];
+
+  t.match(album.trackSections, [
+    {tracks: [track1, track2]},
+    {tracks: [track3, track4]},
+  ], `Album.trackSections #1: exposes tracks`);
+
+  t.match(album.trackSections, [
+    {tracks: [track1, track2], startIndex: 0},
+    {tracks: [track3, track4], startIndex: 2},
+  ], `Album.trackSections #2: exposes startIndex`);
+
+  album.trackSections = [
+    {name: 'First section', tracks: ['track:track1']},
+    {name: 'Second section', tracks: ['track:track2']},
+    {tracks: ['track:track3']},
+  ];
+
+  t.match(album.trackSections, [
+    {name: 'First section', tracks: [track1]},
+    {name: 'Second section', tracks: [track2]},
+    {name: 'Unnamed Track Section', tracks: [track3]},
+  ], `Album.trackSections #3: exposes name, with fallback value`);
+
+  album.color = '#123456';
+
+  album.trackSections = [
+    {tracks: ['track:track1'], color: null},
+    {tracks: ['track:track2'], color: '#abcdef'},
+    {tracks: ['track:track3'], color: null},
+  ];
+
+  t.match(album.trackSections, [
+    {tracks: [track1], color: '#123456'},
+    {tracks: [track2], color: '#abcdef'},
+    {tracks: [track3], color: '#123456'},
+  ], `Album.trackSections #4: exposes color, inherited from album`);
+
+  album.trackSections = [
+    {tracks: ['track:track1'], dateOriginallyReleased: null},
+    {tracks: ['track:track2'], dateOriginallyReleased: new Date('2009-04-11')},
+    {tracks: ['track:track3'], dateOriginallyReleased: null},
+  ];
+
+  t.match(album.trackSections, [
+    {tracks: [track1], dateOriginallyReleased: null},
+    {tracks: [track2], dateOriginallyReleased: new Date('2009-04-11')},
+    {tracks: [track3], dateOriginallyReleased: null},
+  ], `Album.trackSections #5: exposes dateOriginallyReleased, if present`);
+
+  album.trackSections = [
+    {tracks: ['track:track1'], isDefaultTrackSection: true},
+    {tracks: ['track:track2'], isDefaultTrackSection: false},
+    {tracks: ['track:track3'], isDefaultTrackSection: null},
+  ];
+
+  t.match(album.trackSections, [
+    {tracks: [track1], isDefaultTrackSection: true},
+    {tracks: [track2], isDefaultTrackSection: false},
+    {tracks: [track3], isDefaultTrackSection: false},
+  ], `Album.trackSections #6: exposes isDefaultTrackSection, defaults to false`);
+
+  album.trackSections = [
+    {tracks: ['track:track1', 'track:track2', 'track:snooping'], color: '#112233'},
+    {tracks: ['track:track3', 'track:as-usual'],                 color: '#334455'},
+    {tracks: [],                                                 color: '#bbbbba'},
+    {tracks: ['track:icy', 'track:chilly', 'track:frigid'],      color: '#556677'},
+    {tracks: ['track:track4'],                                   color: '#778899'},
+  ];
+
+  t.match(album.trackSections, [
+    {tracks: [track1, track2], color: '#112233'},
+    {tracks: [track3],         color: '#334455'},
+    {tracks: [track4],         color: '#778899'},
+  ], `Album.trackSections #7: filters out references without matches & empty sections`);
+});
+
+t.test(`Album.wallpaperFileExtension`, t => {
+  t.plan(5);
+
+  const album = new Album();
+  const {artist, contribs, badContribs} = stubArtistAndContribs();
+
+  linkAndBindWikiData({
+    albumData: [album],
+    artistData: [artist],
+  });
+
+  t.equal(album.wallpaperFileExtension, null,
+    `Album.wallpaperFileExtension #1: defaults to null`);
+
+  album.wallpaperFileExtension = 'png';
+
+  t.equal(album.wallpaperFileExtension, null,
+    `Album.wallpaperFileExtension #2: is null if wallpaperArtistContribs empty`);
+
+  album.wallpaperArtistContribs = contribs;
+
+  t.equal(album.wallpaperFileExtension, 'png',
+    `Album.wallpaperFileExtension #3: is own value`);
+
+  album.wallpaperFileExtension = null;
+
+  t.equal(album.wallpaperFileExtension, 'jpg',
+    `Album.wallpaperFileExtension #4: defaults to jpg`);
+
+  album.wallpaperArtistContribs = badContribs;
+
+  t.equal(album.wallpaperFileExtension, null,
+    `Album.wallpaperFileExtension #5: is null if wallpaperArtistContribs resolves empty`);
+});
+
+t.test(`Album.wallpaperStyle`, t => {
+  t.plan(4);
+
+  const album = new Album();
+  const {artist, contribs, badContribs} = stubArtistAndContribs();
+
+  linkAndBindWikiData({
+    albumData: [album],
+    artistData: [artist],
+  });
+
+  t.equal(album.wallpaperStyle, null,
+    `Album.wallpaperStyle #1: defaults to null`);
+
+  album.wallpaperStyle = `opacity: 0.5`;
+
+  t.equal(album.wallpaperStyle, null,
+    `Album.wallpaperStyle #2: is null if wallpaperArtistContribs empty`);
+
+  album.wallpaperArtistContribs = badContribs;
+
+  t.equal(album.wallpaperStyle, null,
+    `Album.wallpaperStyle #3: is null if wallpaperArtistContribs resolves empty`);
+
+  album.wallpaperArtistContribs = contribs;
+
+  t.equal(album.wallpaperStyle, `opacity: 0.5`,
+    `Album.wallpaperStyle #4: is own value`);
+});
diff --git a/test/unit/data/things/art-tag.js b/test/unit/data/things/art-tag.js
new file mode 100644
index 0000000..561c93e
--- /dev/null
+++ b/test/unit/data/things/art-tag.js
@@ -0,0 +1,71 @@
+import t from 'tap';
+
+import {linkAndBindWikiData} from '#test-lib';
+import thingConstructors from '#things';
+
+const {
+  Album,
+  Artist,
+  ArtTag,
+  Track,
+} = thingConstructors;
+
+function stubAlbum(tracks, directory = 'bar') {
+  const album = new Album();
+  album.directory = directory;
+
+  const trackRefs = tracks.map(t => Thing.getReference(t));
+  album.trackSections = [{tracks: trackRefs}];
+
+  return album;
+}
+
+function stubTrack(directory = 'foo') {
+  const track = new Track();
+  track.directory = directory;
+
+  return track;
+}
+
+function stubTrackAndAlbum(trackDirectory = 'foo', albumDirectory = 'bar') {
+  const track = stubTrack(trackDirectory);
+  const album = stubAlbum([track], albumDirectory);
+
+  return {track, album};
+}
+
+function stubArtist(artistName = `Test Artist`) {
+  const artist = new Artist();
+  artist.name = artistName;
+
+  return artist;
+}
+
+function stubArtistAndContribs(artistName = `Test Artist`) {
+  const artist = stubArtist(artistName);
+  const contribs = [{who: artistName, what: null}];
+  const badContribs = [{who: `Figment of Your Imagination`, what: null}];
+
+  return {artist, contribs, badContribs};
+}
+
+t.test(`ArtTag.nameShort`, t => {
+  t.plan(3);
+
+  const artTag = new ArtTag();
+
+  artTag.name = `Dave Strider`;
+
+  t.equal(artTag.nameShort, `Dave Strider`,
+    `ArtTag #1: defaults to name`);
+
+  artTag.name = `Dave Strider (Homestuck)`;
+
+  t.equal(artTag.nameShort, `Dave Strider`,
+    `ArtTag #2: trims parenthical part at end`);
+
+  artTag.name = `This (And) That (Then)`;
+
+  t.equal(artTag.nameShort, `This (And) That`,
+    `ArtTag #2: doesn't trim midlde parenthical part`);
+});
diff --git a/test/unit/data/things/flash.js b/test/unit/data/things/flash.js
new file mode 100644
index 0000000..6205960
--- /dev/null
+++ b/test/unit/data/things/flash.js
@@ -0,0 +1,55 @@
+import t from 'tap';
+
+import {linkAndBindWikiData} from '#test-lib';
+import thingConstructors from '#things';
+
+const {
+  Flash,
+  FlashAct,
+  Thing,
+} = thingConstructors;
+
+function stubFlash(directory = 'foo') {
+  const flash = new Flash();
+  flash.directory = directory;
+
+  return flash;
+}
+
+function stubFlashAct(flashes, directory = 'bar') {
+  const flashAct = new FlashAct();
+  flashAct.directory = directory;
+  flashAct.flashes = flashes.map(flash => Thing.getReference(flash));
+
+  return flashAct;
+}
+
+t.test(`Flash.color`, t => {
+  t.plan(4);
+
+  const flash = stubFlash();
+  const flashAct = stubFlashAct([flash]);
+
+  const {XXX_decacheWikiData} = linkAndBindWikiData({
+    flashData: [flash],
+    flashActData: [flashAct],
+  });
+
+  t.equal(flash.color, null,
+    `color #1: defaults to null`);
+
+  flashAct.color = '#abcdef';
+  XXX_decacheWikiData();
+
+  t.equal(flash.color, '#abcdef',
+    `color #2: inherits from flash act`);
+
+  flash.color = '#123456';
+
+  t.equal(flash.color, '#123456',
+    `color #3: is own value`);
+
+  t.throws(() => { flash.color = '#aeiouw'; },
+    {cause: TypeError},
+    `color #4: must be set to valid color`);
+});
diff --git a/test/unit/data/things/track.js b/test/unit/data/things/track.js
index 383e3e3..806efbf 100644
--- a/test/unit/data/things/track.js
+++ b/test/unit/data/things/track.js
@@ -1,74 +1,683 @@
 import t from 'tap';
+
+import {showAggregate} from '#sugar';
+import {linkAndBindWikiData} from '#test-lib';
 import thingConstructors from '#things';
 
 const {
   Album,
+  Artist,
+  Flash,
+  FlashAct,
   Thing,
   Track,
-  TrackGroup,
 } = thingConstructors;
 
-function stubAlbum(tracks) {
+function stubAlbum(tracks, directory = 'bar') {
   const album = new Album();
-  album.trackSections = [
-    {
-      tracksByRef: tracks.map(t => Thing.getReference(t)),
-    },
-  ];
-  album.trackData = tracks;
+  album.directory = directory;
+
+  const trackRefs = tracks.map(t => Thing.getReference(t));
+  album.trackSections = [{tracks: trackRefs}];
+
   return album;
 }
 
-t.test(`Track.coverArtDate`, t => {
+function stubTrack(directory = 'foo') {
+  const track = new Track();
+  track.directory = directory;
+
+  return track;
+}
+
+function stubTrackAndAlbum(trackDirectory = 'foo', albumDirectory = 'bar') {
+  const track = stubTrack(trackDirectory);
+  const album = stubAlbum([track], albumDirectory);
+
+  return {track, album};
+}
+
+function stubArtist(artistName = `Test Artist`) {
+  const artist = new Artist();
+  artist.name = artistName;
+
+  return artist;
+}
+
+function stubArtistAndContribs(artistName = `Test Artist`) {
+  const artist = stubArtist(artistName);
+  const contribs = [{who: artistName, what: null}];
+  const badContribs = [{who: `Figment of Your Imagination`, what: null}];
+
+  return {artist, contribs, badContribs};
+}
+
+function stubFlashAndAct(directory = 'zam') {
+  const flash = new Flash();
+  flash.directory = directory;
+
+  const flashAct = new FlashAct();
+  flashAct.flashes = [Thing.getReference(flash)];
+
+  return {flash, flashAct};
+}
+
+t.test(`Track.album`, t => {
+  t.plan(6);
+
+  // Note: These asserts use manual albumData/trackData relationships
+  // to illustrate more specifically the properties which are expected to
+  // be relevant for this case. Other properties use the same underlying
+  // get-album behavior as Track.album so aren't tested as aggressively.
+
+  const track1 = stubTrack('track1');
+  const track2 = stubTrack('track2');
+  const album1 = new Album();
+  const album2 = new Album();
+
+  t.equal(track1.album, null,
+    `album #1: defaults to null`);
+
+  track1.albumData = [album1, album2];
+  track2.albumData = [album1, album2];
+  album1.trackData = [track1, track2];
+  album2.trackData = [track1, track2];
+  album1.trackSections = [{tracks: ['track:track1']}];
+  album2.trackSections = [{tracks: ['track:track2']}];
+
+  t.equal(track1.album, album1,
+    `album #2: is album when album's trackSections matches track`);
+
+  track1.albumData = [album2, album1];
+
+  t.equal(track1.album, album1,
+    `album #3: is album when albumData is in different order`);
+
+  track1.albumData = [];
+
+  t.equal(track1.album, null,
+    `album #4: is null when track missing albumData`);
+
+  album1.trackData = [];
+  track1.albumData = [album1, album2];
+
+  t.equal(track1.album, null,
+    `album #5: is null when album missing trackData`);
+
+  album1.trackData = [track1, track2];
+  album1.trackSections = [{tracks: ['track:track2']}];
+
+  // XXX_decacheWikiData
+  track1.albumData = [];
+  track1.albumData = [album1, album2];
+
+  t.equal(track1.album, null,
+    `album #6: is null when album's trackSections don't match track`);
+});
+
+t.test(`Track.artistContribs`, t => {
+  t.plan(4);
+
+  const {track, album} = stubTrackAndAlbum();
+  const artist1 = stubArtist(`Artist 1`);
+  const artist2 = stubArtist(`Artist 2`);
+
+  const {XXX_decacheWikiData} = linkAndBindWikiData({
+    albumData: [album],
+    artistData: [artist1, artist2],
+    trackData: [track],
+  });
+
+  t.same(track.artistContribs, [],
+    `artistContribs #1: defaults to empty array`);
+
+  album.artistContribs = [
+    {who: `Artist 1`, what: `composition`},
+    {who: `Artist 2`, what: null},
+  ];
+
+  XXX_decacheWikiData();
+
+  t.same(track.artistContribs,
+    [{who: artist1, what: `composition`}, {who: artist2, what: null}],
+    `artistContribs #2: inherits album artistContribs`);
+
+  track.artistContribs = [
+    {who: `Artist 1`, what: `arrangement`},
+  ];
+
+  t.same(track.artistContribs, [{who: artist1, what: `arrangement`}],
+    `artistContribs #3: resolves from own value`);
+
+  track.artistContribs = [
+    {who: `Artist 1`, what: `snooping`},
+    {who: `Artist 413`, what: `as`},
+    {who: `Artist 2`, what: `usual`},
+  ];
+
+  t.same(track.artistContribs,
+    [{who: artist1, what: `snooping`}, {who: artist2, what: `usual`}],
+    `artistContribs #4: filters out names without matches`);
+});
+
+t.test(`Track.color`, t => {
   t.plan(5);
 
-  // Priority order is as follows, with the last (trackCoverArtDate) being
-  // greatest priority.
-  const albumDate = new Date('2010-10-10');
-  const albumTrackArtDate = new Date('2012-12-12');
-  const trackDateFirstReleased = new Date('2008-08-08');
-  const trackCoverArtDate = new Date('2009-09-09');
+  const {track, album} = stubTrackAndAlbum();
+
+  const {wikiData, linkWikiDataArrays, XXX_decacheWikiData} = linkAndBindWikiData({
+    albumData: [album],
+    trackData: [track],
+  });
+
+  t.equal(track.color, null,
+    `color #1: defaults to null`);
+
+  album.color = '#abcdef';
+  album.trackSections = [{
+    color: '#beeeef',
+    tracks: [Thing.getReference(track)],
+  }];
+  XXX_decacheWikiData();
+
+  t.equal(track.color, '#beeeef',
+    `color #2: inherits from track section before album`);
+
+  // Replace the album with a completely fake one. This isn't realistic, since
+  // in correct data, Album.tracks depends on Albums.trackSections and so the
+  // track's album will always have a corresponding track section. But if that
+  // connection breaks for some future reason (with the album still present),
+  // Track.color should still inherit directly from the album.
+  wikiData.albumData = [
+    new Proxy({
+      color: '#abcdef',
+      tracks: [track],
+      trackSections: [
+        {color: '#baaaad', tracks: []},
+      ],
+    }, {getPrototypeOf: () => Album.prototype}),
+  ];
+
+  linkWikiDataArrays();
+
+  t.equal(track.color, '#abcdef',
+    `color #3: inherits from album without matching track section`);
+
+  track.color = '#123456';
+
+  t.equal(track.color, '#123456',
+    `color #4: is own value`);
+
+  t.throws(() => { track.color = '#aeiouw'; },
+    {cause: TypeError},
+    `color #5: must be set to valid color`);
+});
+
+t.test(`Track.commentatorArtists`, t => {
+  t.plan(6);
 
   const track = new Track();
-  track.directory = 'foo';
+  const artist1 = stubArtist(`SnooPING`);
+  const artist2 = stubArtist(`ASUsual`);
+  const artist3 = stubArtist(`Icy`);
+
+  linkAndBindWikiData({
+    trackData: [track],
+    artistData: [artist1, artist2, artist3],
+  });
+
+  track.commentary =
+    `<i>SnooPING:</i>\n` +
+    `Wow.\n`;
+
+  t.same(track.commentatorArtists, [artist1],
+    `Track.commentatorArtists #1: works with one commentator`);
+
+  track.commentary +=
+    `<i>ASUsual:</i>\n` +
+    `Yes!\n`;
+
+  t.same(track.commentatorArtists, [artist1, artist2],
+    `Track.commentatorArtists #2: works with two commentators`);
+
+  track.commentary +=
+    `<i><b>Icy:</b></i>\n` +
+    `Incredible.\n`;
+
+  t.same(track.commentatorArtists, [artist1, artist2, artist3],
+    `Track.commentatorArtists #3: works with boldface name`);
+
+  track.commentary =
+    `<i>Icy:</i> (project manager)\n` +
+    `Very good track.\n`;
+
+  t.same(track.commentatorArtists, [artist3],
+    `Track.commentatorArtists #4: works with parenthical accent`);
+
+  track.commentary +=
+    `<i>SNooPING ASUsual Icy:</i>\n` +
+    `WITH ALL THREE POWERS COMBINED...`;
+
+  t.same(track.commentatorArtists, [artist3],
+    `Track.commentatorArtists #5: ignores artist names not found`);
+
+  track.commentary +=
+    `<i>Icy:</i>\n` +
+    `I'm back!\n`;
+
+  t.same(track.commentatorArtists, [artist3],
+    `Track.commentatorArtists #6: ignores duplicate artist`);
+});
+
+t.test(`Track.coverArtistContribs`, t => {
+  t.plan(5);
+
+  const {track, album} = stubTrackAndAlbum();
+  const artist1 = stubArtist(`Artist 1`);
+  const artist2 = stubArtist(`Artist 2`);
+
+  const {XXX_decacheWikiData} = linkAndBindWikiData({
+    albumData: [album],
+    artistData: [artist1, artist2],
+    trackData: [track],
+  });
+
+  t.same(track.coverArtistContribs, [],
+    `coverArtistContribs #1: defaults to empty array`);
+
+  album.trackCoverArtistContribs = [
+    {who: `Artist 1`, what: `lines`},
+    {who: `Artist 2`, what: null},
+  ];
+
+  XXX_decacheWikiData();
+
+  t.same(track.coverArtistContribs,
+    [{who: artist1, what: `lines`}, {who: artist2, what: null}],
+    `coverArtistContribs #2: inherits album trackCoverArtistContribs`);
+
+  track.coverArtistContribs = [
+    {who: `Artist 1`, what: `collage`},
+  ];
+
+  t.same(track.coverArtistContribs, [{who: artist1, what: `collage`}],
+    `coverArtistContribs #3: resolves from own value`);
+
+  track.coverArtistContribs = [
+    {who: `Artist 1`, what: `snooping`},
+    {who: `Artist 413`, what: `as`},
+    {who: `Artist 2`, what: `usual`},
+  ];
+
+  t.same(track.coverArtistContribs,
+    [{who: artist1, what: `snooping`}, {who: artist2, what: `usual`}],
+    `coverArtistContribs #4: filters out names without matches`);
+
+  track.disableUniqueCoverArt = true;
+
+  t.same(track.coverArtistContribs, [],
+    `coverArtistContribs #5: is empty if track disables unique cover artwork`);
+});
+
+t.test(`Track.coverArtDate`, t => {
+  t.plan(8);
+
+  const {track, album} = stubTrackAndAlbum();
+  const {artist, contribs, badContribs} = stubArtistAndContribs();
+
+  const {XXX_decacheWikiData} = linkAndBindWikiData({
+    albumData: [album],
+    artistData: [artist],
+    trackData: [track],
+  });
+
+  track.coverArtistContribs = contribs;
 
-  const album = stubAlbum([track]);
+  t.equal(track.coverArtDate, null,
+    `coverArtDate #1: defaults to null`);
 
-  track.albumData = [album];
+  album.trackArtDate = new Date('2012-12-12');
 
-  // 1. coverArtDate defaults to null
+  XXX_decacheWikiData();
 
-  t.equal(track.coverArtDate, null);
+  t.same(track.coverArtDate, new Date('2012-12-12'),
+    `coverArtDate #2: inherits album trackArtDate`);
 
-  // 2. coverArtDate inherits album release date
+  track.coverArtDate = new Date('2009-09-09');
 
-  album.date = albumDate;
+  t.same(track.coverArtDate, new Date('2009-09-09'),
+    `coverArtDate #3: is own value`);
 
-  // XXX clear cache so change in album's property is reflected
-  track.albumData = [];
-  track.albumData = [album];
+  track.coverArtistContribs = [];
 
-  t.equal(track.coverArtDate, albumDate);
+  t.equal(track.coverArtDate, null,
+    `coverArtDate #4: is null if track coverArtistContribs empty`);
+
+  album.trackCoverArtistContribs = contribs;
+
+  XXX_decacheWikiData();
+
+  t.same(track.coverArtDate, new Date('2009-09-09'),
+    `coverArtDate #5: is not null if album trackCoverArtistContribs specified`);
+
+  album.trackCoverArtistContribs = badContribs;
+
+  XXX_decacheWikiData();
+
+  t.equal(track.coverArtDate, null,
+    `coverArtDate #6: is null if album trackCoverArtistContribs resolves empty`);
+
+  track.coverArtistContribs = badContribs;
+
+  t.equal(track.coverArtDate, null,
+    `coverArtDate #7: is null if track coverArtistContribs resolves empty`);
+
+  track.coverArtistContribs = contribs;
+  track.disableUniqueCoverArt = true;
+
+  t.equal(track.coverArtDate, null,
+    `coverArtDate #8: is null if track disables unique cover artwork`);
+});
+
+t.test(`Track.coverArtFileExtension`, t => {
+  t.plan(8);
+
+  const {track, album} = stubTrackAndAlbum();
+  const {artist, contribs} = stubArtistAndContribs();
+
+  const {XXX_decacheWikiData} = linkAndBindWikiData({
+    albumData: [album],
+    artistData: [artist],
+    trackData: [track],
+  });
+
+  t.equal(track.coverArtFileExtension, null,
+    `coverArtFileExtension #1: defaults to null`);
+
+  track.coverArtistContribs = contribs;
+
+  t.equal(track.coverArtFileExtension, 'jpg',
+    `coverArtFileExtension #2: is jpg if has cover art and not further specified`);
+
+  track.coverArtistContribs = [];
+
+  album.coverArtistContribs = contribs;
+  XXX_decacheWikiData();
+
+  t.equal(track.coverArtFileExtension, null,
+    `coverArtFileExtension #3: only has value for unique cover art`);
+
+  track.coverArtistContribs = contribs;
+
+  album.trackCoverArtFileExtension = 'png';
+  XXX_decacheWikiData();
+
+  t.equal(track.coverArtFileExtension, 'png',
+    `coverArtFileExtension #4: inherits album trackCoverArtFileExtension (1/2)`);
+
+  track.coverArtFileExtension = 'gif';
+
+  t.equal(track.coverArtFileExtension, 'gif',
+    `coverArtFileExtension #5: is own value (1/2)`);
+
+  track.coverArtistContribs = [];
+
+  album.trackCoverArtistContribs = contribs;
+  XXX_decacheWikiData();
+
+  t.equal(track.coverArtFileExtension, 'gif',
+    `coverArtFileExtension #6: is own value (2/2)`);
+
+  track.coverArtFileExtension = null;
+
+  t.equal(track.coverArtFileExtension, 'png',
+    `coverArtFileExtension #7: inherits album trackCoverArtFileExtension (2/2)`);
+
+  track.disableUniqueCoverArt = true;
+
+  t.equal(track.coverArtFileExtension, null,
+    `coverArtFileExtension #8: is null if track disables unique cover art`);
+});
+
+t.test(`Track.date`, t => {
+  t.plan(3);
+
+  const {track, album} = stubTrackAndAlbum();
+
+  const {XXX_decacheWikiData} = linkAndBindWikiData({
+    albumData: [album],
+    trackData: [track],
+  });
+
+  t.equal(track.date, null,
+    `date #1: defaults to null`);
+
+  album.date = new Date('2012-12-12');
+  XXX_decacheWikiData();
+
+  t.same(track.date, album.date,
+    `date #2: inherits from album`);
+
+  track.dateFirstReleased = new Date('2009-09-09');
+
+  t.same(track.date, new Date('2009-09-09'),
+    `date #3: is own dateFirstReleased`);
+});
+
+t.test(`Track.featuredInFlashes`, t => {
+  t.plan(2);
+
+  const {track, album} = stubTrackAndAlbum('track1');
+
+  const {flash: flash1, flashAct: flashAct1} = stubFlashAndAct('flash1');
+  const {flash: flash2, flashAct: flashAct2} = stubFlashAndAct('flash2');
+
+  const {XXX_decacheWikiData} = linkAndBindWikiData({
+    albumData: [album],
+    trackData: [track],
+    flashData: [flash1, flash2],
+    flashActData: [flashAct1, flashAct2],
+  });
+
+  t.same(track.featuredInFlashes, [],
+    `featuredInFlashes #1: defaults to empty array`);
+
+  flash1.featuredTracks = ['track:track1'];
+  flash2.featuredTracks = ['track:track1'];
+  XXX_decacheWikiData();
+
+  t.same(track.featuredInFlashes, [flash1, flash2],
+    `featuredInFlashes #2: matches flashes' featuredTracks`);
+});
+
+t.test(`Track.hasUniqueCoverArt`, t => {
+  t.plan(7);
+
+  const {track, album} = stubTrackAndAlbum();
+  const {artist, contribs, badContribs} = stubArtistAndContribs();
+
+  const {XXX_decacheWikiData} = linkAndBindWikiData({
+    albumData: [album],
+    artistData: [artist],
+    trackData: [track],
+  });
+
+  t.equal(track.hasUniqueCoverArt, false,
+    `hasUniqueCoverArt #1: defaults to false`);
+
+  album.trackCoverArtistContribs = contribs;
+  XXX_decacheWikiData();
+
+  t.equal(track.hasUniqueCoverArt, true,
+    `hasUniqueCoverArt #2: is true if album specifies trackCoverArtistContribs`);
+
+  track.disableUniqueCoverArt = true;
+
+  t.equal(track.hasUniqueCoverArt, false,
+    `hasUniqueCoverArt #3: is false if disableUniqueCoverArt is true (1/2)`);
+
+  track.disableUniqueCoverArt = false;
+
+  album.trackCoverArtistContribs = badContribs;
+  XXX_decacheWikiData();
+
+  t.equal(track.hasUniqueCoverArt, false,
+    `hasUniqueCoverArt #4: is false if album's trackCoverArtistContribs resolve empty`);
+
+  track.coverArtistContribs = contribs;
+
+  t.equal(track.hasUniqueCoverArt, true,
+    `hasUniqueCoverArt #5: is true if track specifies coverArtistContribs`);
+
+  track.disableUniqueCoverArt = true;
+
+  t.equal(track.hasUniqueCoverArt, false,
+    `hasUniqueCoverArt #6: is false if disableUniqueCoverArt is true (2/2)`);
+
+  track.disableUniqueCoverArt = false;
+
+  track.coverArtistContribs = badContribs;
+
+  t.equal(track.hasUniqueCoverArt, false,
+    `hasUniqueCoverArt #7: is false if track's coverArtistContribs resolve empty`);
+});
+
+t.test(`Track.originalReleaseTrack`, t => {
+  t.plan(3);
+
+  const {track: track1, album: album1} = stubTrackAndAlbum('track1');
+  const {track: track2, album: album2} = stubTrackAndAlbum('track2');
+
+  const {wikiData, linkWikiDataArrays, XXX_decacheWikiData} = linkAndBindWikiData({
+    albumData: [album1, album2],
+    trackData: [track1, track2],
+  });
+
+  t.equal(track2.originalReleaseTrack, null,
+    `originalReleaseTrack #1: defaults to null`);
+
+  track2.originalReleaseTrack = 'track:track1';
+
+  t.equal(track2.originalReleaseTrack, track1,
+    `originalReleaseTrack #2: is resolved from own value`);
+
+  track2.trackData = [];
+
+  t.equal(track2.originalReleaseTrack, null,
+    `originalReleaseTrack #3: is null when track missing trackData`);
+});
+
+t.test(`Track.otherReleases`, t => {
+  t.plan(6);
+
+  const {track: track1, album: album1} = stubTrackAndAlbum('track1');
+  const {track: track2, album: album2} = stubTrackAndAlbum('track2');
+  const {track: track3, album: album3} = stubTrackAndAlbum('track3');
+  const {track: track4, album: album4} = stubTrackAndAlbum('track4');
+
+  const {wikiData, linkWikiDataArrays, XXX_decacheWikiData} = linkAndBindWikiData({
+    albumData: [album1, album2, album3, album4],
+    trackData: [track1, track2, track3, track4],
+  });
+
+  t.same(track1.otherReleases, [],
+    `otherReleases #1: defaults to empty array`);
+
+  track2.originalReleaseTrack = 'track:track1';
+  track3.originalReleaseTrack = 'track:track1';
+  track4.originalReleaseTrack = 'track:track1';
+  XXX_decacheWikiData();
+
+  t.same(track1.otherReleases, [track2, track3, track4],
+    `otherReleases #2: otherReleases of original release are its rereleases`);
+
+  wikiData.trackData = [track1, track3, track2, track4];
+  linkWikiDataArrays();
+
+  t.same(track1.otherReleases, [track3, track2, track4],
+    `otherReleases #3: otherReleases matches trackData order`);
+
+  wikiData.trackData = [track3, track2, track1, track4];
+  linkWikiDataArrays();
+
+  t.same(track2.otherReleases, [track1, track3, track4],
+    `otherReleases #4: otherReleases of rerelease are original track then other rereleases (1/3)`);
+
+  t.same(track3.otherReleases, [track1, track2, track4],
+    `otherReleases #5: otherReleases of rerelease are original track then other rereleases (2/3)`);
+
+  t.same(track4.otherReleases, [track1, track3, track2],
+    `otherReleases #6: otherReleases of rerelease are original track then other rereleases (3/3)`);
+});
+
+t.test(`Track.referencedByTracks`, t => {
+  t.plan(4);
+
+  const {track: track1, album: album1} = stubTrackAndAlbum('track1');
+  const {track: track2, album: album2} = stubTrackAndAlbum('track2');
+  const {track: track3, album: album3} = stubTrackAndAlbum('track3');
+  const {track: track4, album: album4} = stubTrackAndAlbum('track4');
+
+  const {XXX_decacheWikiData} = linkAndBindWikiData({
+    albumData: [album1, album2, album3, album4],
+    trackData: [track1, track2, track3, track4],
+  });
+
+  t.same(track1.referencedByTracks, [],
+    `referencedByTracks #1: defaults to empty array`);
+
+  track2.referencedTracks = ['track:track1'];
+  track3.referencedTracks = ['track:track1'];
+  XXX_decacheWikiData();
+
+  t.same(track1.referencedByTracks, [track2, track3],
+    `referencedByTracks #2: matches tracks' referencedTracks`);
+
+  track4.sampledTracks = ['track:track1'];
+  XXX_decacheWikiData();
+
+  t.same(track1.referencedByTracks, [track2, track3],
+    `referencedByTracks #3: doesn't match tracks' sampledTracks`);
+
+  track3.originalReleaseTrack = 'track:track2';
+  XXX_decacheWikiData();
+
+  t.same(track1.referencedByTracks, [track2],
+    `referencedByTracks #4: doesn't include re-releases`);
+});
 
-  // 3. coverArtDate inherits album trackArtDate
+t.test(`Track.sampledByTracks`, t => {
+  t.plan(4);
 
-  album.trackArtDate = albumTrackArtDate;
+  const {track: track1, album: album1} = stubTrackAndAlbum('track1');
+  const {track: track2, album: album2} = stubTrackAndAlbum('track2');
+  const {track: track3, album: album3} = stubTrackAndAlbum('track3');
+  const {track: track4, album: album4} = stubTrackAndAlbum('track4');
 
-  // XXX clear cache again
-  track.albumData = [];
-  track.albumData = [album];
+  const {XXX_decacheWikiData} = linkAndBindWikiData({
+    albumData: [album1, album2, album3, album4],
+    trackData: [track1, track2, track3, track4],
+  });
 
-  t.equal(track.coverArtDate, albumTrackArtDate);
+  t.same(track1.sampledByTracks, [],
+    `sampledByTracks #1: defaults to empty array`);
 
-  // 4. coverArtDate is overridden dateFirstReleased
+  track2.sampledTracks = ['track:track1'];
+  track3.sampledTracks = ['track:track1'];
+  XXX_decacheWikiData();
 
-  track.dateFirstReleased = trackDateFirstReleased;
+  t.same(track1.sampledByTracks, [track2, track3],
+    `sampledByTracks #2: matches tracks' sampledTracks`);
 
-  t.equal(track.coverArtDate, trackDateFirstReleased);
+  track4.referencedTracks = ['track:track1'];
+  XXX_decacheWikiData();
 
-  // 5. coverArtDate is overridden coverArtDate
+  t.same(track1.sampledByTracks, [track2, track3],
+    `sampledByTracks #3: doesn't match tracks' referencedTracks`);
 
-  track.coverArtDate = trackCoverArtDate;
+  track3.originalReleaseTrack = 'track:track2';
+  XXX_decacheWikiData();
 
-  t.equal(track.coverArtDate, trackCoverArtDate);
+  t.same(track1.sampledByTracks, [track2],
+    `sampledByTracks #4: doesn't include re-releases`);
 });