« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--data-tests/index.js4
-rw-r--r--package.json1
-rw-r--r--src/content/dependencies/generateAlbumInfoPage.js2
-rw-r--r--src/content/dependencies/generateAlbumTrackListItem.js20
-rw-r--r--src/content/dependencies/generateTrackInfoPage.js7
-rw-r--r--src/content/dependencies/generateTrackList.js59
-rw-r--r--src/content/dependencies/generateWikiHomeAlbumsRow.js2
-rw-r--r--src/content/dependencies/index.js15
-rw-r--r--src/data/things/album.js397
-rw-r--r--src/data/things/art-tag.js45
-rw-r--r--src/data/things/artist.js97
-rw-r--r--src/data/things/cacheable-object.js65
-rw-r--r--src/data/things/composite.js2359
-rw-r--r--src/data/things/flash.js102
-rw-r--r--src/data/things/group.js72
-rw-r--r--src/data/things/homepage-layout.js111
-rw-r--r--src/data/things/index.js23
-rw-r--r--src/data/things/language.js24
-rw-r--r--src/data/things/news-entry.js15
-rw-r--r--src/data/things/static-page.js22
-rw-r--r--src/data/things/thing.js994
-rw-r--r--src/data/things/track.js1100
-rw-r--r--src/data/things/validators.js75
-rw-r--r--src/data/things/wiki-info.js55
-rw-r--r--src/data/yaml.js209
-rw-r--r--src/find.js6
-rw-r--r--src/gen-thumbs.js15
-rw-r--r--src/listing-spec.js12
-rw-r--r--src/page/flash.js2
-rw-r--r--src/repl.js3
-rwxr-xr-xsrc/upd8.js24
-rw-r--r--src/util/cli.js8
-rw-r--r--src/util/html.js6
-rw-r--r--src/util/replacer.js5
-rw-r--r--src/util/sugar.js97
-rw-r--r--src/util/urls.js21
-rw-r--r--src/util/wiki-data.js70
-rw-r--r--src/write/build-modes/live-dev-server.js9
-rw-r--r--src/write/build-modes/static-build.js15
-rw-r--r--test/lib/index.js3
-rw-r--r--test/lib/wiki-data.js24
-rw-r--r--test/unit/data/cacheable-object.js (renamed from test/unit/data/things/cacheable-object.js)0
-rw-r--r--test/unit/data/composite/common-utilities/exposeConstant.js52
-rw-r--r--test/unit/data/composite/common-utilities/exposeDependency.js74
-rw-r--r--test/unit/data/composite/common-utilities/withPropertiesFromObject.js224
-rw-r--r--test/unit/data/composite/common-utilities/withPropertyFromObject.js125
-rw-r--r--test/unit/data/composite/common-utilities/withResultOfAvailabilityCheck.js200
-rw-r--r--test/unit/data/compositeFrom.js345
-rw-r--r--test/unit/data/templateCompositeFrom.js218
-rw-r--r--test/unit/data/things/album.js387
-rw-r--r--test/unit/data/things/art-tag.js71
-rw-r--r--test/unit/data/things/track.js589
52 files changed, 6969 insertions, 1511 deletions
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.json b/package.json
index b6e26b6..535405a 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
     },
     "imports": {
         "#colors": "./src/util/colors.js",
+        "#composite": "./src/data/things/composite.js",
         "#content-dependencies": "./src/content/dependencies/index.js",
         "#content-function": "./src/content-function.js",
         "#cli": "./src/util/cli.js",
diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js
index 5c4344b..5fe27ca 100644
--- a/src/content/dependencies/generateAlbumInfoPage.js
+++ b/src/content/dependencies/generateAlbumInfoPage.js
@@ -44,7 +44,7 @@ export default {
 
     relations.coverArtistChronologyContributions =
       getChronologyRelations(album, {
-        contributions: album.coverArtistContribs,
+        contributions: album.coverArtistContribs ?? [],
 
         linkArtist: artist => relation('linkArtist', artist),
 
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/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js
index c20c0d0..1083d86 100644
--- a/src/content/dependencies/generateTrackInfoPage.js
+++ b/src/content/dependencies/generateTrackInfoPage.js
@@ -51,7 +51,10 @@ export default {
 
     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/index.js b/src/content/dependencies/index.js
index 3bc3484..7180205 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;
@@ -192,7 +191,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 +218,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/data/things/album.js b/src/data/things/album.js
index c012c24..fd8a71d 100644
--- a/src/data/things/album.js
+++ b/src/data/things/album.js
@@ -1,163 +1,264 @@
-import {empty} from '#sugar';
 import find from '#find';
-
-import Thing from './thing.js';
+import {empty, stitchArrays} from '#sugar';
+import {isDate, isTrackSectionList} from '#validators';
+import {filterMultipleArrays} from '#wiki-data';
+
+import {
+  exitWithoutDependency,
+  exitWithoutUpdateValue,
+  exposeDependency,
+  exposeUpdateValueOrContinue,
+  input,
+  fillMissingListItems,
+  withFlattenedList,
+  withPropertiesFromList,
+  withUnflattenedList,
+} from '#composite';
+
+import Thing, {
+  additionalFiles,
+  commentary,
+  color,
+  commentatorArtists,
+  contribsPresent,
+  contributionList,
+  dimensions,
+  directory,
+  exitWithoutContribs,
+  fileExtension,
+  flag,
+  name,
+  referenceList,
+  simpleDate,
+  simpleString,
+  urls,
+  wikiData,
+  withResolvedReferenceList,
+} 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)) ??
-              [],
-          }));
+    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: [
+      exitWithoutDependency({
+        dependency: 'trackData',
+        value: input.value([]),
+      }),
+
+      exitWithoutUpdateValue({
+        mode: input.value('empty'),
+        value: input.value([]),
+      }),
+
+      withPropertiesFromList({
+        list: input.updateValue(),
+        prefix: input.value('#sections'),
+        properties: input.value([
+          'tracks',
+          'dateOriginallyReleased',
+          'isDefaultTrackSection',
+          'color',
+        ]),
+      }),
+
+      fillMissingListItems({
+        list: '#sections.tracks',
+        fill: input.value([]),
+      }),
+
+      fillMissingListItems({
+        list: '#sections.isDefaultTrackSection',
+        fill: input.value(false),
+      }),
+
+      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',
+      }),
+
+      {
+        flags: {update: true, expose: true},
+
+        update: {validate: isTrackSectionList},
+
+        expose: {
+          dependencies: [
+            '#sections.tracks',
+            '#sections.color',
+            '#sections.dateOriginallyReleased',
+            '#sections.isDefaultTrackSection',
+            '#sections.startIndex',
+          ],
+
+          transform(trackSections, {
+            '#sections.tracks': tracks,
+            '#sections.color': color,
+            '#sections.dateOriginallyReleased': dateOriginallyReleased,
+            '#sections.isDefaultTrackSection': isDefaultTrackSection,
+            '#sections.startIndex': startIndex,
+          }) {
+            filterMultipleArrays(
+              tracks, color, dateOriginallyReleased, isDefaultTrackSection, startIndex,
+              tracks => !empty(tracks));
+
+            return stitchArrays({
+              tracks,
+              color,
+              dateOriginallyReleased,
+              isDefaultTrackSection,
+              startIndex,
+            });
+          }
         },
       },
-    },
-
-    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(),
+    ],
+
+    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)
-            : [],
+    commentatorArtists: commentatorArtists(),
+
+    hasCoverArt: contribsPresent({contribs: 'coverArtistContribs'}),
+    hasWallpaperArt: contribsPresent({contribs: 'wallpaperArtistContribs'}),
+    hasBannerArt: contribsPresent({contribs: 'bannerArtistContribs'}),
+
+    tracks: [
+      exitWithoutDependency({
+        dependency: 'trackData',
+        value: input.value([]),
+      }),
+
+      exitWithoutDependency({
+        dependency: 'trackSections',
+        mode: input.value('empty'),
+        value: input.value([]),
+      }),
+
+      {
+        dependencies: ['trackSections'],
+        compute: (continuation, {trackSections}) =>
+          continuation({
+            '#trackRefs': trackSections
+              .flatMap(section => section.tracks ?? []),
+          }),
       },
-    },
-
-    groups: Thing.common.dynamicThingsFromReferenceList(
-      'groupsByRef',
-      'groupData',
-      find.group
-    ),
-
-    artTags: Thing.common.dynamicThingsFromReferenceList(
-      'artTagsByRef',
-      'artTagData',
-      find.artTag
-    ),
+
+      withResolvedReferenceList({
+        list: '#trackRefs',
+        data: 'trackData',
+        find: input.value(find.track),
+      }),
+
+      exposeDependency({dependency: '#resolvedReferenceList'}),
+    ],
   });
 
   static [Thing.getSerializeDescriptors] = ({
@@ -202,9 +303,9 @@ export class Album extends Thing {
 
 export class TrackSectionHelper extends Thing {
   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 Group'),
+    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..ba3cbd0 100644
--- a/src/data/things/art-tag.js
+++ b/src/data/things/art-tag.js
@@ -1,35 +1,42 @@
+import {exposeUpdateValueOrContinue, input} from '#composite';
 import {sortAlbumsTracksChronologically} from '#wiki-data';
+import {isName} from '#validators';
 
-import Thing from './thing.js';
+import Thing, {
+  color,
+  directory,
+  flag,
+  name,
+  wikiData,
+} from './thing.js';
 
 export class ArtTag extends Thing {
   static [Thing.referenceType] = '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 +44,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 522ca5f..085e566 100644
--- a/src/data/things/artist.js
+++ b/src/data/things/artist.js
@@ -1,29 +1,31 @@
+import {input} from '#composite';
 import find from '#find';
-
-import Thing from './thing.js';
+import {isName, validateArrayItems} from '#validators';
+
+import Thing, {
+  directory,
+  fileExtension,
+  flag,
+  name,
+  simpleString,
+  singleReference,
+  urls,
+  wikiData,
+} 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 +33,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 +61,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 +77,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)) ?? [],
       },
@@ -103,18 +98,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] = ({
@@ -148,15 +141,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..4bc3668 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) {
@@ -185,7 +181,7 @@ export default class CacheableObject {
           }
         } catch (error) {
           error.message = [
-            `Property ${color.green(property)}`,
+            `Property ${colors.green(property)}`,
             `(${inspect(this[property])} -> ${inspect(newValue)}):`,
             error.message
           ].join(' ');
@@ -250,20 +246,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 +350,12 @@ export default class CacheableObject {
       console.log(` - ${line}`);
     }
   }
+
+  static getUpdateValue(object, key) {
+    if (!Object.hasOwn(object, key)) {
+      return undefined;
+    }
+
+    return object.#propertyUpdateValues[key] ?? null;
+  }
 }
diff --git a/src/data/things/composite.js b/src/data/things/composite.js
new file mode 100644
index 0000000..c03f883
--- /dev/null
+++ b/src/data/things/composite.js
@@ -0,0 +1,2359 @@
+import {inspect} from 'node:util';
+
+import {colors} from '#cli';
+import {TupleMap} from '#wiki-data';
+
+import {
+  a,
+  is,
+  isString,
+  isWholeNumber,
+  validateArrayItems,
+} from '#validators';
+
+import {
+  decorateErrorWithIndex,
+  empty,
+  filterProperties,
+  openAggregate,
+  stitchArrays,
+  typeAppearance,
+  unique,
+  withAggregate,
+} from '#sugar';
+
+// Composes multiple compositional "steps" and a "base" to form a property
+// descriptor out of modular building blocks. This is an extension to the
+// more general-purpose CacheableObject property descriptor syntax, and
+// aims to make modular data processing - which lends to declarativity -
+// much easier, without fundamentally altering much of the typical syntax
+// or terminology, nor building on it to an excessive degree.
+//
+// Think of a composition as being a chain of steps which lead into a final
+// base property, which is usually responsible for returning the value that
+// will actually get exposed when the property being described is accessed.
+//
+// == The compositional base: ==
+//
+// The final item in a compositional list is its base, and it identifies
+// the essential qualities of the property descriptor. The compositional
+// steps preceding it may exit early, in which case the expose function
+// defined on the base won't be called; or they will provide dependencies
+// that the base may use to compute the final value that gets exposed for
+// this property.
+//
+// The base indicates the capabilities of the composition as a whole.
+// It should be {expose: true}, since that's the only area that preceding
+// compositional steps (currently) can actually influence. If it's also
+// {update: true}, then the composition as a whole accepts an update value
+// just like normal update-flag property descriptors - meaning it can be
+// set with `thing.someProperty = value` and that value will be paseed
+// into each (implementing) step's transform() function, as well as the
+// base. Bases usually aren't {compose: true}, but can be - check out the
+// section on "nesting compositions" for details about that.
+//
+// Every composition always has exactly one compositional base, and it's
+// always the last item in the composition list. All items preceding it
+// are compositional steps, described below.
+//
+// == Compositional steps: ==
+//
+// Compositional steps are, in essence, typical property descriptors with
+// the extra flag {compose: true}. They operate on existing dependencies,
+// and are typically dynamically constructed by "utility" functions (but
+// can also be manually declared within the step list of a composition).
+// Compositional steps serve two purposes:
+//
+//  1. exit early, if some condition is matched, returning and exposing
+//     some value directly from that step instead of continuing further
+//     down the step list;
+//
+//  2. and/or provide new, dynamically created "private" dependencies which
+//     can be accessed by further steps down the list, or at the base at
+//     the bottom, modularly supplying information that will contribute to
+//     the final value exposed for this property.
+//
+// Usually it's just one of those two, but it's fine for a step to perform
+// both jobs if the situation benefits.
+//
+// Compositional steps are the real "modular" or "compositional" part of
+// this data processing style - they're designed to be combined together
+// in dynamic, versatile ways, as each property demands it. You usually
+// define a compositional step to be returned by some ordinary static
+// property-descriptor-returning function (customarily namespaced under
+// the relevant Thing class's static `composite` field) - that lets you
+// reuse it in multiple compositions later on.
+//
+// Compositional steps are implemented with "continuation passing style",
+// meaning the connection to the next link on the chain is passed right to
+// each step's compute (or transform) function, and the implementation gets
+// to decide whether to continue on that chain or exit early by returning
+// some other value.
+//
+// Every step along the chain, apart from the base at the bottom, has to
+// have the {compose: true} step. That means its compute() or transform()
+// function will be passed an extra argument at the end, `continuation`.
+// To provide new dependencies to items further down the chain, just pass
+// them directly to this continuation() function, customarily with a hash
+// ('#') prefixing each name - for example:
+//
+//   compute({..some dependencies..}, continuation) {
+//     return continuation({
+//       '#excitingProperty': (..a value made from dependencies..),
+//     });
+//   }
+//
+// Performing an early exit is as simple as returning some other value,
+// instead of the continuation. You may also use `continuation.exit(value)`
+// to perform the exact same kind of early exit - it's just a different
+// syntax that might fit in better in certain longer compositions.
+//
+// It may be fine to simply provide new dependencies under a hard-coded
+// name, such as '#excitingProperty' above, but if you're writing a utility
+// that dynamically returns the compositional step and you suspect you
+// might want to use this step multiple times in a single composition,
+// it's customary to accept a name for the result.
+//
+// Here's a detailed example showing off early exit, dynamically operating
+// on a provided dependency name, and then providing a result in another
+// also-provided dependency name:
+//
+//   withResolvedContribs = ({
+//     from: contribsByRefDependency,
+//     into: outputDependency,
+//   }) => ({
+//     flags: {expose: true, compose: true},
+//     expose: {
+//       dependencies: [contribsByRefDependency, 'artistData'],
+//       compute({
+//         [contribsByRefDependency]: contribsByRef,
+//         artistData,
+//       }, continuation) {
+//         if (!artistData) return null;  /* early exit! */
+//         return continuation({
+//           [outputDependency]:  /* this is the important part */
+//             (..resolve contributions one way or another..),
+//         });
+//       },
+//     },
+//   });
+//
+// And how you might work that into a composition:
+//
+//   Track.coverArtists =
+//     compositeFrom([
+//       doSomethingWhichMightEarlyExit(),
+//
+//       withResolvedContribs({
+//         from: 'coverArtistContribsByRef',
+//         into: '#coverArtistContribs',
+//       }),
+//
+//       {
+//         flags: {expose: true},
+//         expose: {
+//           dependencies: ['#coverArtistContribs'],
+//           compute: ({'#coverArtistContribs': coverArtistContribs}) =>
+//             coverArtistContribs.map(({who}) => who),
+//         },
+//       },
+//     ]);
+//
+// One last note! A super common code pattern when creating more complex
+// compositions is to have several steps which *only* expose and compose.
+// As a syntax shortcut, you can skip the outer section. It's basically
+// like writing out just the {expose: {...}} part. Remember that this
+// indicates that the step you're defining is compositional, so you have
+// to specify the flags manually for the base, even if this property isn't
+// going to get an {update: true} flag.
+//
+// == Cache-safe dependency names: ==
+//
+// [Disclosure: The caching engine hasn't actually been implemented yet.
+//  As such, this section is subject to change, and simply provides sound
+//  forward-facing advice and interfaces.]
+//
+// It's a good idea to write individual compositional steps in such a way
+// that they're "cache-safe" - meaning the same input (dependency) values
+// will always result in the same output (continuation or early exit).
+//
+// In order to facilitate this, compositional step descriptors may specify
+// unique `mapDependencies`, `mapContinuation`, and `options` values.
+//
+// Consider the `withResolvedContribs` example adjusted to make use of
+// two of these options below:
+//
+//   withResolvedContribs = ({
+//     from: contribsByRefDependency,
+//     into: outputDependency,
+//   }) => ({
+//     flags: {expose: true, compose: true},
+//     expose: {
+//       dependencies: ['artistData'],
+//       mapDependencies: {contribsByRef: contribsByRefDependency},
+//       mapContinuation: {outputDependency},
+//       compute({
+//         contribsByRef, /* no longer in square brackets */
+//         artistData,
+//       }, continuation) {
+//         if (!artistData) return null;
+//         return continuation({
+//           outputDependency: /* no longer in square brackets */
+//             (..resolve contributions one way or another..),
+//         });
+//       },
+//     },
+//   });
+//
+// With a little destructuring and restructuring JavaScript sugar, the
+// above can be simplified some more:
+//
+//   withResolvedContribs = ({from, to}) => ({
+//     flags: {expose: true, compose: true},
+//     expose: {
+//       dependencies: ['artistData'],
+//       mapDependencies: {from},
+//       mapContinuation: {into},
+//       compute({artistData, from: contribsByRef}, continuation) {
+//         if (!artistData) return null;
+//         return continuation({
+//           into: (..resolve contributions one way or another..),
+//         });
+//       },
+//     },
+//   });
+//
+// These two properties let you separate the name-mapping behavior (for
+// dependencies and the continuation) from the main body of the compute
+// function. That means the compute function will *always* get inputs in
+// the same form (dependencies 'artistData' and 'from' above), and will
+// *always* provide its output in the same form (early return or 'to').
+//
+// Thanks to that, this `compute` function is cache-safe! Its outputs can
+// be cached corresponding to each set of mapped inputs. So it won't matter
+// whether the `from` dependency is named `coverArtistContribsByRef` or
+// `contributorContribsByRef` or something else - the compute function
+// doesn't care, and only expects that value to be provided via its `from`
+// argument. Likewise, it doesn't matter if the output should be sent to
+// '#coverArtistContribs` or `#contributorContribs` or some other name;
+// the mapping is handled automatically outside, and compute will always
+// output its value to the continuation's `to`.
+//
+// Note that `mapDependencies` and `mapContinuation` should be objects of
+// the same "shape" each run - that is, the values will change depending on
+// outside context, but the keys are always the same. You shouldn't use
+// `mapDependencies` to dynamically select more or fewer dependencies.
+// If you need to dynamically select a range of dependencies, just specify
+// them in the `dependencies` array like usual. The caching engine will
+// understand that differently named `dependencies` indicate separate
+// input-output caches should be used.
+//
+// The 'options' property makes it possible to specify external arguments
+// that fundamentally change the behavior of the `compute` function, while
+// still remaining cache-safe. It indicates that the caching engine should
+// use a completely different input-to-output cache for each permutation
+// of the 'options' values. This way, those functions are still cacheable
+// at all; they'll just be cached separately for each set of option values.
+// Values on the 'options' property will always be provided in compute's
+// dependencies under '#options' (to avoid name conflicts with other
+// dependencies).
+//
+// == To compute or to transform: ==
+//
+// A compositional step can work directly on a property's stored update
+// value, transforming it in place and either early exiting with it or
+// passing it on (via continuation) to the next item(s) in the
+// compositional step list. (If needed, these can provide dependencies
+// the same way as compute functions too - just pass that object after
+// the updated (or same) transform value in your call to continuation().)
+//
+// But in order to make them more versatile, compositional steps have an
+// extra trick up their sleeve. If a compositional step implements compute
+// and *not* transform, it can still be used in a composition targeting a
+// property which updates! These retain their full dependency-providing and
+// early exit functionality - they just won't be provided the update value.
+// If a compute-implementing step returns its continuation, then whichever
+// later step (or the base) next implements transform() will receive the
+// update value that had so far been running - as well as any dependencies
+// the compute() step returned, of course!
+//
+// Please note that a compositional step which transforms *should not*
+// specify, in its flags, {update: true}. Just provide the transform()
+// function in its expose descriptor; it will be automatically detected
+// and used when appropriate.
+//
+// It's actually possible for a step to specify both transform and compute,
+// in which case the transform() implementation will only be selected if
+// the composition's base is {update: true}. It's not exactly known why you
+// would want to specify unique-but-related transform and compute behavior,
+// but the basic possibility was too cool to skip out on.
+//
+// == Nesting compositions: ==
+//
+// Compositional steps are so convenient that you just might want to bundle
+// them together, and form a whole new step-shaped unit of its own!
+//
+// In order to allow for this while helping to ensure internal dependencies
+// remain neatly isolated from the composition which nests your bundle,
+// the compositeFrom() function will accept and adapt to a base that
+// specifies the {compose: true} flag, just like the steps preceding it.
+//
+// The continuation function that gets provided to the base will be mildly
+// special - after all, nothing follows the base within the composition's
+// own list! Instead of appending dependencies alongside any previously
+// provided ones to be available to the next step, the base's continuation
+// function should be used to define "exports" of the composition as a
+// whole. It's similar to the usual behavior of the continuation, just
+// expanded to the scope of the composition instead of following steps.
+//
+// For example, suppose your composition (which you expect to include in
+// other compositions) brings about several private, hash-prefixed
+// dependencies to contribute to its own results. Those dependencies won't
+// end up "bleeding" into the dependency list of whichever composition is
+// nesting this one - they will totally disappear once all the steps in
+// the nested composition have finished up.
+//
+// To "export" the results of processing all those dependencies (provided
+// that's something you want to do and this composition isn't used purely
+// for a conditional early-exit), you'll want to define them in the
+// continuation passed to the base. (Customarily, those should start with
+// a hash just like the exports from any other compositional step; they're
+// still dynamically provided dependencies!)
+//
+// Another way to "export" dependencies is by using calling *any* step's
+// `continuation.raise()` function. This is sort of like early exiting,
+// but instead of quitting out the whole entire property, it will just
+// break out of the current, nested composition's list of steps, acting
+// as though the composition had finished naturally. The dependencies
+// passed to `raise` will be the ones which get exported.
+//
+// Since `raise` is another way to export dependencies, if you're using
+// dynamic export names, you should specify `mapContinuation` on the step
+// calling `continuation.raise` as well.
+//
+// An important note on `mapDependencies` here: A nested composition gets
+// free access to all the ordinary properties defined on the thing it's
+// working on, but if you want it to depend on *private* dependencies -
+// ones prefixed with '#' - which were provided by some other compositional
+// step preceding wherever this one gets nested, then you *have* to use
+// `mapDependencies` to gain access. Check out the section on "cache-safe
+// dependency names" for information on this syntax!
+//
+// Also - on rare occasion - you might want to make a reusable composition
+// that itself causes the composition *it's* nested in to raise. If that's
+// the case, give `composition.raiseAbove()` a go! This effectively means
+// kicking out of *two* layers of nested composition - the one including
+// the step with the `raiseAbove` call, and the composition which that one
+// is nested within. You don't need to use `raiseAbove` if the reusable
+// utility function just returns a single compositional step, but if you
+// want to make use of other compositional steps, it gives you access to
+// the same conditional-raise capabilities.
+//
+// Have some syntax sugar! Since nested compositions are defined by having
+// the base be {compose: true}, the composition will infer as much if you
+// don't specifying the base's flags at all. Simply use the same shorthand
+// syntax as for other compositional steps, and it'll work out cleanly!
+//
+
+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;
+}
+
+// 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.
+//
+export const exposeDependency = templateCompositeFrom({
+  annotation: `exposeDependency`,
+
+  compose: false,
+
+  inputs: {
+    dependency: input.staticDependency({acceptsNull: true}),
+  },
+
+  steps: () => [
+    {
+      dependencies: [input('dependency')],
+      compute: ({
+        [input('dependency')]: dependency
+      }) => dependency,
+    },
+  ],
+});
+
+// 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.
+export const exposeConstant = templateCompositeFrom({
+  annotation: `exposeConstant`,
+
+  compose: false,
+
+  inputs: {
+    value: input.staticValue(),
+  },
+
+  steps: () => [
+    {
+      dependencies: [input('value')],
+      compute: ({
+        [input('value')]: value,
+      }) => value,
+    },
+  ],
+});
+
+// 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!
+//
+
+const inputAvailabilityCheckMode = () => input({
+  validate: is('null', 'empty', 'falsy'),
+  defaultValue: 'null',
+});
+
+export const withResultOfAvailabilityCheck = 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;
+        }
+
+        return continuation({'#availability': availability});
+      },
+    },
+  ],
+});
+
+// Exposes a dependency as it is, or continues if it's unavailable.
+// See withResultOfAvailabilityCheck for {mode} options!
+export const exposeDependencyOrContinue = 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()),
+    },
+  ],
+});
+
+// Exposes the update value of an {update: true} property as it is,
+// or continues if it's unavailable. See withResultOfAvailabilityCheck
+// for {mode} options! Also provide {validate} here to conveniently
+// set a custom validation check for this property's update value.
+export const exposeUpdateValueOrContinue = 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'),
+    }),
+  ],
+});
+
+// Early exits if a dependency isn't available.
+// See withResultOfAvailabilityCheck for {mode} options!
+export const exitWithoutDependency = 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)),
+    },
+  ],
+});
+
+// Early exits if this property's update value isn't available.
+// See withResultOfAvailabilityCheck for {mode} options!
+export const exitWithoutUpdateValue = templateCompositeFrom({
+  annotation: `exitWithoutUpdateValue`,
+
+  inputs: {
+    mode: inputAvailabilityCheckMode(),
+    value: input({defaultValue: null}),
+  },
+
+  steps: () => [
+    exitWithoutDependency({
+      dependency: input.updateValue(),
+      mode: input('mode'),
+      value: input('value'),
+    }),
+  ],
+});
+
+// Raises if a dependency isn't available.
+// See withResultOfAvailabilityCheck for {mode} options!
+export const raiseOutputWithoutDependency = 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)),
+    },
+  ],
+});
+
+// Raises if this property's update value isn't available.
+// See withResultOfAvailabilityCheck for {mode} options!
+export const raiseOutputWithoutUpdateValue = 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'),
+    }),
+
+    {
+      dependencies: ['#availability', input('output')],
+      compute: (continuation, {
+        ['#availability']: availability,
+        [input('output')]: output,
+      }) =>
+        (availability
+          ? continuation()
+          : continuation.raiseOutputAbove(output)),
+    },
+  ],
+});
+
+// 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.
+export const withPropertyFromObject = 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),
+      }),
+    },
+  ],
+});
+
+// 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.
+export const withPropertiesFromObject = 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),
+            })),
+    },
+  ],
+});
+
+// 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.
+export function withPropertyFromList({
+  list,
+  property,
+  into = null,
+}) {
+  into ??=
+    (list.startsWith('#')
+      ? `${list}.${property}`
+      : `#${list}.${property}`);
+
+  return {
+    annotation: `withPropertyFromList`,
+    flags: {expose: true, compose: true},
+
+    expose: {
+      mapDependencies: {list},
+      mapContinuation: {into},
+      options: {property},
+
+      compute(continuation, {list, '#options': {property}}) {
+        if (list === undefined || empty(list)) {
+          return continuation({into: []});
+        }
+
+        return continuation({
+          into:
+            list.map(item =>
+              (item === null || item === undefined
+                ? null
+                : item[property] ?? null)),
+        });
+      },
+    },
+  };
+}
+
+// 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.
+export const withPropertiesFromList = 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})),
+    },
+  ],
+});
+
+// Replaces items of a list, which are null or undefined, with some fallback
+// value. By default, this replaces the passed dependency.
+export const fillMissingListItems = 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,
+      }),
+    },
+  ],
+});
+
+// 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.
+export const excludeFromList = 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;
+          }),
+      }),
+    },
+  ],
+});
+
+// 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.
+export const withFlattenedList = 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,
+        });
+      },
+    },
+  ],
+});
+
+// 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).
+export const withUnflattenedList = 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/things/flash.js b/src/data/things/flash.js
index 6eb5234..c3f9026 100644
--- a/src/data/things/flash.js
+++ b/src/data/things/flash.js
@@ -1,25 +1,33 @@
+import {input} from '#composite';
 import find from '#find';
 
-import Thing from './thing.js';
+import {
+  isColor,
+  isDirectory,
+  isNumber,
+  isString,
+  oneOf,
+} from '#validators';
+
+import Thing, {
+  color,
+  contributionList,
+  fileExtension,
+  name,
+  referenceList,
+  simpleDate,
+  simpleString,
+  urls,
+  wikiData,
+} 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,39 +55,35 @@ export class Flash extends Thing {
       },
     },
 
-    date: Thing.common.simpleDate(),
+    date: simpleDate(),
 
-    coverArtFileExtension: Thing.common.fileExtension('jpg'),
+    coverArtFileExtension: fileExtension('jpg'),
 
-    contributorContribsByRef: Thing.common.contribsByRef(),
+    contributorContribs: contributionList(),
 
-    featuredTracksByRef: Thing.common.referenceList(Track),
+    featuredTracks: referenceList({
+      class: input.value(Track),
+      find: input.value(find.track),
+      data: 'trackData',
+    }),
 
-    urls: Thing.common.urls(),
+    urls: urls(),
 
     // Update only
 
-    artistData: Thing.common.wikiData(Artist),
-    trackData: Thing.common.wikiData(Track),
-    flashActData: Thing.common.wikiData(FlashAct),
+    artistData: wikiData(Artist),
+    trackData: wikiData(Track),
+    flashActData: wikiData(FlashAct),
 
     // Expose only
 
-    contributorContribs: Thing.common.dynamicContribs('contributorContribsByRef'),
-
-    featuredTracks: Thing.common.dynamicThingsFromReferenceList(
-      'featuredTracksByRef',
-      'trackData',
-      find.track
-    ),
-
     act: {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['flashActData'],
+        dependencies: ['this', 'flashActData'],
 
-        compute: ({flashActData, [Flash.instance]: flash}) =>
+        compute: ({this: flash, flashActData}) =>
           flashActData.find((act) => act.flashes.includes(flash)) ?? null,
       },
     },
@@ -88,9 +92,9 @@ export class Flash extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['flashActData'],
+        dependencies: ['this', 'flashActData'],
 
-        compute: ({flashActData, [Flash.instance]: flash}) =>
+        compute: ({this: flash, flashActData}) =>
           flashActData.find((act) => act.flashes.includes(flash))?.color ?? null,
       },
     },
@@ -111,17 +115,13 @@ export class Flash extends Thing {
 }
 
 export class FlashAct extends Thing {
-  static [Thing.getPropertyDescriptors] = ({
-    validators: {
-      isColor,
-    },
-  }) => ({
+  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'),
+    color: color(),
+    anchor: simpleString(),
+    jump: simpleString(),
 
     jumpColor: {
       flags: {update: true, expose: true},
@@ -133,18 +133,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..0b11780 100644
--- a/src/data/things/group.js
+++ b/src/data/things/group.js
@@ -1,33 +1,42 @@
+import {input} from '#composite';
 import find from '#find';
 
-import Thing from './thing.js';
+import Thing, {
+  color,
+  directory,
+  name,
+  referenceList,
+  simpleString,
+  urls,
+  wikiData,
+} 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 +50,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 +60,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 +71,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 +81,20 @@ export class Group extends Thing {
 }
 
 export class GroupCategory extends Thing {
-  static [Thing.getPropertyDescriptors] = ({
-    Group,
-  }) => ({
+  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..bcf99e8 100644
--- a/src/data/things/homepage-layout.js
+++ b/src/data/things/homepage-layout.js
@@ -1,20 +1,35 @@
 import find from '#find';
 
-import Thing from './thing.js';
+import {
+  exposeDependency,
+  input,
+} from '#composite';
+
+import {
+  is,
+  isCountingNumber,
+  isString,
+  isStringNonEmpty,
+  oneOf,
+  validateArrayItems,
+  validateInstanceOf,
+  validateReference,
+} from '#validators';
+
+import Thing, {
+  color,
+  name,
+  referenceList,
+  simpleString,
+  wikiData,
+  withResolvedReference,
+} from './thing.js';
 
 export class HomepageLayout extends Thing {
-  static [Thing.getPropertyDescriptors] = ({
-    HomepageLayoutRow,
-
-    validators: {
-      isStringNonEmpty,
-      validateArrayItems,
-      validateInstanceOf,
-    },
-  }) => ({
+  static [Thing.getPropertyDescriptors] = ({HomepageLayoutRow}) => ({
     // Update & expose
 
-    sidebarContent: Thing.common.simpleString(),
+    sidebarContent: simpleString(),
 
     navbarLinks: {
       flags: {update: true, expose: true},
@@ -32,13 +47,10 @@ export class HomepageLayout extends Thing {
 }
 
 export class HomepageLayoutRow extends Thing {
-  static [Thing.getPropertyDescriptors] = ({
-    Album,
-    Group,
-  }) => ({
+  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 +62,20 @@ 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.getPropertyDescriptors] = (opts, {Album, Group} = opts) => ({
     ...HomepageLayoutRow[Thing.getPropertyDescriptors](opts),
 
     // Update & expose
@@ -104,8 +106,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 +149,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..77e5fa7 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';
 
@@ -82,6 +82,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 +121,7 @@ function descriptorAggregateHelper({
 }
 
 function evaluatePropertyDescriptors() {
-  const opts = {...allClasses, validators};
+  const opts = {...allClasses};
 
   return descriptorAggregateHelper({
     message: `Errors evaluating Thing class property descriptors`,
@@ -129,8 +131,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 afa9f1e..a325d6a 100644
--- a/src/data/things/language.js
+++ b/src/data/things/language.js
@@ -1,16 +1,16 @@
-import Thing from './thing.js';
-
 import {Tag} from '#html';
 import {isLanguageCode} from '#validators';
 
 import CacheableObject from './cacheable-object.js';
 
+import Thing, {
+  externalFunction,
+  flag,
+  simpleString,
+} 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
@@ -23,7 +23,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
@@ -45,7 +45,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.
@@ -73,7 +73,7 @@ export class Language extends Thing {
 
     // Update only
 
-    escapeHTML: Thing.common.externalFunction({expose: true}),
+    escapeHTML: externalFunction(),
 
     // Expose only
 
@@ -192,7 +192,7 @@ export class Language extends Thing {
   // html.Tag objects, which are treated as sanitized by default (so that they
   // can be nested inside strings at all).
   #sanitizeStringArg(arg) {
-    const escapeHTML = this.escapeHTML;
+    const escapeHTML = CacheableObject.getUpdateValue(this, 'escapeHTML');
 
     if (!escapeHTML) {
       throw new Error(`escapeHTML unavailable`);
@@ -224,7 +224,7 @@ export class Language extends Thing {
   // contents of a slot directly, it should be manually sanitized with this
   // function first.
   sanitize(arg) {
-    const escapeHTML = this.escapeHTML;
+    const escapeHTML = CacheableObject.getUpdateValue(this, 'escapeHTML');
 
     if (!escapeHTML) {
       throw new Error(`escapeHTML unavailable`);
diff --git a/src/data/things/news-entry.js b/src/data/things/news-entry.js
index 4391141..6984874 100644
--- a/src/data/things/news-entry.js
+++ b/src/data/things/news-entry.js
@@ -1,4 +1,9 @@
-import Thing from './thing.js';
+import Thing, {
+  directory,
+  name,
+  simpleDate,
+  simpleString,
+} from './thing.js';
 
 export class NewsEntry extends Thing {
   static [Thing.referenceType] = 'news-entry';
@@ -6,11 +11,11 @@ export class NewsEntry extends Thing {
   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..0133e0b 100644
--- a/src/data/things/static-page.js
+++ b/src/data/things/static-page.js
@@ -1,16 +1,18 @@
-import Thing from './thing.js';
+import {isName} from '#validators';
+
+import Thing, {
+  directory,
+  name,
+  simpleString,
+} from './thing.js';
 
 export class StaticPage extends Thing {
   static [Thing.referenceType] = 'static';
 
-  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 +24,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 5705ee7..f1302e1 100644
--- a/src/data/things/thing.js
+++ b/src/data/things/thing.js
@@ -3,419 +3,747 @@
 
 import {inspect} from 'node:util';
 
-import {color} from '#cli';
+import {colors} from '#cli';
 import find from '#find';
-import {empty} from '#sugar';
-import {getKebabCase} from '#wiki-data';
+import {stitchArrays, unique} from '#sugar';
+import {filterMultipleArrays, getKebabCase} from '#wiki-data';
+import {is} from '#validators';
+
+import {
+  compositeFrom,
+  exitWithoutDependency,
+  exposeConstant,
+  exposeDependency,
+  exposeDependencyOrContinue,
+  input,
+  raiseOutputWithoutDependency,
+  templateCompositeFrom,
+  withResultOfAvailabilityCheck,
+  withPropertiesFromList,
+} from '#composite';
 
 import {
   isAdditionalFileList,
   isBoolean,
-  isCommentary,
   isColor,
+  isCommentary,
   isContributionList,
   isDate,
+  isDimensions,
   isDirectory,
+  isDuration,
   isFileExtension,
   isName,
   isString,
+  isType,
   isURL,
   validateArrayItems,
   validateInstanceOf,
   validateReference,
   validateReferenceList,
+  validateWikiData,
 } from '#validators';
 
 import CacheableObject from './cacheable-object.js';
 
 export default class Thing extends CacheableObject {
-  static referenceType = Symbol('Thing.referenceType');
+  static referenceType = Symbol.for('Thing.referenceType');
 
   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},
-    }),
+  // 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
+  // identifying the Thing being presented.
+  [inspect.custom]() {
+    const cname = this.constructor.name;
 
-    color: () => ({
-      flags: {update: true, expose: true},
-      update: {validate: isColor},
-    }),
+    return (
+      (this.name ? `${cname} ${colors.green(`"${this.name}"`)}` : `${cname}`) +
+      (this.directory ? ` (${colors.blue(Thing.getReference(this))})` : '')
+    );
+  }
 
-    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;
-        },
+  static getReference(thing) {
+    if (!thing.constructor[Thing.referenceType]) {
+      throw TypeError(`Passed Thing is ${thing.constructor.name}, which provides no [Thing.referenceType]`);
+    }
+
+    if (!thing.directory) {
+      throw TypeError(`Passed ${thing.constructor.name} is missing its directory`);
+    }
+
+    return `${thing.constructor[Thing.referenceType]}:${thing.directory}`;
+  }
+}
+
+// Property descriptor templates
+//
+// 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!
+
+export function name(defaultName) {
+  return {
+    flags: {update: true, expose: true},
+    update: {validate: isName, default: defaultName},
+  };
+}
+
+export function color() {
+  return {
+    flags: {update: true, expose: true},
+    update: {validate: isColor},
+  };
+}
+
+export function directory() {
+  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;
       },
-    }),
+    },
+  };
+}
 
-    urls: () => ({
-      flags: {update: true, expose: true},
-      update: {validate: validateArrayItems(isURL)},
-      expose: {transform: (value) => value ?? []},
-    }),
+export function urls() {
+  return {
+    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},
-    }),
+// A file extension! Or the default, if provided when calling this.
+export function fileExtension(defaultFileExtension = null) {
+  return {
+    flags: {update: true, expose: true},
+    update: {validate: isFileExtension},
+    expose: {transform: (value) => value ?? defaultFileExtension},
+  };
+}
+
+// Plain ol' image dimensions. This is a two-item array of positive integers,
+// corresponding to width and height respectively.
+export function dimensions() {
+  return {
+    flags: {update: true, expose: true},
+    update: {validate: isDimensions},
+  };
+}
+
+// Duration! This is a number of seconds, possibly floating point, always
+// at minimum zero.
+export function duration() {
+  return {
+    flags: {update: true, expose: true},
+    update: {validate: isDuration},
+  };
+}
+
+// Straightforward flag descriptor for a variety of property purposes.
+// Provide a default value, true or false!
+export function flag(defaultValue = false) {
+  // TODO:                        ^ Are you actually kidding me
+  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.
+export function simpleDate() {
+  return {
+    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.
+export function simpleString() {
+  return {
+    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.
+export function externalFunction() {
+  return {
+    flags: {update: true},
+    update: {validate: (t) => typeof t === 'function'},
+  };
+}
+
+// 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!
+//
+export function contributionList() {
+  return compositeFrom({
+    annotation: `contributionList`,
+
+    compose: false,
+
+    update: {validate: isContributionList},
+
+    steps: [
+      withResolvedContribs({from: input.updateValue()}),
+      exposeDependencyOrContinue({dependency: '#resolvedContribs'}),
+      exposeConstant({value: input.value([])}),
+    ],
+  });
+}
 
-    // 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},
-      };
+// Artist commentary! Generally present on tracks and albums.
+export function commentary() {
+  return {
+    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: [...]},
+//         ...
+//     ]
+//
+export function additionalFiles() {
+  return {
+    flags: {update: true, expose: true},
+    update: {validate: isAdditionalFileList},
+    expose: {
+      transform: (additionalFiles) =>
+        additionalFiles ?? [],
     },
+  };
+}
 
-    // 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},
-    }),
+const thingClassInput = {
+  validate(thingClass) {
+    isType(thingClass, 'function');
+
+    if (!Object.hasOwn(thingClass, Thing.referenceType)) {
+      throw new TypeError(`Expected a Thing constructor, missing Thing.referenceType`);
+    }
 
-    // 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},
+    return true;
+  },
+};
+
+// 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.
+export const referenceList = templateCompositeFrom({
+  annotation: `referenceList`,
+
+  compose: false,
+
+  inputs: {
+    class: input.staticValue(thingClassInput),
+
+    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'),
     }),
 
-    // External function. These should only be used as dependencies for other
-    // properties, so they're left unexposed.
-    externalFunction: ({expose = false} = {}) => ({
-      flags: {update: true, expose},
-      update: {validate: (t) => typeof t === 'function'},
+    exposeDependency({dependency: '#resolvedReferenceList'}),
+  ],
+});
+
+// Corresponding function for a single reference.
+export const singleReference = templateCompositeFrom({
+  annotation: `singleReference`,
+
+  compose: false,
+
+  inputs: {
+    class: input(thingClassInput),
+    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'),
     }),
 
-    // 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},
+    exposeDependency({dependency: '#resolvedReference'}),
+  ],
+});
+
+// Nice 'n simple shorthand for an exposed-only flag which is true when any
+// contributions are present in the specified property.
+export const contribsPresent = templateCompositeFrom({
+  annotation: `contribsPresent`,
+
+  compose: false,
+
+  inputs: {
+    contribs: input.staticDependency({
+      validate: isContributionList,
+      acceptsNull: true,
     }),
+  },
 
-    // Artist commentary! Generally present on tracks and albums.
-    commentary: () => ({
-      flags: {update: true, expose: true},
-      update: {validate: isCommentary},
+  steps: () => [
+    withResultOfAvailabilityCheck({
+      from: input('contribs'),
+      mode: input.value('empty'),
     }),
 
-    // 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 ?? [],
-      },
+    exposeDependency({dependency: '#availability'}),
+  ],
+});
+
+// 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.
+export const reverseReferenceList = templateCompositeFrom({
+  annotation: `reverseReferenceList`,
+
+  compose: false,
+
+  inputs: {
+    data: inputWikiData({allowMixedTypes: false}),
+    list: input({type: 'string'}),
+  },
+
+  steps: () => [
+    withReverseReferenceList({
+      data: input('data'),
+      list: input('list'),
     }),
 
-    // 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)},
-      };
+    exposeDependency({dependency: '#reverseReferenceList'}),
+  ],
+});
+
+// General purpose wiki data constructor, for properties like artistData,
+// trackData, etc.
+export function wikiData(thingClass) {
+  return {
+    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.
+export const commentatorArtists = templateCompositeFrom({
+  annotation: `commentatorArtists`,
+
+  compose: false,
+
+  steps: () => [
+    exitWithoutDependency({
+      dependency: 'commentary',
+      mode: input.value('falsy'),
+      value: input.value([]),
+    }),
 
-    // 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)},
-      };
+    {
+      dependencies: ['commentary'],
+      compute: (continuation, {commentary}) =>
+        continuation({
+          '#artistRefs':
+            Array.from(
+              commentary
+                .replace(/<\/?b>/g, '')
+                .matchAll(/<i>(?<who>.*?):<\/i>/g))
+              .map(({groups: {who}}) => who),
+        }),
     },
 
-    // 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
-    ) => ({
+    withResolvedReferenceList({
+      list: '#artistRefs',
+      data: 'artistData',
+      find: input.value(find.artist),
+    }).outputs({
+      '#resolvedReferenceList': '#artists',
+    }),
+
+    {
       flags: {expose: true},
 
       expose: {
-        dependencies: [referenceListProperty, thingDataProperty],
-        compute: ({
-          [referenceListProperty]: refs,
-          [thingDataProperty]: thingData,
-        }) =>
-          refs && thingData
-            ? refs
-                .map((ref) => findFn(ref, thingData, {mode: 'quiet'}))
-                .filter(Boolean)
-            : [],
+        dependencies: ['#artists'],
+        compute: ({'#artists': artists}) =>
+          unique(artists),
       },
+    },
+  ],
+});
+
+// Compositional utilities
+
+// 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 function inputWikiData({
+  referenceType = '',
+  allowMixedTypes = false,
+} = {}) {
+  return input({
+    validate: validateWikiData({referenceType, allowMixedTypes}),
+    acceptsNull: true,
+  });
+}
+
+// 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.
+export const withResolvedContribs = templateCompositeFrom({
+  annotation: `withResolvedContribs`,
+
+  inputs: {
+    from: input({
+      validate: isContributionList,
+      acceptsNull: true,
     }),
 
-    // Corresponding function for a single reference.
-    dynamicThingFromSingleReference: (
-      singleReferenceProperty,
-      thingDataProperty,
-      findFn
-    ) => ({
-      flags: {expose: true},
+    notFoundMode: input({
+      validate: is('exit', 'filter', 'null'),
+      defaultValue: 'null',
+    }),
+  },
 
-      expose: {
-        dependencies: [singleReferenceProperty, thingDataProperty],
-        compute: ({
-          [singleReferenceProperty]: ref,
-          [thingDataProperty]: thingData,
-        }) => (ref && thingData ? findFn(ref, thingData, {mode: 'quiet'}) : null),
-      },
+  outputs: ['#resolvedContribs'],
+
+  steps: () => [
+    raiseOutputWithoutDependency({
+      dependency: input('from'),
+      mode: input.value('empty'),
+      output: input.value({
+        ['#resolvedContribs']: [],
+      }),
     }),
 
-    // 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)
-            : [],
-      },
+    withPropertiesFromList({
+      list: input('from'),
+      properties: input.value(['who', 'what']),
+      prefix: input.value('#contribs'),
     }),
 
-    // 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);
-        },
+    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}),
+        });
       },
+    },
+  ],
+});
+
+// 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.
+export const exitWithoutContribs = templateCompositeFrom({
+  annotation: `exitWithoutContribs`,
+
+  inputs: {
+    contribs: input({
+      validate: isContributionList,
+      acceptsNull: true,
     }),
 
-    // 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);
-        },
-      }
+    value: input({defaultValue: null}),
+  },
+
+  steps: () => [
+    withResolvedContribs({
+      from: input('contribs'),
     }),
 
-    // 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},
+    withResultOfAvailabilityCheck({
+      from: '#resolvedContribs',
+      mode: input.value('empty'),
+    }),
 
-      expose: {
-        dependencies: [thingDataProperty],
+    {
+      dependencies: ['#availability', input('value')],
+      compute: (continuation, {
+        ['#availability']: availability,
+        [input('value')]: value,
+      }) =>
+        (availability
+          ? continuation()
+          : continuation.exit(value)),
+    },
+  ],
+});
+
+// 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.
+export const withResolvedReference = 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,
+      }),
+    }),
 
-        compute: ({[thingDataProperty]: thingData, [Thing.instance]: thing}) =>
-          thingData?.filter(t => t[referencerRefListProperty].includes(thing)) ?? [],
+    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,
+        });
       },
+    },
+  ],
+});
+
+// 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').
+export const withResolvedReferenceList = templateCompositeFrom({
+  annotation: `withResolvedReferenceList`,
+
+  inputs: {
+    list: input({
+      validate: validateArrayItems(isString),
+      acceptsNull: true,
     }),
 
-    // 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},
+    data: inputWikiData({allowMixedTypes: false}),
+    find: input({type: 'function'}),
 
-      expose: {
-        dependencies: [thingDataProperty],
+    notFoundMode: input({
+      validate: is('exit', 'filter', 'null'),
+      defaultValue: 'filter',
+    }),
+  },
 
-        compute: ({[thingDataProperty]: thingData, [Thing.instance]: thing}) =>
-          thingData?.filter((t) => t[referencerRefListProperty] === thing) ?? [],
-      },
+  outputs: ['#resolvedReferenceList'],
+
+  steps: () => [
+    exitWithoutDependency({
+      dependency: input('data'),
+      value: input.value([]),
     }),
 
-    // General purpose wiki data constructor, for properties like artistData,
-    // trackData, etc.
-    wikiData: (thingClass) => ({
-      flags: {update: true},
-      update: {
-        validate: validateArrayItems(validateInstanceOf(thingClass)),
-      },
+    raiseOutputWithoutDependency({
+      dependency: input('list'),
+      mode: input.value('empty'),
+      output: input.value({
+        ['#resolvedReferenceList']: [],
+      }),
     }),
 
-    // 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},
+    {
+      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'})),
+        }),
+    },
 
-      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'})
-                  )
-                )
-              )
-            : [],
+    {
+      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`);
+        }
       },
-    }),
-  };
+    },
+  ],
+});
 
-  // 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
-  // identifying the Thing being presented.
-  [inspect.custom]() {
-    const cname = this.constructor.name;
+// Check out the info on reverseReferenceList!
+// This is its composable form.
+export const withReverseReferenceList = templateCompositeFrom({
+  annotation: `withReverseReferenceList`,
 
-    return (
-      (this.name ? `${cname} ${color.green(`"${this.name}"`)}` : `${cname}`) +
-      (this.directory ? ` (${color.blue(Thing.getReference(this))})` : '')
-    );
-  }
+  inputs: {
+    data: inputWikiData({allowMixedTypes: false}),
+    list: input({type: 'string'}),
+  },
 
-  static getReference(thing) {
-    if (!thing.constructor[Thing.referenceType]) {
-      throw TypeError(`Passed Thing is ${thing.constructor.name}, which provides no [Thing.referenceType]`);
-    }
+  outputs: ['#reverseReferenceList'],
 
-    if (!thing.directory) {
-      throw TypeError(`Passed ${thing.constructor.name} is missing its directory`);
-    }
+  steps: () => [
+    exitWithoutDependency({
+      dependency: input('data'),
+      value: input.value([]),
+    }),
 
-    return `${thing.constructor[Thing.referenceType]}:${thing.directory}`;
-  }
-}
+    {
+      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/things/track.js b/src/data/things/track.js
index 14510d9..c77bf88 100644
--- a/src/data/things/track.js
+++ b/src/data/things/track.js
@@ -1,330 +1,344 @@
 import {inspect} from 'node:util';
 
-import {color} from '#cli';
+import {colors} from '#cli';
 import find from '#find';
 import {empty} from '#sugar';
 
-import Thing from './thing.js';
+import {
+  exitWithoutDependency,
+  excludeFromList,
+  exposeConstant,
+  exposeDependency,
+  exposeDependencyOrContinue,
+  exposeUpdateValueOrContinue,
+  input,
+  raiseOutputWithoutDependency,
+  templateCompositeFrom,
+  withPropertyFromObject,
+} from '#composite';
+
+import {
+  is,
+  isBoolean,
+  isColor,
+  isContributionList,
+  isDate,
+  isFileExtension,
+  validateWikiData,
+} from '#validators';
+
+import CacheableObject from './cacheable-object.js';
+
+import Thing, {
+  additionalFiles,
+  commentary,
+  commentatorArtists,
+  contributionList,
+  directory,
+  duration,
+  flag,
+  name,
+  referenceList,
+  reverseReferenceList,
+  simpleDate,
+  singleReference,
+  simpleString,
+  urls,
+  wikiData,
+  withResolvedContribs,
+  withResolvedReference,
+  withReverseReferenceList,
+} 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(),
-
-    // 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.
-    alwaysReferenceByDirectory: {
-      flags: {update: true, expose: true},
-
-      // Deliberately defaults to null - this will fall back to false in most
-      // cases.
-      update: {validate: isBoolean, default: null},
+    name: name('Unnamed Track'),
+    directory: directory(),
 
-      expose: {
-        dependencies: ['name', 'originalReleaseTrackByRef', 'trackData'],
+    duration: duration(),
+    urls: urls(),
+    dateFirstReleased: simpleDate(),
 
-        transform(value, {
-          name,
-          originalReleaseTrackByRef,
-          trackData,
-          [Track.instance]: thisTrack,
-        }) {
-          if (value !== null) return value;
+    color: [
+      exposeUpdateValueOrContinue({
+        validate: input.value(isColor),
+      }),
 
-          const original =
-            find.track(
-              originalReleaseTrackByRef,
-              trackData.filter(track => track !== thisTrack),
-              {quiet: true});
+      withContainingTrackSection(),
 
-          if (!original) return false;
+      withPropertyFromObject({
+        object: '#trackSection',
+        property: input.value('color'),
+      }),
 
-          return name === original.name;
-        }
-      },
-    },
+      exposeDependencyOrContinue({dependency: '#trackSection.color'}),
 
-    artistContribsByRef: Thing.common.contribsByRef(),
-    contributorContribsByRef: Thing.common.contribsByRef(),
-    coverArtistContribsByRef: Thing.common.contribsByRef(),
+      withPropertyFromAlbum({
+        property: input.value('color'),
+      }),
 
-    referencedTracksByRef: Thing.common.referenceList(Track),
-    sampledTracksByRef: Thing.common.referenceList(Track),
-    artTagsByRef: Thing.common.referenceList(ArtTag),
+      exposeDependency({dependency: '#album.color'}),
+    ],
 
-    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',
+    // 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.
+    alwaysReferenceByDirectory: [
+      exposeUpdateValueOrContinue({
+        validate: input.value(isBoolean),
+      }),
+
+      excludeFromList({
+        list: 'trackData',
+        item: input.myself(),
+      }),
+
+      withOriginalRelease({
+        data: '#trackData',
+      }),
+
+      exitWithoutDependency({
+        dependency: '#originalRelease',
+        value: input.value(false),
+      }),
+
+      withPropertyFromObject({
+        object: '#originalRelease',
+        property: input.value('name'),
+      }),
+
+      {
+        dependencies: ['name', '#originalRelease.name'],
+        compute: ({name, '#originalRelease.name': originalName}) =>
+          name === originalName,
       },
-    },
-
-    originalReleaseTrackByRef: Thing.common.singleReference(Track),
-
-    dataSourceAlbumByRef: Thing.common.singleReference(Album),
+    ],
+
+    // 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'}),
+
+      withPropertyFromAlbum({
+        property: input.value('artistContribs'),
+      }),
+
+      exposeDependency({dependency: '#album.artistContribs'}),
+    ],
+
+    contributorContribs: [
+      inheritFromOriginalRelease({
+        property: input.value('contributorContribs'),
+      }),
+
+      contributionList(),
+    ],
 
-    commentary: Thing.common.commentary(),
-    lyrics: Thing.common.simpleString(),
-    additionalFiles: Thing.common.additionalFiles(),
-    sheetMusicFiles: Thing.common.additionalFiles(),
-    midiProjectFiles: Thing.common.additionalFiles(),
+    // 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(),
+
+      withResolvedContribs({
+        from: input.updateValue({validate: isContributionList}),
+      }).outputs({
+        '#resolvedContribs': '#coverArtistContribs',
+      }),
+
+      exposeDependencyOrContinue({dependency: '#coverArtistContribs'}),
+
+      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);
+    commentatorArtists: commentatorArtists(),
+
+    album: [
+      withAlbum(),
+      exposeDependency({dependency: '#album'}),
+    ],
+
+    date: [
+      exposeDependencyOrContinue({dependency: 'dateFirstReleased'}),
+
+      withPropertyFromAlbum({
+        property: input.value('date'),
+      }),
+
+      exposeDependency({dependency: '#album.date'}),
+    ],
+
+    // 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.)
+    hasUniqueCoverArt: [
+      withHasUniqueCoverArt(),
+      exposeDependency({dependency: '#hasUniqueCoverArt'}),
+    ],
+
+    otherReleases: [
+      exitWithoutDependency({
+        dependency: 'trackData',
+        mode: input.value('empty'),
+      }),
+
+      withOriginalRelease({
+        selfIfOriginal: input.value(true),
+      }),
+
+      {
+        flags: {expose: true},
+        expose: {
+          dependencies: [input.myself(), '#originalRelease', 'trackData'],
+          compute: ({
+            [input.myself()]: thisTrack,
+            ['#originalRelease']: originalRelease,
+            trackData,
+          }) =>
+            (originalRelease === thisTrack
+              ? []
+              : [originalRelease])
+              .concat(trackData.filter(track =>
+                track !== originalRelease &&
+                track !== thisTrack &&
+                track.originalReleaseTrack === originalRelease)),
         },
       },
-    },
-
-    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
@@ -334,162 +348,382 @@ export class Track extends Thing {
     // 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))
-            : [],
-      },
-    },
+    referencedByTracks: trackReverseReferenceList({
+      list: input.value('referencedTracks'),
+    }),
 
     // 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))
-            : [],
-      },
-    },
+    sampledByTracks: trackReverseReferenceList({
+      list: input.value('sampledTracks'),
+    }),
+
+    featuredInFlashes: reverseReferenceList({
+      data: 'flashData',
+      list: input.value('featuredTracks'),
+    }),
+  });
 
-    featuredInFlashes: Thing.common.reverseReferenceList(
-      'flashData',
-      'featuredTracks'
-    ),
+  [inspect.custom](depth) {
+    const parts = [];
 
-    artTags: Thing.common.dynamicThingsFromReferenceList(
-      'artTagsByRef',
-      'artTagData',
-      find.artTag
-    ),
-  });
+    parts.push(Thing.prototype[inspect.custom].apply(this));
 
-  // 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;
+    if (CacheableObject.getUpdateValue(this, 'originalReleaseTrack')) {
+      parts.unshift(`${colors.yellow('[rerelease]')} `);
     }
 
-    const album = Track.findAlbum(track, albumData);
-    if (album && !empty(album.trackCoverArtistContribsByRef)) {
-      return true;
+    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)})`);
     }
 
-    return false;
+    return parts.join('');
   }
+}
 
-  static hasUniqueCoverArt(
-    track,
-    albumData,
-    coverArtistContribsByRef,
-    hasCoverArt
-  ) {
-    if (!empty(coverArtistContribsByRef)) {
-      return true;
-    }
-
-    if (hasCoverArt === false) {
-      return false;
-    }
-
-    const album = Track.findAlbum(track, albumData);
-    if (album && !empty(album.trackCoverArtistContribsByRef)) {
-      return true;
-    }
+// 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.
+export const inheritFromOriginalRelease = templateCompositeFrom({
+  annotation: `Track.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);
+      },
+    },
+  ],
+});
+
+// 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.
+export const withAlbum = templateCompositeFrom({
+  annotation: `Track.withAlbum`,
+
+  inputs: {
+    notFoundMode: input({
+      validate: is('exit', 'null'),
+      defaultValue: 'null',
+    }),
+  },
+
+  outputs: ['#album'],
+
+  steps: () => [
+    raiseOutputWithoutDependency({
+      dependency: 'albumData',
+      mode: input.value('empty'),
+      output: input.value({
+        ['#album']: null,
+      }),
+    }),
+
+    {
+      dependencies: [input.myself(), 'albumData'],
+      compute: (continuation, {
+        [input.myself()]: track,
+        ['albumData']: albumData,
+      }) =>
+        continuation({
+          ['#album']:
+            albumData.find(album => album.tracks.includes(track)),
+        }),
+    },
 
-    return false;
-  }
+    raiseOutputWithoutDependency({
+      dependency: '#album',
+      output: input.value({
+        ['#album']: null,
+      }),
+    }),
+
+    {
+      dependencies: ['#album'],
+      compute: (continuation, {'#album': album}) =>
+        continuation.raiseOutput({'#album': album}),
+    },
+  ],
+});
+
+// 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.
+export const withPropertyFromAlbum = 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,
+      }),
+    },
+  ],
+});
+
+// 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.
+export const withContainingTrackSection = 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,
+          });
+        }
 
-  static inheritFromOriginalRelease(
-    originalProperty,
-    originalMissingValue,
-    ownPropertyDescriptor
-  ) {
-    return {
-      flags: {expose: true},
+        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,
+          });
+        }
+      },
+    },
+  ],
+});
+
+// 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.
+export const withOriginalRelease = 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)),
+        }),
+    },
+  ],
+});
+
+// The algorithm for checking if a track has unique cover art is used in a
+// couple places, so it's defined in full as a compositional step.
+export const withHasUniqueCoverArt = templateCompositeFrom({
+  annotation: 'withHasUniqueCoverArt',
+
+  outputs: ['#hasUniqueCoverArt'],
+
+  steps: () => [
+    {
+      dependencies: ['disableUniqueCoverArt'],
+      compute: (continuation, {disableUniqueCoverArt}) =>
+        (disableUniqueCoverArt
+          ? continuation.raiseOutput({
+              ['#hasUniqueCoverArt']: false,
+            })
+          : continuation()),
+    },
 
-      expose: {
-        dependencies: [
-          ...ownPropertyDescriptor.expose.dependencies,
-          'originalReleaseTrackByRef',
-          'trackData',
-        ],
-
-        compute(dependencies) {
-          const {
-            originalReleaseTrackByRef,
-            trackData,
-          } = dependencies;
+    withResolvedContribs({from: 'coverArtistContribs'}),
+
+    {
+      dependencies: ['#resolvedContribs'],
+      compute: (continuation, {
+        ['#resolvedContribs']: contribsFromTrack,
+      }) =>
+        (empty(contribsFromTrack)
+          ? continuation()
+          : continuation.raiseOutput({
+              ['#hasUniqueCoverArt']: true,
+            })),
+    },
 
-          if (originalReleaseTrackByRef) {
-            if (!trackData) return originalMissingValue;
-            const original = find.track(originalReleaseTrackByRef, trackData, {mode: 'quiet'});
-            if (!original) return originalMissingValue;
-            return original[originalProperty];
-          }
+    withPropertyFromAlbum({
+      property: input.value('trackCoverArtistContribs'),
+    }),
+
+    {
+      dependencies: ['#album.trackCoverArtistContribs'],
+      compute: (continuation, {
+        ['#album.trackCoverArtistContribs']: contribsFromAlbum,
+      }) =>
+        continuation.raiseOutput({
+          ['#hasUniqueCoverArt']:
+            !empty(contribsFromAlbum),
+        }),
+    },
+  ],
+});
 
-          return ownPropertyDescriptor.expose.compute(dependencies);
-        },
-      },
-    };
-  }
+// Shorthand for checking if the track has unique cover art and exposing a
+// fallback value if it isn't.
+export const exitWithoutUniqueCoverArt = templateCompositeFrom({
+  annotation: `exitWithoutUniqueCoverArt`,
 
-  [inspect.custom]() {
-    const base = Thing.prototype[inspect.custom].apply(this);
+  inputs: {
+    value: input({defaultValue: null}),
+  },
 
-    const rereleasePart =
-      (this.originalReleaseTrackByRef
-        ? `${color.yellow('[rerelease]')} `
-        : ``);
+  steps: () => [
+    withHasUniqueCoverArt(),
 
-    const {album, dataSourceAlbum} = this;
+    exitWithoutDependency({
+      dependency: '#hasUniqueCoverArt',
+      mode: input.value('falsy'),
+      value: input('value'),
+    }),
+  ],
+});
 
-    const albumName =
-      (album
-        ? album.name
-        : dataSourceAlbum?.name);
+export const trackReverseReferenceList = templateCompositeFrom({
+  annotation: `trackReverseReferenceList`,
 
-    const albumIndex =
-      albumName &&
-        (album
-          ? album.tracks.indexOf(this)
-          : dataSourceAlbum.tracks.indexOf(this));
+  compose: false,
 
-    const trackNum =
-      albumName &&
-        (albumIndex === -1
-          ? '#?'
-          : `#${albumIndex + 1}`);
+  inputs: {
+    list: input({type: 'string'}),
+  },
 
-    const albumPart =
-      albumName
-        ? ` (${color.yellow(trackNum)} in ${color.green(albumName)})`
-        : ``;
+  steps: () => [
+    withReverseReferenceList({
+      data: 'trackData',
+      list: input('list'),
+    }),
 
-    return rereleasePart + base + albumPart;
-  }
-}
+    {
+      flags: {expose: true},
+      expose: {
+        dependencies: ['#reverseReferenceList'],
+        compute: ({
+          ['#reverseReferenceList']: reverseReferenceList,
+        }) =>
+          reverseReferenceList.filter(track => !track.originalReleaseTrack),
+      },
+    },
+  ],
+});
diff --git a/src/data/things/validators.js b/src/data/things/validators.js
index 5748eac..bdb2205 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.yellow(`#${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,62 @@ export function validateReferenceList(type = '') {
   return validateArrayItems(validateReference(type));
 }
 
+export function validateWikiData({
+  referenceType = '',
+  allowMixedTypes = false,
+}) {
+  if (referenceType && allowMixedTypes) {
+    throw new TypeError(`Don't specify both referenceType and allowMixedTypes`);
+  }
+
+  const isArrayOfObjects = validateArrayItems(isObject);
+
+  return (array) => {
+    isArrayOfObjects(array);
+
+    if (empty(array)) {
+      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) {
+        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]}`)
+    }
+
+    return true;
+  };
+}
+
 // Compositional utilities
 
 export function oneOf(...checks) {
diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js
index e906cab..c764b52 100644
--- a/src/data/things/wiki-info.js
+++ b/src/data/things/wiki-info.js
@@ -1,20 +1,21 @@
+import {input} from '#composite';
 import find from '#find';
+import {isLanguageCode, isName, isURL} from '#validators';
 
-import Thing from './thing.js';
+import Thing, {
+  color,
+  flag,
+  name,
+  referenceList,
+  simpleString,
+  wikiData,
+} from './thing.js';
 
 export class WikiInfo extends Thing {
-  static [Thing.getPropertyDescriptors] = ({
-    Group,
-
-    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 +28,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 +45,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 07e0a3d..c799be5 100644
--- a/src/data/yaml.js
+++ b/src/data/yaml.js
@@ -7,10 +7,10 @@ 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, Thing} from '#things';
 
 import {
   conditionallySuppressError,
@@ -137,7 +137,7 @@ 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;
       }
     };
@@ -195,7 +195,7 @@ function makeProcessDocument(
 
     const thing = Reflect.construct(thingClass, []);
 
-    withAggregate({message: `Errors applying ${color.green(thingClass.name)} properties`}, ({call}) => {
+    withAggregate({message: `Errors applying ${colors.green(thingClass.name)} properties`}, ({call}) => {
       for (const [property, value] of Object.entries(sourceProperties)) {
         call(() => (thing[property] = value));
       }
@@ -228,7 +228,7 @@ makeProcessDocument.FieldCombinationsError = class FieldCombinationsError extend
 makeProcessDocument.FieldCombinationError = 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 combinePart = `Don't combine ${fieldNames.map(field => colors.red(field)).join(', ')}`;
 
     const messagePart =
       (typeof message === 'function'
@@ -278,11 +278,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 +290,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 +316,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 +340,7 @@ 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',
 
@@ -346,13 +350,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: [
@@ -422,8 +426,8 @@ export const processFlashDocument = makeProcessDocument(T.Flash, {
     date: 'Date',
     coverArtFileExtension: 'Cover Art File Extension',
 
-    featuredTracksByRef: 'Featured Tracks',
-    contributorContribsByRef: 'Contributors',
+    featuredTracks: 'Featured Tracks',
+    contributorContribs: 'Contributors',
   },
 });
 
@@ -468,7 +472,7 @@ export const processGroupDocument = makeProcessDocument(T.Group, {
     description: 'Description',
     urls: 'URLs',
 
-    featuredAlbumsByRef: 'Featured Albums',
+    featuredAlbums: 'Featured Albums',
   },
 });
 
@@ -499,7 +503,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',
@@ -534,9 +538,9 @@ export const homepageLayoutRowTypeProcessMapping = {
   albums: makeProcessHomepageLayoutRowDocument(T.HomepageLayoutAlbumsRow, {
     propertyFieldMapping: {
       displayStyle: 'Display Style',
-      sourceGroupByRef: 'Group',
+      sourceGroup: 'Group',
       countAlbumsFromGroup: 'Count',
-      sourceAlbumsByRef: 'Albums',
+      sourceAlbums: 'Albums',
       actionLinks: 'Actions',
     },
   }),
@@ -769,13 +773,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);
           }
         };
@@ -789,7 +793,7 @@ export const dataSteps = [
               color: entry.color,
               dateOriginallyReleased: entry.dateOriginallyReleased,
               isDefaultTrackSection: false,
-              tracksByRef: [],
+              tracks: [],
             };
 
             continue;
@@ -797,9 +801,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();
@@ -823,12 +827,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;
         }) ?? [];
@@ -852,7 +856,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`);
@@ -861,18 +865,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);
@@ -895,7 +899,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`);
@@ -904,18 +908,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);
@@ -1007,7 +1011,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;
       }
     };
@@ -1030,7 +1034,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 =
@@ -1074,10 +1078,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) {
@@ -1087,7 +1091,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('')));
@@ -1318,13 +1322,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];
       }
     }
@@ -1342,7 +1360,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) {
@@ -1379,7 +1397,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 = [];
 
@@ -1405,7 +1423,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')
           );
         });
@@ -1446,45 +1464,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',
     }],
   ];
 
@@ -1500,7 +1518,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) {
@@ -1516,10 +1534,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;
             }
 
@@ -1536,23 +1554,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 =
@@ -1562,12 +1591,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;
@@ -1580,7 +1609,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
@@ -1598,13 +1627,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/find.js b/src/find.js
index 966629e..66f705e 100644
--- a/src/find.js
+++ b/src/find.js
@@ -1,6 +1,6 @@
 import {inspect} from 'node:util';
 
-import {color, logWarn} from '#cli';
+import {colors, logWarn} from '#cli';
 
 function warnOrThrow(mode, message) {
   if (mode === 'error') {
@@ -66,7 +66,7 @@ function findHelper(keys, findFns = {}) {
     const found = key ? byDirectory(ref, data, mode) : byName(ref, data, mode);
 
     if (!found) {
-      warnOrThrow(mode, `Didn't match anything for ${color.bright(fullRef)}`);
+      warnOrThrow(mode, `Didn't match anything for ${colors.bright(fullRef)}`);
     }
 
     cacheForThisData[fullRef] = found;
@@ -103,7 +103,7 @@ function matchName(ref, data, mode) {
 
   if (ref !== thing.name) {
     warnOrThrow(mode,
-      `Bad capitalization: ${color.red(ref)} -> ${color.green(thing.name)}`);
+      `Bad capitalization: ${colors.red(ref)} -> ${colors.green(thing.name)}`);
   }
 
   return thing;
diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js
index 34eed9c..3d441bc 100644
--- a/src/gen-thumbs.js
+++ b/src/gen-thumbs.js
@@ -93,8 +93,11 @@ 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,
@@ -110,8 +113,6 @@ import {
   traverse,
 } from '#node-utils';
 
-import {delay, empty, queue} from '#sugar';
-
 export const defaultMagickThreads = 8;
 
 export function getThumbnailsAvailableForDimensions([width, height]) {
@@ -629,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),
 
@@ -683,14 +684,14 @@ export async function verifyImagePaths(mediaPath, {urls, wikiData}) {
   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);
     }
   }
 
diff --git a/src/listing-spec.js b/src/listing-spec.js
index fe36fc0..2b33744 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/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/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/upd8.js b/src/upd8.js
index df172e4..2cc8f55 100755
--- a/src/upd8.js
+++ b/src/upd8.js
@@ -38,6 +38,7 @@ 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';
@@ -47,7 +48,7 @@ import {generateURLs, urlSpec} from '#urls';
 import {sortByName} from '#wiki-data';
 
 import {
-  color,
+  colors,
   decorateTime,
   logWarn,
   logInfo,
@@ -279,7 +280,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
@@ -310,13 +311,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);
@@ -336,7 +337,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,
@@ -496,7 +497,7 @@ async function main() {
 
   {
     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');
@@ -625,6 +626,11 @@ async function main() {
       .map(thing => () => CacheableObject.cacheAllExposedProperties(thing)));
   }
 
+  if (noBuild) {
+    displayCompositeCacheAnalysis();
+    if (precacheData) return;
+  }
+
   const internalDefaultLanguage = await processLanguageFile(
     path.join(__dirname, DEFAULT_STRINGS_FILE));
 
@@ -755,7 +761,9 @@ async function main() {
 
   logInfo`Done preloading filesizes!`;
 
-  if (noBuild) return;
+  if (noBuild) {
+    return;
+  }
 
   const developersComment =
     `<!--\n` + [
diff --git a/src/util/cli.js b/src/util/cli.js
index e8c8c79..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'),
@@ -335,8 +335,8 @@ export function fileIssue({
   topMessage = `This shouldn't happen.`,
 } = {}) {
   if (topMessage) {
-    console.error(color.red(`${topMessage} Please let the HSMusic developers know:`));
+    console.error(colors.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/`));
+  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 f0c7bfd..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) {
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 5b1f319..2e724ba 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,24 @@ 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) {
+  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 = {};
+
+  for (const property of properties) {
+    if (Object.hasOwn(object, property)) {
+      filteredObject[property] = object[property];
+    }
+  }
+
+  return filteredObject;
 }
 
 export function queue(array, max = 50) {
@@ -218,6 +230,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 +554,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,38 +572,44 @@ 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 =
-    (topError instanceof AggregateError
-      ? recursive(topError, {level: 0})
-      : (showTraces
-          ? topError.stack
-          : topError.toString()));
+  const message = recursive(topError, {level: 0});
 
   if (print) {
     console.error(message);
@@ -593,7 +623,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..ac652b2 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
 
@@ -874,3 +874,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/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js
index 730686d..3986de3 100644
--- a/src/write/build-modes/live-dev-server.js
+++ b/src/write/build-modes/live-dev-server.js
@@ -10,11 +10,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';
@@ -333,13 +331,6 @@ export async function go({
         return;
       }
 
-      const localizedPathnames = getPagePathnameAcrossLanguages({
-        defaultLanguage,
-        languages,
-        pagePath: servePath,
-        urls,
-      });
-
       const bound = bindUtilities({
         absoluteTo,
         cachebust,
diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js
index 79c8def..0931699 100644
--- a/src/write/build-modes/static-build.js
+++ b/src/write/build-modes/static-build.js
@@ -21,13 +21,11 @@ import {
   logError,
   logInfo,
   logWarn,
-  progressCallAll,
   progressPromiseAll,
 } from '#cli';
 
 import {
   getPagePathname,
-  getPagePathnameAcrossLanguages,
   getURLsFrom,
   getURLsFromRoot,
 } from '#urls';
@@ -94,7 +92,6 @@ export async function go({
   srcRootPath,
   thumbsCache,
   urls,
-  urlSpec,
   wikiData,
 
   cachebust,
@@ -271,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,
@@ -480,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/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/unit/data/things/cacheable-object.js b/test/unit/data/cacheable-object.js
index 2e82af0..2e82af0 100644
--- a/test/unit/data/things/cacheable-object.js
+++ b/test/unit/data/cacheable-object.js
diff --git a/test/unit/data/composite/common-utilities/exposeConstant.js b/test/unit/data/composite/common-utilities/exposeConstant.js
new file mode 100644
index 0000000..829dc70
--- /dev/null
+++ b/test/unit/data/composite/common-utilities/exposeConstant.js
@@ -0,0 +1,52 @@
+import t from 'tap';
+
+import {
+  compositeFrom,
+  continuationSymbol,
+  exposeConstant,
+  input,
+} from '#composite';
+
+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/common-utilities/exposeDependency.js b/test/unit/data/composite/common-utilities/exposeDependency.js
new file mode 100644
index 0000000..7880134
--- /dev/null
+++ b/test/unit/data/composite/common-utilities/exposeDependency.js
@@ -0,0 +1,74 @@
+import t from 'tap';
+
+import {
+  compositeFrom,
+  continuationSymbol,
+  exposeDependency,
+  input,
+} from '#composite';
+
+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/common-utilities/withPropertiesFromObject.js b/test/unit/data/composite/common-utilities/withPropertiesFromObject.js
new file mode 100644
index 0000000..b1b8be7
--- /dev/null
+++ b/test/unit/data/composite/common-utilities/withPropertiesFromObject.js
@@ -0,0 +1,224 @@
+import t from 'tap';
+
+import {
+  compositeFrom,
+  exposeDependency,
+  input,
+  withPropertiesFromObject,
+} from '#composite';
+
+t.test(`withPropertiesFromObject: basic behavior`, t => {
+  t.plan(4);
+
+  const composite = compositeFrom({
+    compose: false,
+
+    steps: [
+      withPropertiesFromObject({
+        object: 'object',
+        properties: 'properties',
+      }),
+
+      exposeDependency({dependency: '#object'}),
+    ],
+  });
+
+  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/,
+        },
+      ]},
+    ]});
+});
diff --git a/test/unit/data/composite/common-utilities/withPropertyFromObject.js b/test/unit/data/composite/common-utilities/withPropertyFromObject.js
new file mode 100644
index 0000000..1148722
--- /dev/null
+++ b/test/unit/data/composite/common-utilities/withPropertyFromObject.js
@@ -0,0 +1,125 @@
+import t from 'tap';
+
+import {
+  compositeFrom,
+  exposeDependency,
+  input,
+  withPropertyFromObject,
+} from '#composite';
+
+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/common-utilities/withResultOfAvailabilityCheck.js b/test/unit/data/composite/common-utilities/withResultOfAvailabilityCheck.js
new file mode 100644
index 0000000..dacf60f
--- /dev/null
+++ b/test/unit/data/composite/common-utilities/withResultOfAvailabilityCheck.js
@@ -0,0 +1,200 @@
+import t from 'tap';
+
+import {
+  compositeFrom,
+  continuationSymbol,
+  withResultOfAvailabilityCheck,
+  input,
+} from '#composite';
+
+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(10);
+
+  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: 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(10);
+
+  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: 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(10);
+
+  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, 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: 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, 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, 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/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..e96b782
--- /dev/null
+++ b/test/unit/data/templateCompositeFrom.js
@@ -0,0 +1,218 @@
+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..0695fdb
--- /dev/null
+++ b/test/unit/data/things/album.js
@@ -0,0 +1,387 @@
+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(4);
+
+  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`);
+});
+
+t.test(`Album.trackSections`, t => {
+  t.plan(6);
+
+  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.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 #3: 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 #4: 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 #5: 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 #6: 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/track.js b/test/unit/data/things/track.js
index 383e3e3..f68510c 100644
--- a/test/unit/data/things/track.js
+++ b/test/unit/data/things/track.js
@@ -1,74 +1,587 @@
 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.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'; }, 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.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;
+
+  t.equal(track.coverArtDate, null,
+    `coverArtDate #1: defaults to null`);
+
+  album.trackArtDate = new Date('2012-12-12');
+
+  XXX_decacheWikiData();
+
+  t.same(track.coverArtDate, new Date('2012-12-12'),
+    `coverArtDate #2: inherits album trackArtDate`);
+
+  track.coverArtDate = new Date('2009-09-09');
+
+  t.same(track.coverArtDate, new Date('2009-09-09'),
+    `coverArtDate #3: is own value`);
+
+  track.coverArtistContribs = [];
+
+  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`);
 
-  const album = stubAlbum([track]);
+  track.coverArtistContribs = contribs;
+  track.disableUniqueCoverArt = true;
 
-  track.albumData = [album];
+  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],
+  });
 
-  // 1. coverArtDate defaults to null
+  t.equal(track.hasUniqueCoverArt, false,
+    `hasUniqueCoverArt #1: defaults to false`);
 
-  t.equal(track.coverArtDate, null);
+  album.trackCoverArtistContribs = contribs;
+  XXX_decacheWikiData();
 
-  // 2. coverArtDate inherits album release date
+  t.equal(track.hasUniqueCoverArt, true,
+    `hasUniqueCoverArt #2: is true if album specifies trackCoverArtistContribs`);
 
-  album.date = albumDate;
+  track.disableUniqueCoverArt = true;
 
-  // XXX clear cache so change in album's property is reflected
-  track.albumData = [];
-  track.albumData = [album];
+  t.equal(track.hasUniqueCoverArt, false,
+    `hasUniqueCoverArt #3: is false if disableUniqueCoverArt is true (1/2)`);
 
-  t.equal(track.coverArtDate, albumDate);
+  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`);
 });