« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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/data/things/album.js15
-rw-r--r--src/data/things/art-tag.js4
-rw-r--r--src/data/things/artist.js30
-rw-r--r--src/data/things/cacheable-object.js35
-rw-r--r--src/data/things/flash.js14
-rw-r--r--src/data/things/group.js13
-rw-r--r--src/data/things/thing.js1265
-rw-r--r--src/data/things/track.js796
-rw-r--r--src/data/things/wiki-info.js6
-rw-r--r--src/data/yaml.js26
-rw-r--r--src/util/sugar.js24
15 files changed, 1829 insertions, 487 deletions
diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js
index ce17ab21..51ea5927 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 f65b47c9..f92712f9 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 334c5422..7002204c 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 f001c3b3..65f5552b 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/data/things/album.js b/src/data/things/album.js
index c012c243..06982903 100644
--- a/src/data/things/album.js
+++ b/src/data/things/album.js
@@ -128,6 +128,9 @@ export class Album extends Thing {
 
     commentatorArtists: Thing.common.commentatorArtists(),
 
+    groups: Thing.common.dynamicThingsFromReferenceList('groupsByRef', 'groupData', find.group),
+    artTags: Thing.common.dynamicThingsFromReferenceList('artTagsByRef', 'artTagData', find.artTag),
+
     hasCoverArt: Thing.common.contribsPresent('coverArtistContribsByRef'),
     hasWallpaperArt: Thing.common.contribsPresent('wallpaperArtistContribsByRef'),
     hasBannerArt: Thing.common.contribsPresent('bannerArtistContribsByRef'),
@@ -146,18 +149,6 @@ export class Album extends Thing {
             : [],
       },
     },
-
-    groups: Thing.common.dynamicThingsFromReferenceList(
-      'groupsByRef',
-      'groupData',
-      find.group
-    ),
-
-    artTags: Thing.common.dynamicThingsFromReferenceList(
-      'artTagsByRef',
-      'artTagData',
-      find.artTag
-    ),
   });
 
   static [Thing.getSerializeDescriptors] = ({
diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js
index c103c4d5..bb36e09e 100644
--- a/src/data/things/art-tag.js
+++ b/src/data/things/art-tag.js
@@ -37,8 +37,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 522ca5f9..b2383057 100644
--- a/src/data/things/artist.js
+++ b/src/data/things/artist.js
@@ -66,14 +66,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 +82,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 +103,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 +146,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 ea705a61..62c23d13 100644
--- a/src/data/things/cacheable-object.js
+++ b/src/data/things/cacheable-object.js
@@ -83,8 +83,6 @@ function inspect(value) {
 }
 
 export default class CacheableObject {
-  static instance = Symbol('CacheableObject `this` instance');
-
   #propertyUpdateValues = Object.create(null);
   #propertyUpdateCacheInvalidators = Object.create(null);
 
@@ -143,7 +141,7 @@ export default class CacheableObject {
 
       const definition = {
         configurable: false,
-        enumerable: true,
+        enumerable: flags.expose,
       };
 
       if (flags.update) {
@@ -250,20 +248,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];
+        }
 
-      getAllDependencies = () =>
-        Object.fromEntries(dependencyGetters
-          .map(f => f())
-          .concat([reflectionEntry]));
+        if (shouldReflect) {
+          dependencies.this = this;
+        }
+
+        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) {
diff --git a/src/data/things/flash.js b/src/data/things/flash.js
index 6eb5234f..3f870c51 100644
--- a/src/data/things/flash.js
+++ b/src/data/things/flash.js
@@ -77,9 +77,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)) ?? null,
       },
     },
@@ -88,9 +88,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,
       },
     },
@@ -141,10 +141,6 @@ export class FlashAct extends Thing {
 
     // Expose only
 
-    flashes: Thing.common.dynamicThingsFromReferenceList(
-      'flashesByRef',
-      'flashData',
-      find.flash
-    ),
+    flashes: Thing.common.dynamicThingsFromReferenceList('flashesByRef', 'flashData', find.flash),
   })
 }
diff --git a/src/data/things/group.js b/src/data/things/group.js
index ba339b3e..f552b8f3 100644
--- a/src/data/things/group.js
+++ b/src/data/things/group.js
@@ -41,8 +41,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 +51,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 +62,8 @@ export class Group extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['groupCategoryData'],
-        compute: ({groupCategoryData, [Group.instance]: group}) =>
+        dependencies: ['this', 'groupCategoryData'],
+        compute: ({this: group, groupCategoryData}) =>
           groupCategoryData.find((category) => category.groups.includes(group)) ??
           null,
       },
diff --git a/src/data/things/thing.js b/src/data/things/thing.js
index c2876f56..19f5fb53 100644
--- a/src/data/things/thing.js
+++ b/src/data/things/thing.js
@@ -5,7 +5,7 @@ import {inspect} from 'node:util';
 
 import {color} from '#cli';
 import find from '#find';
-import {empty} from '#sugar';
+import {empty, filterProperties, openAggregate} from '#sugar';
 import {getKebabCase} from '#wiki-data';
 
 import {
@@ -192,26 +192,29 @@ export default class Thing extends CacheableObject {
     // Corresponding dynamic property to referenceList, which takes the values
     // in the provided property and searches the specified wiki data for
     // matching actual Thing-subclass objects.
-    dynamicThingsFromReferenceList: (
-      referenceListProperty,
-      thingDataProperty,
-      findFn
-    ) => ({
-      flags: {expose: true},
+    dynamicThingsFromReferenceList(
+      refs,
+      data,
+      findFunction
+    ) {
+      return Thing.composite.from(`Thing.common.dynamicThingsFromReferenceList`, [
+        Thing.composite.earlyExitWithoutDependency(refs, {value: []}),
+        Thing.composite.earlyExitWithoutDependency(data, {value: []}),
 
-      expose: {
-        dependencies: [referenceListProperty, thingDataProperty],
-        compute: ({
-          [referenceListProperty]: refs,
-          [thingDataProperty]: thingData,
-        }) =>
-          refs && thingData
-            ? refs
-                .map((ref) => findFn(ref, thingData, {mode: 'quiet'}))
-                .filter(Boolean)
-            : [],
-      },
-    }),
+        {
+          flags: {expose: true},
+          expose: {
+            mapDependencies: {refs, data},
+            options: {findFunction},
+
+            compute: ({refs, data, '#options': {findFunction}}) =>
+              refs
+                .map(ref => findFunction(ref, data, {mode: 'quiet'}))
+                .filter(Boolean),
+          },
+        },
+      ]);
+    },
 
     // Corresponding function for a single reference.
     dynamicThingFromSingleReference: (
@@ -250,14 +253,7 @@ export default class Thing extends CacheableObject {
       expose: {
         dependencies: ['artistData', contribsByRefProperty],
         compute: ({artistData, [contribsByRefProperty]: contribsByRef}) =>
-          contribsByRef && artistData
-            ? contribsByRef
-                .map(({who: ref, what}) => ({
-                  who: find.artist(ref, artistData),
-                  what,
-                }))
-                .filter(({who}) => who)
-            : [],
+          Thing.findArtistsFromContribs(contribsByRef, artistData),
       },
     }),
 
@@ -285,6 +281,7 @@ export default class Thing extends CacheableObject {
       flags: {expose: true},
       expose: {
         dependencies: [
+          'this',
           contribsByRefProperty,
           thingDataProperty,
           nullerProperty,
@@ -292,7 +289,7 @@ export default class Thing extends CacheableObject {
         ].filter(Boolean),
 
         compute({
-          [Thing.instance]: thing,
+          this: thing,
           [nullerProperty]: nuller,
           [contribsByRefProperty]: contribsByRef,
           [thingDataProperty]: thingData,
@@ -333,16 +330,15 @@ export default class Thing extends CacheableObject {
     // you would use this to compute a corresponding "referenced *by* tracks"
     // property. Naturally, the passed ref list property is of the things in the
     // wiki data provided, not the requesting Thing itself.
-    reverseReferenceList: (thingDataProperty, referencerRefListProperty) => ({
-      flags: {expose: true},
-
-      expose: {
-        dependencies: [thingDataProperty],
-
-        compute: ({[thingDataProperty]: thingData, [Thing.instance]: thing}) =>
-          thingData?.filter(t => t[referencerRefListProperty].includes(thing)) ?? [],
-      },
-    }),
+    reverseReferenceList({
+      data,
+      refList,
+    }) {
+      return Thing.composite.from(`Thing.common.reverseReferenceList`, [
+        Thing.composite.withReverseReferenceList({data, refList}),
+        Thing.composite.exposeDependency('#reverseReferenceList'),
+      ]);
+    },
 
     // Corresponding function for single references. Note that the return value
     // is still a list - this is for matching all the objects whose single
@@ -351,9 +347,9 @@ export default class Thing extends CacheableObject {
       flags: {expose: true},
 
       expose: {
-        dependencies: [thingDataProperty],
+        dependencies: ['this', thingDataProperty],
 
-        compute: ({[thingDataProperty]: thingData, [Thing.instance]: thing}) =>
+        compute: ({this: thing, [thingDataProperty]: thingData}) =>
           thingData?.filter((t) => t[referencerRefListProperty] === thing) ?? [],
       },
     }),
@@ -418,4 +414,1191 @@ export default class Thing extends CacheableObject {
 
     return `${thing.constructor[Thing.referenceType]}:${thing.directory}`;
   }
+
+  static findArtistsFromContribs(contribsByRef, artistData) {
+    if (empty(contribsByRef)) return null;
+
+    return (
+      contribsByRef
+        .map(({who, what}) => ({
+          who: find.artist(who, artistData, {mode: 'quiet'}),
+          what,
+        }))
+        .filter(({who}) => who));
+  }
+
+  static composite = {
+    // 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:
+    //
+    //   static Thing.composite.withResolvedContribs = ({
+    //     from: contribsByRefDependency,
+    //     to: 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:
+    //
+    //   static Track[Thing.getPropertyDescriptors].coverArtists =
+    //     Thing.composite.from([
+    //       Track.composite.doSomethingWhichMightEarlyExit(),
+    //       Thing.composite.withResolvedContribs({
+    //         from: 'coverArtistContribsByRef',
+    //         to: '#coverArtistContribs',
+    //       }),
+    //
+    //       {
+    //         flags: {expose: true},
+    //         expose: {
+    //           dependencies: ['#coverArtistContribs'],
+    //           compute({'#coverArtistContribs': coverArtistContribs}) {
+    //             return 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:
+    //
+    //   static Thing.composite.withResolvedContribs = ({
+    //     from: contribsByRefDependency,
+    //     to: 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:
+    //
+    //   static Thing.composite.withResolvedContribs = ({from, to}) => ({
+    //     flags: {expose: true, compose: true},
+    //     expose: {
+    //       dependencies: ['artistData'],
+    //       mapDependencies: {from},
+    //       mapContinuation: {to},
+    //       compute({artistData, from: contribsByRef}, continuation) {
+    //         if (!artistData) return null;
+    //         return continuation({
+    //           to: (..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 Thing.composite.from() 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!
+    //
+    from(firstArg, secondArg) {
+      const debug = fn => {
+        if (Thing.composite.from.debug === true) {
+          const label =
+            (annotation
+              ? color.dim(`[composite: ${annotation}]`)
+              : color.dim(`[composite]`));
+          const result = fn();
+          if (Array.isArray(result)) {
+            console.log(label, ...result.map(value =>
+              (typeof value === 'object'
+                ? inspect(value, {depth: 0, colors: true, compact: true, breakLength: Infinity})
+                : value)));
+          } else {
+            console.log(label, result);
+          }
+        }
+      };
+
+      let annotation, composition;
+      if (typeof firstArg === 'string') {
+        [annotation, composition] = [firstArg, secondArg];
+      } else {
+        [annotation, composition] = [null, firstArg];
+      }
+
+      const base = composition.at(-1);
+      const steps = composition.slice();
+
+      const aggregate = openAggregate({
+        message:
+          `Errors preparing Thing.composite.from() composition` +
+          (annotation ? ` (${annotation})` : ''),
+      });
+
+      const baseExposes =
+        (base.flags
+          ? base.flags.expose
+          : true);
+
+      const baseUpdates =
+        (base.flags
+          ? base.flags.update
+          : false);
+
+      const baseComposes =
+        (base.flags
+          ? base.flags.compose
+          : true);
+
+      if (!baseExposes) {
+        aggregate.push(new TypeError(`All steps, including base, must expose`));
+      }
+
+      const exposeDependencies = new Set();
+
+      let anyStepsCompute = false;
+      let anyStepsTransform = false;
+
+      for (let i = 0; i < steps.length; i++) {
+        const step = steps[i];
+        const isBase = i === steps.length - 1;
+        const message =
+          `Errors in step #${i + 1}` +
+          (isBase ? ` (base)` : ``) +
+          (step.annotation ? ` (${step.annotation})` : ``);
+
+        aggregate.nest({message}, ({push}) => {
+          if (step.flags) {
+            let flagsErrored = false;
+
+            if (!step.flags.compose && !isBase) {
+              push(new TypeError(`All steps but base must compose`));
+              flagsErrored = true;
+            }
+
+            if (!step.flags.expose) {
+              push(new TypeError(`All steps must expose`));
+              flagsErrored = true;
+            }
+
+            if (flagsErrored) {
+              return;
+            }
+          }
+
+          const expose =
+            (step.flags
+              ? step.expose
+              : step);
+
+          const stepComputes = !!expose.compute;
+          const stepTransforms = !!expose.transform;
+
+          if (!stepComputes && !stepTransforms) {
+            push(new TypeError(`Steps must provide compute or transform (or both)`));
+            return;
+          }
+
+          if (
+            stepTransforms && !stepComputes &&
+            !baseUpdates && !baseComposes
+          ) {
+            push(new TypeError(`Steps which only transform can't be composed with a non-updating base`));
+            return;
+          }
+
+          if (stepComputes) {
+            anyStepsCompute = true;
+          }
+
+          if (stepTransforms) {
+            anyStepsTransform = true;
+          }
+
+          // Unmapped dependencies are exposed on the final composition only if
+          // they're "public", i.e. pointing to update values of other properties
+          // on the CacheableObject.
+          for (const dependency of expose.dependencies ?? []) {
+            if (typeof dependency === 'string' && dependency.startsWith('#')) {
+              continue;
+            }
+
+            exposeDependencies.add(dependency);
+          }
+
+          // Mapped dependencies are always exposed on the final composition.
+          // These are explicitly for reading values which are named outside of
+          // the current compositional step.
+          for (const dependency of Object.values(expose.mapDependencies ?? {})) {
+            exposeDependencies.add(dependency);
+          }
+        });
+      }
+
+      if (!baseComposes) {
+        if (baseUpdates) {
+          if (!anyStepsTransform) {
+            push(new TypeError(`Expected at least one step to transform`));
+          }
+        } else {
+          if (!anyStepsCompute) {
+            push(new TypeError(`Expected at least one step to compute`));
+          }
+        }
+      }
+
+      aggregate.close();
+
+      const constructedDescriptor = {};
+
+      if (annotation) {
+        constructedDescriptor.annotation = annotation;
+      }
+
+      constructedDescriptor.flags = {
+        update: baseUpdates,
+        expose: baseExposes,
+        compose: baseComposes,
+      };
+
+      if (baseUpdates) {
+        constructedDescriptor.update = base.update;
+      }
+
+      if (baseExposes) {
+        const expose = constructedDescriptor.expose = {};
+        expose.dependencies = Array.from(exposeDependencies);
+
+        const continuationSymbol = Symbol('continuation symbol');
+        const noTransformSymbol = Symbol('no-transform symbol');
+
+        function _filterDependencies(availableDependencies, {
+          dependencies,
+          mapDependencies,
+          options,
+        }) {
+          const filteredDependencies =
+            (dependencies
+              ? filterProperties(availableDependencies, dependencies)
+              : {});
+
+          if (mapDependencies) {
+            for (const [to, from] of Object.entries(mapDependencies)) {
+              filteredDependencies[to] = availableDependencies[from] ?? null;
+            }
+          }
+
+          if (options) {
+            filteredDependencies['#options'] = options;
+          }
+
+          return filteredDependencies;
+        }
+
+        function _assignDependencies(continuationAssignment, {mapContinuation}) {
+          if (!mapContinuation) {
+            return continuationAssignment;
+          }
+
+          const assignDependencies = {};
+
+          for (const [from, to] of Object.entries(mapContinuation)) {
+            assignDependencies[to] = continuationAssignment[from] ?? null;
+          }
+
+          return assignDependencies;
+        }
+
+        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 (baseComposes) {
+            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.raise = makeRaiseLike('raise');
+            continuation.raiseAbove = makeRaiseLike('raiseAbove');
+          }
+
+          return {continuation, continuationStorage};
+        }
+
+        function _computeOrTransform(initialValue, initialDependencies, continuationIfApplicable) {
+          const expectingTransform = initialValue !== noTransformSymbol;
+
+          let valueSoFar =
+            (expectingTransform
+              ? initialValue
+              : undefined);
+
+          const availableDependencies = {...initialDependencies};
+
+          if (expectingTransform) {
+            debug(() => [color.bright(`begin composition - transforming from:`), initialValue]);
+          } else {
+            debug(() => color.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);
+
+            const callingTransformForThisStep =
+              expectingTransform && expose.transform;
+
+            const filteredDependencies = _filterDependencies(availableDependencies, expose);
+            const {continuation, continuationStorage} = _prepareContinuation(callingTransformForThisStep);
+
+            debug(() => [
+              `step #${i+1} - ${callingTransformForThisStep ? 'transform' : 'compute'}`,
+              `with dependencies:`, filteredDependencies]);
+
+            const result =
+              (callingTransformForThisStep
+                ? expose.transform(valueSoFar, filteredDependencies, continuation)
+                : expose.compute(filteredDependencies, continuation));
+
+            if (result !== continuationSymbol) {
+              debug(() => [`step #${i+1} - result: exit (inferred) ->`, result]);
+
+              if (baseComposes) {
+                throw new TypeError(`Inferred early-exit is disallowed in nested compositions`);
+              }
+
+              debug(() => color.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(() => color.bright(`end composition - exit (explicit)`));
+
+              if (baseComposes) {
+                return continuationIfApplicable.exit(providedValue);
+              } else {
+                return providedValue;
+              }
+            }
+
+            const {providedValue, providedDependencies} = continuationStorage;
+
+            const continuingWithValue =
+              (expectingTransform
+                ? (callingTransformForThisStep
+                    ? providedValue ?? null
+                    : valueSoFar ?? null)
+                : undefined);
+
+            const continuingWithDependencies =
+              (providedDependencies
+                ? _assignDependencies(providedDependencies, expose)
+                : null);
+
+            const continuationArgs = [];
+            if (continuingWithValue !== undefined) continuationArgs.push(continuingWithValue);
+            if (continuingWithDependencies !== null) continuationArgs.push(continuingWithDependencies);
+
+            debug(() => {
+              const base = `step #${i+1} - result: ` + returnedWith;
+              const parts = [];
+
+              if (callingTransformForThisStep) {
+                if (continuingWithValue === undefined) {
+                  parts.push(`(no value)`);
+                } else {
+                  parts.push(`value:`, providedValue);
+                }
+              }
+
+              if (continuingWithDependencies !== null) {
+                parts.push(`deps:`, continuingWithDependencies);
+              } else {
+                parts.push(`(no deps)`);
+              }
+
+              if (empty(parts)) {
+                return base;
+              } else {
+                return [base + ' ->', ...parts];
+              }
+            });
+
+            switch (returnedWith) {
+              case 'raise':
+                debug(() =>
+                  (isBase
+                    ? color.bright(`end composition - raise (base: explicit)`)
+                    : color.bright(`end composition - raise`)));
+                return continuationIfApplicable(...continuationArgs);
+
+              case 'raiseAbove':
+                debug(() => color.bright(`end composition - raiseAbove`));
+                return continuationIfApplicable.raise(...continuationArgs);
+
+              case 'continuation':
+                if (isBase) {
+                  debug(() => color.bright(`end composition - raise (inferred)`));
+                  return continuationIfApplicable(...continuationArgs);
+                } else {
+                  Object.assign(availableDependencies, continuingWithDependencies);
+                  break;
+                }
+            }
+          }
+        }
+
+        const transformFn =
+          (value, initialDependencies, continuationIfApplicable) =>
+            _computeOrTransform(value, initialDependencies, continuationIfApplicable);
+
+        const computeFn =
+          (initialDependencies, continuationIfApplicable) =>
+            _computeOrTransform(noTransformSymbol, initialDependencies, continuationIfApplicable);
+
+        if (baseComposes) {
+          if (anyStepsTransform) expose.transform = transformFn;
+          if (anyStepsCompute) expose.compute = computeFn;
+        } else if (baseUpdates) {
+          expose.transform = transformFn;
+        } else {
+          expose.compute = computeFn;
+        }
+      }
+
+      return constructedDescriptor;
+    },
+
+    // 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(Thing.composite.debug(() => thing.someProp), value)
+    //
+    debug(fn) {
+      Thing.composite.from.debug = true;
+      const value = fn();
+      Thing.composite.from.debug = false;
+      return value;
+    },
+
+    // -- Compositional steps for compositions to nest --
+
+    // Provides dependencies exactly as they are (or null if not defined) to the
+    // continuation. Although this can *technically* be used to alias existing
+    // dependencies to some other name within the middle of a composition, it's
+    // intended to be used only as a composition's base - doing so makes the
+    // composition as a whole suitable as a step in some other composition,
+    // providing the listed (internal) dependencies to later steps just like
+    // other compositional steps.
+    export(mapping) {
+      const mappingEntries = Object.entries(mapping);
+
+      return {
+        annotation: `Thing.composite.export`,
+        flags: {expose: true, compose: true},
+
+        expose: {
+          options: {mappingEntries},
+          dependencies: Object.values(mapping),
+
+          compute({'#options': {mappingEntries}, ...dependencies}, continuation) {
+            const exports = {};
+
+            // Note: This is slightly different behavior from filterProperties,
+            // as defined in sugar.js, which doesn't fall back to null for
+            // properties which don't exist on the original object.
+            for (const [exportKey, dependencyKey] of mappingEntries) {
+              exports[exportKey] =
+                (Object.hasOwn(dependencies, dependencyKey)
+                  ? dependencies[dependencyKey]
+                  : null);
+            }
+
+            return continuation.raise(exports);
+          }
+        },
+      };
+    },
+
+    // -- Compositional steps for top-level property descriptors --
+
+    // 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.
+    // Since this serves as a base, specify a value for {update} to indicate
+    // that the property as a whole updates (and some previous compositional
+    // step works with that update value). Set {update: true} to only enable
+    // the update flag, or set update to an object to specify a descriptor
+    // (e.g. for custom value validation).
+    //
+    // 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.
+    //
+    exposeDependency(dependency, {
+      update = false,
+    } = {}) {
+      return {
+        annotation: `Thing.composite.exposeDependency`,
+        flags: {expose: true, update: !!update},
+
+        expose: {
+          mapDependencies: {dependency},
+          compute: ({dependency}) => dependency,
+        },
+
+        update:
+          (typeof update === 'object'
+            ? update
+            : null),
+      };
+    },
+
+    // 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. Like exposeDependency, set {update} to true or
+    // an object to indicate that the property as a whole updates.
+    exposeConstant(value, {
+      update = false,
+    } = {}) {
+      return {
+        annotation: `Thing.composite.exposeConstant`,
+        flags: {expose: true, update: !!update},
+
+        expose: {
+          options: {value},
+          compute: ({'#options': {value}}) => value,
+        },
+
+        update:
+          (typeof update === 'object'
+            ? update
+            : null),
+      };
+    },
+
+    // Checks the availability of a dependency or the update value 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 leave unset and default to 'null':
+    //
+    // * 'null':  Check that the value isn't null.
+    // * 'empty': Check that the value is neither null 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!
+    //
+    withResultOfAvailabilityCheck({
+      fromUpdateValue,
+      fromDependency,
+      mode = 'null',
+      to = '#availability',
+    }) {
+      if (!['null', 'empty', 'falsy'].includes(mode)) {
+        throw new TypeError(`Expected mode to be null, empty, or falsy`);
+      }
+
+      if (fromUpdateValue && fromDependency) {
+        throw new TypeError(`Don't provide both fromUpdateValue and fromDependency`);
+      }
+
+      if (!fromUpdateValue && !fromDependency) {
+        throw new TypeError(`Missing dependency name (or fromUpdateValue)`);
+      }
+
+      const checkAvailability = (value, mode) => {
+        switch (mode) {
+          case 'null': return value !== null;
+          case 'empty': return !empty(value);
+          case 'falsy': return !!value && (!Array.isArray(value) || !empty(value));
+          default: return false;
+        }
+      };
+
+      if (fromDependency) {
+        return {
+          annotation: `Thing.composite.withResultOfAvailabilityCheck.fromDependency`,
+          flags: {expose: true, compose: true},
+          expose: {
+            mapDependencies: {from: fromDependency},
+            mapContinuation: {to},
+            options: {mode},
+            compute: ({from, '#options': {mode}}, continuation) =>
+              continuation({to: checkAvailability(from, mode)}),
+          },
+        };
+      } else {
+        return {
+          annotation: `Thing.composite.withResultOfAvailabilityCheck.fromUpdateValue`,
+          flags: {expose: true, compose: true},
+          expose: {
+            mapContinuation: {to},
+            options: {mode},
+            transform: (value, {'#options': {mode}}, continuation) =>
+              continuation(value, {to: checkAvailability(value, mode)}),
+          },
+        };
+      }
+    },
+
+    // Exposes a dependency as it is, or continues if it's unavailable.
+    // See withResultOfAvailabilityCheck for {mode} options!
+    exposeDependencyOrContinue(dependency, {
+      mode = 'null',
+    } = {}) {
+      return Thing.composite.from(`Thing.composite.exposeDependencyOrContinue`, [
+        Thing.composite.withResultOfAvailabilityCheck({
+          fromDependency: dependency,
+          mode,
+        }),
+
+        {
+          dependencies: ['#availability'],
+          compute: ({'#availability': availability}, continuation) =>
+            (availability
+              ? continuation()
+              : continuation.raise()),
+        },
+
+        {
+          mapDependencies: {dependency},
+          compute: ({dependency}, continuation) =>
+            continuation.exit(dependency),
+        },
+      ]);
+    },
+
+    // Exposes the update value of an {update: true} property as it is,
+    // or continues if it's unavailable. See withResultOfAvailabilityCheck
+    // for {mode} options!
+    exposeUpdateValueOrContinue({
+      mode = 'null',
+    } = {}) {
+      return Thing.composite.from(`Thing.composite.exposeUpdateValueOrContinue`, [
+        Thing.composite.withResultOfAvailabilityCheck({
+          fromUpdateValue: true,
+          mode,
+        }),
+
+        {
+          dependencies: ['#availability'],
+          compute: ({'#availability': availability}, continuation) =>
+            (availability
+              ? continuation()
+              : continuation.raise()),
+        },
+
+        {
+          transform: (value, {}, continuation) =>
+            continuation.exit(value),
+        },
+      ]);
+    },
+
+    // Early exits if an availability check fails.
+    // This is for internal use only - use `earlyExitWithoutDependency` or
+    // `earlyExitWIthoutUpdateValue` instead.
+    earlyExitIfAvailabilityCheckFailed({
+      availability = '#availability',
+      value = null,
+    }) {
+      return Thing.composite.from(`Thing.composite.earlyExitIfAvailabilityCheckFailed`, [
+        {
+          mapDependencies: {availability},
+          compute: ({availability}, continuation) =>
+            (availability
+              ? continuation.raise()
+              : continuation()),
+        },
+
+        {
+          options: {value},
+          compute: ({'#options': {value}}, continuation) =>
+            continuation.exit(value),
+        },
+      ]);
+    },
+
+    // Early exits if a dependency isn't available.
+    // See withResultOfAvailabilityCheck for {mode} options!
+    earlyExitWithoutDependency(dependency, {
+      mode = 'null',
+      value = null,
+    } = {}) {
+      return Thing.composite.from(`Thing.composite.earlyExitWithoutDependency`, [
+        Thing.composite.withResultOfAvailabilityCheck({fromDependency: dependency, mode}),
+        Thing.composite.earlyExitIfAvailabilityCheckFailed({value}),
+      ]);
+    },
+
+    // Early exits if this property's update value isn't available.
+    // See withResultOfAvailabilityCheck for {mode} options!
+    earlyExitWithoutUpdateValue({
+      mode = 'null',
+      value = null,
+    } = {}) {
+      return Thing.composite.from(`Thing.composite.earlyExitWithoutDependency`, [
+        Thing.composite.withResultOfAvailabilityCheck({fromUpdateValue: true, mode}),
+        Thing.composite.earlyExitIfAvailabilityCheckFailed({value}),
+      ]);
+    },
+
+    // Raises if a dependency isn't available.
+    // See withResultOfAvailabilityCheck for {mode} options!
+    raiseWithoutDependency(dependency, {
+      mode = 'null',
+      map = {},
+      raise = {},
+    } = {}) {
+      return Thing.composite.from(`Thing.composite.raiseWithoutDependency`, [
+        Thing.composite.withResultOfAvailabilityCheck({fromDependency: dependency, mode}),
+
+        {
+          dependencies: ['#availability'],
+          compute: ({'#availability': availability}, continuation) =>
+            (availability
+              ? continuation.raise()
+              : continuation()),
+        },
+
+        {
+          options: {raise},
+          mapContinuation: map,
+          compute: ({'#options': {raise}}, continuation) =>
+            continuation.raiseAbove(raise),
+        },
+      ]);
+    },
+
+    // Raises if this property's update value isn't available.
+    // See withResultOfAvailabilityCheck for {mode} options!
+    raiseWithoutUpdateValue({
+      mode = 'null',
+      map = {},
+      raise = {},
+    } = {}) {
+      return Thing.composite.from(`Thing.composite.raiseWithoutUpdateValue`, [
+        Thing.composite.withResultOfAvailabilityCheck({fromUpdateValue: true, mode}),
+
+        {
+          mapDependencies: {availability},
+          compute: ({availability}, continuation) =>
+            (availability
+              ? continuation.raise()
+              : continuation()),
+        },
+
+        {
+          options: {raise},
+          mapContinuation: map,
+          compute: ({'#options': {raise}}, continuation) =>
+            continuation.raiseAbove(raise),
+        },
+      ]);
+    },
+
+    // -- Compositional steps for processing data --
+
+    // 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.
+    withResolvedContribs({from, to}) {
+      return {
+        annotation: `Thing.composite.withResolvedContribs`,
+        flags: {expose: true, compose: true},
+
+        expose: {
+          dependencies: ['artistData'],
+          mapDependencies: {from},
+          mapContinuation: {to},
+          compute: ({artistData, from}, continuation) =>
+            continuation({
+              to: Thing.findArtistsFromContribs(from, artistData),
+            }),
+        },
+      };
+    },
+
+    // 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 earlyExitIfNotFound is set to true,
+    // 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.
+    withResolvedReference({
+      ref,
+      data,
+      to,
+      find: findFunction,
+      earlyExitIfNotFound = false,
+    }) {
+      return Thing.composite.from(`Thing.composite.withResolvedReference`, [
+        Thing.composite.raiseWithoutDependency(ref, {map: {to}, raise: {to: null}}),
+        Thing.composite.earlyExitWithoutDependency(data),
+
+        {
+          options: {findFunction, earlyExitIfNotFound},
+          mapDependencies: {ref, data},
+          mapContinuation: {match: to},
+
+          compute({ref, data, '#options': {findFunction, earlyExitIfNotFound}}, continuation) {
+            const match = findFunction(ref, data, {mode: 'quiet'});
+
+            if (match === null && earlyExitIfNotFound) {
+              return continuation.exit(null);
+            }
+
+            return continuation.raise({match});
+          },
+        },
+      ]);
+    },
+
+    // Check out the info on Thing.common.reverseReferenceList!
+    // This is its composable form.
+    withReverseReferenceList({
+      data,
+      to = '#reverseReferenceList',
+      refList: refListProperty,
+    }) {
+      return Thing.composite.from(`Thing.common.reverseReferenceList`, [
+        Thing.composite.earlyExitWithoutDependency(data, {value: []}),
+
+        {
+          dependencies: ['this'],
+          mapDependencies: {data},
+          mapContinuation: {to},
+          options: {refListProperty},
+
+          compute: ({this: thisThing, data, '#options': {refListProperty}}, continuation) =>
+            continuation({
+              to: data.filter(thing => thing[refListProperty].includes(thisThing)),
+            }),
+        },
+      ]);
+    },
+  };
 }
diff --git a/src/data/things/track.js b/src/data/things/track.js
index e176acb4..bf56a6dd 100644
--- a/src/data/things/track.js
+++ b/src/data/things/track.js
@@ -44,60 +44,72 @@ export class Track extends Thing {
     sampledTracksByRef: Thing.common.referenceList(Track),
     artTagsByRef: Thing.common.referenceList(ArtTag),
 
-    hasCoverArt: {
-      flags: {update: true, expose: true},
-
-      update: {
-        validate(value) {
-          if (value !== false) {
-            throw new TypeError(`Expected false or null`);
-          }
-
-          return true;
-        },
-      },
-
-      expose: {
-        dependencies: ['albumData', 'coverArtistContribsByRef'],
-        transform: (hasCoverArt, {
-          albumData,
-          coverArtistContribsByRef,
-          [Track.instance]: track,
-        }) =>
-          Track.hasCoverArt(
-            track,
-            albumData,
-            coverArtistContribsByRef,
-            hasCoverArt
-          ),
+    color: Thing.composite.from(`Track.color`, [
+      Thing.composite.exposeUpdateValueOrContinue(),
+      Track.composite.withContainingTrackSection({earlyExitIfNotFound: false}),
+
+      {
+        dependencies: ['#trackSection'],
+        compute: ({'#trackSection': trackSection}, continuation) =>
+          // Album.trackSections guarantees the track section will have a
+          // color property (inheriting from the album's own color), but only
+          // if it's actually present! Color will be inherited directly from
+          // album otherwise.
+          (trackSection
+            ? trackSection.color
+            : continuation()),
       },
-    },
 
-    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',
-      },
-    },
+      Track.composite.withAlbumProperty('color'),
+      Thing.composite.exposeDependency('#album.color', {
+        update: {validate: isColor},
+      }),
+    ]),
+
+    // 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: Thing.common.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: Thing.composite.from(`Track.coverArtFileExtension`, [
+      // No cover art file extension if the track doesn't have unique artwork
+      // in the first place.
+      Track.composite.withHasUniqueCoverArt(),
+      Thing.composite.earlyExitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}),
+
+      // Expose custom coverArtFileExtension update value first.
+      Thing.composite.exposeUpdateValueOrContinue(),
+
+      // Expose album's trackCoverArtFileExtension if no update value set.
+      Track.composite.withAlbumProperty('trackCoverArtFileExtension'),
+      Thing.composite.exposeDependencyOrContinue('#album.trackCoverArtFileExtension'),
+
+      // Fallback to 'jpg'.
+      Thing.composite.exposeConstant('jpg', {
+        update: {validate: isFileExtension},
+      }),
+    ]),
+
+    // 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: Thing.composite.from(`Track.coverArtDate`, [
+      Track.composite.withHasUniqueCoverArt(),
+      Thing.composite.earlyExitWithoutDependency('#hasUniqueCoverArt', {mode: 'falsy'}),
+
+      Thing.composite.exposeUpdateValueOrContinue(),
+
+      Track.composite.withAlbumProperty('trackArtDate'),
+      Thing.composite.exposeDependency('#album.trackArtDate', {
+        update: {validate: isDate},
+      }),
+    ]),
 
     originalReleaseTrackByRef: Thing.common.singleReference(Track),
 
@@ -121,15 +133,10 @@ export class Track extends Thing {
 
     commentatorArtists: Thing.common.commentatorArtists(),
 
-    album: {
-      flags: {expose: true},
-
-      expose: {
-        dependencies: ['albumData'],
-        compute: ({[Track.instance]: track, albumData}) =>
-          albumData?.find((album) => album.tracks.includes(track)) ?? null,
-      },
-    },
+    album: Thing.composite.from(`Track.album`, [
+      Track.composite.withAlbum(),
+      Thing.composite.exposeDependency('#album'),
+    ]),
 
     // 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
@@ -138,158 +145,120 @@ export class Track extends Thing {
     // 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,
+    dataSourceAlbum: Thing.common.dynamicThingFromSingleReference('dataSourceAlbumByRef', 'albumData', find.album),
+
+    date: Thing.composite.from(`Track.date`, [
+      Thing.composite.exposeDependencyOrContinue('dateFirstReleased'),
+      Track.composite.withAlbumProperty('date'),
+      Thing.composite.exposeDependency('#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: Thing.composite.from(`Track.hasUniqueCoverArt`, [
+      Track.composite.withHasUniqueCoverArt(),
+      Thing.composite.exposeDependency('#hasUniqueCoverArt'),
+    ]),
+
+    originalReleaseTrack: Thing.composite.from(`Track.originalReleaseTrack`, [
+      Track.composite.withOriginalRelease(),
+      Thing.composite.exposeDependency('#originalRelease'),
+    ]),
+
+    otherReleases: Thing.composite.from(`Track.otherReleases`, [
+      Thing.composite.earlyExitWithoutDependency('trackData', {mode: 'empty'}),
+      Track.composite.withOriginalRelease({selfIfOriginal: true}),
+
+      {
+        flags: {expose: true},
+        expose: {
+          dependencies: ['this', 'trackData', '#originalRelease'],
+          compute: ({
+            this: thisTrack,
+            trackData,
+            '#originalRelease': originalRelease,
+          }) =>
+            (originalRelease === thisTrack
+              ? []
+              : [originalRelease])
+              .concat(trackData.filter(track =>
+                track !== originalRelease &&
+                track !== thisTrack &&
+                track.originalReleaseTrack === originalRelease)),
+        },
       },
-    },
-
-    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,
+    ]),
+
+    artistContribs: Thing.composite.from(`Track.artistContribs`, [
+      Track.composite.inheritFromOriginalRelease({property: 'artistContribs'}),
+
+      Thing.composite.withResolvedContribs({
+        from: 'artistContribsByRef',
+        to: '#artistContribs',
+      }),
+
+      {
+        dependencies: ['#artistContribs'],
+        compute: ({'#artistContribs': contribsFromTrack}, continuation) =>
+          (empty(contribsFromTrack)
+            ? continuation()
+            : contribsFromTrack),
       },
-    },
 
-    coverArtDate: {
-      flags: {update: true, expose: true},
+      Track.composite.withAlbumProperty('artistContribs'),
+      Thing.composite.exposeDependency('#album.artistContribs'),
+    ]),
 
-      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),
-      },
-    },
+    contributorContribs: Thing.composite.from(`Track.contributorContribs`, [
+      Track.composite.inheritFromOriginalRelease({property: 'contributorContribs'}),
+      Thing.common.dynamicContribs('contributorContribsByRef'),
+    ]),
 
-    hasUniqueCoverArt: {
-      flags: {expose: true},
-
-      expose: {
-        dependencies: ['albumData', 'coverArtistContribsByRef', 'hasCoverArt'],
-        compute: ({
-          albumData,
-          coverArtistContribsByRef,
-          hasCoverArt,
-          [Track.instance]: track,
-        }) =>
-          Track.hasUniqueCoverArt(
-            track,
-            albumData,
-            coverArtistContribsByRef,
-            hasCoverArt
-          ),
+    // 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.composite.from(`Track.coverArtistContribs`, [
+      {
+        dependencies: ['disableUniqueCoverArt'],
+        compute: ({disableUniqueCoverArt}, continuation) =>
+          (disableUniqueCoverArt
+            ? null
+            : continuation()),
       },
-    },
 
-    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);
-        },
+      Thing.composite.withResolvedContribs({
+        from: 'coverArtistContribsByRef',
+        to: '#coverArtistContribs',
+      }),
+
+      {
+        dependencies: ['#coverArtistContribs'],
+        compute: ({'#coverArtistContribs': contribsFromTrack}, continuation) =>
+          (empty(contribsFromTrack)
+            ? continuation()
+            : contribsFromTrack),
       },
-    },
 
-    artistContribs:
-      Track.inheritFromOriginalRelease('artistContribs', [],
-        Thing.common.dynamicInheritContribs(
-          null,
-          'artistContribsByRef',
-          'artistContribsByRef',
-          'albumData',
-          Track.findAlbum)),
+      Track.composite.withAlbumProperty('trackCoverArtistContribs'),
+      Thing.composite.exposeDependency('#album.trackCoverArtistContribs'),
+    ]),
 
-    contributorContribs:
-      Track.inheritFromOriginalRelease('contributorContribs', [],
-        Thing.common.dynamicContribs('contributorContribsByRef')),
+    referencedTracks: Thing.composite.from(`Track.referencedTracks`, [
+      Track.composite.inheritFromOriginalRelease({property: 'referencedTracks'}),
+      Thing.common.dynamicThingsFromReferenceList('referencedTracksByRef', 'trackData', find.track),
+    ]),
 
-    // 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)),
+    sampledTracks: Thing.composite.from(`Track.sampledTracks`, [
+      Track.composite.inheritFromOriginalRelease({property: 'sampledTracks'}),
+      Thing.common.dynamicThingsFromReferenceList('sampledTracksByRef', 'trackData', find.track),
+    ]),
+
+    artTags: Thing.common.dynamicThingsFromReferenceList('artTagsByRef', 'artTagData', find.artTag),
 
     // 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
@@ -299,162 +268,321 @@ 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: Track.composite.trackReverseReferenceList('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))
-            : [],
-      },
-    },
-
-    featuredInFlashes: Thing.common.reverseReferenceList(
-      'flashData',
-      'featuredTracks'
-    ),
+    sampledByTracks: Track.composite.trackReverseReferenceList('sampledTracks'),
 
-    artTags: Thing.common.dynamicThingsFromReferenceList(
-      'artTagsByRef',
-      'artTagData',
-      find.artTag
-    ),
+    featuredInFlashes: Thing.common.reverseReferenceList({
+      data: 'flashData',
+      refList: 'featuredTracks',
+    }),
   });
 
-  // 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;
-    }
+  static composite = {
+    // 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.
+    inheritFromOriginalRelease({
+      property: originalProperty,
+      allowOverride = false,
+    }) {
+      return Thing.composite.from(`Track.composite.inheritFromOriginalRelease`, [
+        Track.composite.withOriginalRelease(),
+
+        {
+          dependencies: ['#originalRelease'],
+          compute({'#originalRelease': originalRelease}, continuation) {
+            if (!originalRelease) return continuation.raise();
+
+            const value = originalRelease[originalProperty];
+            if (allowOverride && value === null) return continuation.raise();
+
+            return continuation.exit(value);
+          },
+        },
+      ]);
+    },
 
-    const album = Track.findAlbum(track, albumData);
-    if (album && !empty(album.trackCoverArtistContribsByRef)) {
-      return true;
-    }
+    // Gets the track's album. Unless earlyExitIfNotFound is overridden false,
+    // this will early exit with null in two cases - albumData being missing,
+    // or not including an album whose .tracks array includes this track.
+    withAlbum({to = '#album', earlyExitIfNotFound = true} = {}) {
+      return Thing.composite.from(`Track.composite.withAlbum`, [
+        Thing.composite.withResultOfAvailabilityCheck({
+          fromDependency: 'albumData',
+          mode: 'empty',
+          to: '#albumDataAvailability',
+        }),
+
+        {
+          dependencies: ['#albumDataAvailability'],
+          options: {earlyExitIfNotFound},
+          mapContinuation: {to},
+
+          compute: ({
+            '#albumDataAvailability': albumDataAvailability,
+            '#options': {earlyExitIfNotFound},
+          }, continuation) =>
+            (albumDataAvailability
+              ? continuation()
+              : (earlyExitIfNotFound
+                  ? continuation.exit(null)
+                  : continuation.raise({to: null}))),
+        },
 
-    return false;
-  }
+        {
+          dependencies: ['this', 'albumData'],
+          compute: ({this: track, albumData}, continuation) =>
+            continuation({
+              '#album':
+                albumData.find(album => album.tracks.includes(track)),
+            }),
+        },
 
-  static hasUniqueCoverArt(
-    track,
-    albumData,
-    coverArtistContribsByRef,
-    hasCoverArt
-  ) {
-    if (!empty(coverArtistContribsByRef)) {
-      return true;
-    }
+        {
+          dependencies: ['#album'],
+          options: {earlyExitIfNotFound},
+          mapContinuation: {to},
+          compute: ({
+            '#album': album,
+            '#options': {earlyExitIfNotFound},
+          }, continuation) =>
+            (album
+              ? continuation.raise({to: album})
+              : (earlyExitIfNotFound
+                  ? continuation.exit(null)
+                  : continuation.raise({to: album}))),
+        },
+      ]);
+    },
 
-    if (hasCoverArt === false) {
-      return false;
-    }
+    // 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, and earlyExitIfNotFound hasn't been set, the property
+    // will be provided as null.
+    withAlbumProperty(property, {
+      to = '#album.' + property,
+      earlyExitIfNotFound = false,
+    } = {}) {
+      return Thing.composite.from(`Track.composite.withAlbumProperty`, [
+        Track.composite.withAlbum({earlyExitIfNotFound}),
+
+        {
+          dependencies: ['#album'],
+          options: {property},
+          mapContinuation: {to},
+
+          compute: ({
+            '#album': album,
+            '#options': {property},
+          }, continuation) =>
+            (album
+              ? continuation.raise({to: album[property]})
+              : continuation.raise({to: null})),
+        },
+      ]);
+    },
 
-    const album = Track.findAlbum(track, albumData);
-    if (album && !empty(album.trackCoverArtistContribsByRef)) {
-      return true;
-    }
+    // Gets the listed properties from this track's album, providing them as
+    // dependencies (by default) with '#album.' prefixed before each property
+    // name. If the track's album isn't available, and earlyExitIfNotFound
+    // hasn't been set, the same dependency names will be provided as null.
+    withAlbumProperties({
+      properties,
+      prefix = '#album',
+      earlyExitIfNotFound = false,
+    }) {
+      return Thing.composite.from(`Track.composite.withAlbumProperties`, [
+        Track.composite.withAlbum({earlyExitIfNotFound}),
+
+        {
+          dependencies: ['#album'],
+          options: {properties, prefix},
+
+          compute({
+            '#album': album,
+            '#options': {properties, prefix},
+          }, continuation) {
+            const raise = {};
+
+            if (album) {
+              for (const property of properties) {
+                raise[prefix + '.' + property] = album[property];
+              }
+            } else {
+              for (const property of properties) {
+                raise[prefix + '.' + property] = null;
+              }
+            }
+
+            return continuation.raise(raise);
+          },
+        },
+      ]);
+    },
 
-    return false;
-  }
+    // Gets the track section containing this track from its album's track list.
+    // Unless earlyExitIfNotFound is overridden false, this will early exit if
+    // the album can't be found or if none of its trackSections includes the
+    // track for some reason.
+    withContainingTrackSection({
+      to = '#trackSection',
+      earlyExitIfNotFound = true,
+    } = {}) {
+      return Thing.composite.from(`Track.composite.withContainingTrackSection`, [
+        Track.composite.withAlbumProperty('trackSections', {earlyExitIfNotFound}),
+
+        {
+          dependencies: ['this', '#album.trackSections'],
+          mapContinuation: {to},
+
+          compute({
+            this: track,
+            '#album.trackSections': trackSections,
+          }, continuation) {
+            if (!trackSections) {
+              return continuation.raise({to: null});
+            }
+
+            const trackSection =
+              trackSections.find(({tracks}) => tracks.includes(track));
+
+            if (trackSection) {
+              return continuation.raise({to: trackSection});
+            } else if (earlyExitIfNotFound) {
+              return continuation.exit(null);
+            } else {
+              return continuation.raise({to: null});
+            }
+          },
+        },
+      ]);
+    },
 
-  static inheritFromOriginalRelease(
-    originalProperty,
-    originalMissingValue,
-    ownPropertyDescriptor
-  ) {
-    return {
-      flags: {expose: true},
-
-      expose: {
-        dependencies: [
-          ...ownPropertyDescriptor.expose.dependencies,
-          'originalReleaseTrackByRef',
-          'trackData',
-        ],
-
-        compute(dependencies) {
-          const {
-            originalReleaseTrackByRef,
-            trackData,
-          } = dependencies;
+    // 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.
+    withOriginalRelease({
+      to = '#originalRelease',
+      selfIfOriginal = false,
+    } = {}) {
+      return Thing.composite.from(`Track.composite.withOriginalRelease`, [
+        Thing.composite.withResolvedReference({
+          ref: 'originalReleaseTrackByRef',
+          data: 'trackData',
+          to: '#originalRelease',
+          find: find.track,
+          earlyExitIfNotFound: true,
+        }),
+
+        {
+          dependencies: ['this', '#originalRelease'],
+          options: {selfIfOriginal},
+          mapContinuation: {to},
+          compute: ({
+            this: track,
+            '#originalRelease': originalRelease,
+            '#options': {selfIfOriginal},
+          }, continuation) =>
+            continuation.raise({
+              to:
+                (originalRelease ??
+                  (selfIfOriginal
+                    ? track
+                    : null)),
+            }),
+        },
+      ]);
+    },
 
-          if (originalReleaseTrackByRef) {
-            if (!trackData) return originalMissingValue;
-            const original = find.track(originalReleaseTrackByRef, trackData, {mode: 'quiet'});
-            if (!original) return originalMissingValue;
-            return original[originalProperty];
-          }
+    // 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.
+    withHasUniqueCoverArt({
+      to = '#hasUniqueCoverArt',
+    } = {}) {
+      return Thing.composite.from(`Track.composite.withHasUniqueCoverArt`, [
+        {
+          dependencies: ['disableUniqueCoverArt'],
+          mapContinuation: {to},
+          compute: ({disableUniqueCoverArt}, continuation) =>
+            (disableUniqueCoverArt
+              ? continuation.raise({to: false})
+              : continuation()),
+        },
 
-          return ownPropertyDescriptor.expose.compute(dependencies);
+        Thing.composite.withResolvedContribs({
+          from: 'coverArtistContribsByRef',
+          to: '#coverArtistContribs',
+        }),
+
+        {
+          dependencies: ['#coverArtistContribs'],
+          mapContinuation: {to},
+          compute: ({'#coverArtistContribs': contribsFromTrack}, continuation) =>
+            (empty(contribsFromTrack)
+              ? continuation()
+              : continuation.raise({to: true})),
         },
-      },
-    };
-  }
 
-  [inspect.custom]() {
-    const base = Thing.prototype[inspect.custom].apply(this);
+        Track.composite.withAlbumProperty('trackCoverArtistContribs'),
 
-    const rereleasePart =
-      (this.originalReleaseTrackByRef
-        ? `${color.yellow('[rerelease]')} `
-        : ``);
+        {
+          dependencies: ['#album.trackCoverArtistContribs'],
+          mapContinuation: {to},
+          compute: ({'#album.trackCoverArtistContribs': contribsFromAlbum}, continuation) =>
+            (empty(contribsFromAlbum)
+              ? continuation.raise({to: false})
+              : continuation.raise({to: true})),
+        },
+      ]);
+    },
 
-    const {album, dataSourceAlbum} = this;
+    trackReverseReferenceList(refListProperty) {
+      return Thing.composite.from(`Track.composite.trackReverseReferenceList`, [
+        Thing.composite.withReverseReferenceList({
+          data: 'trackData',
+          refList: refListProperty,
+          originalTracksOnly: true,
+        }),
+
+        {
+          flags: {expose: true},
+          expose: {
+            dependencies: ['#reverseReferenceList'],
+            compute: ({'#reverseReferenceList': reverseReferenceList}) =>
+              reverseReferenceList.filter(track => !track.originalReleaseTrack),
+          },
+        },
+      ]);
+    },
+  };
 
-    const albumName =
-      (album
-        ? album.name
-        : dataSourceAlbum?.name);
+  [inspect.custom](depth) {
+    const parts = [];
 
-    const albumIndex =
-      albumName &&
-        (album
-          ? album.tracks.indexOf(this)
-          : dataSourceAlbum.tracks.indexOf(this));
+    parts.push(Thing.prototype[inspect.custom].apply(this));
 
-    const trackNum =
-      albumName &&
+    if (this.originalReleaseTrackByRef) {
+      parts.unshift(`${color.yellow('[rerelease]')} `);
+    }
+
+    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(` (${color.yellow(trackNum)} in ${color.green(albumName)})`);
+    }
 
-    const albumPart =
-      albumName
-        ? ` (${color.yellow(trackNum)} in ${color.green(albumName)})`
-        : ``;
-
-    return rereleasePart + base + albumPart;
+    return parts.join('');
   }
 }
diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js
index e906cab1..e8279987 100644
--- a/src/data/things/wiki-info.js
+++ b/src/data/things/wiki-info.js
@@ -59,10 +59,6 @@ export class WikiInfo extends Thing {
 
     // Expose only
 
-    divideTrackListsByGroups: Thing.common.dynamicThingsFromReferenceList(
-      'divideTrackListsByGroupsByRef',
-      'groupData',
-      find.group
-    ),
+    divideTrackListsByGroups: Thing.common.dynamicThingsFromReferenceList('divideTrackListsByGroupsByRef', 'groupData', find.group),
   });
 }
diff --git a/src/data/yaml.js b/src/data/yaml.js
index 35943199..2ad2d41d 100644
--- a/src/data/yaml.js
+++ b/src/data/yaml.js
@@ -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.
 
     lyrics: 'Lyrics',
     commentary: 'Commentary',
@@ -1316,13 +1320,27 @@ export async function loadAndProcessDataDocuments({dataPath}) {
 
 // Data linking! Basically, provide (portions of) wikiData to the Things which
 // require it - they'll expose dynamically computed properties as a result (many
-// of which are required for page HTML generation).
-export function linkWikiDataArrays(wikiData) {
+// of which are required for page HTML generation and other expected behavior).
+//
+// The XXX_decacheWikiData option should be used specifically to mark
+// points where you *aren't* replacing any of the arrays under wikiData with
+// new values, and are using linkWikiDataArrays to instead "decache" data
+// properties which depend on any of them. It's currently not possible for
+// a CacheableObject to depend directly on the value of a property exposed
+// on some other CacheableObject, so when those values change, you have to
+// manually decache before the object will realize its cache isn't valid
+// anymore.
+export function linkWikiDataArrays(wikiData, {
+  XXX_decacheWikiData = false,
+} = {}) {
   function assignWikiData(things, ...keys) {
+    if (things === undefined) return;
     for (let i = 0; i < things.length; i++) {
       const thing = things[i];
       for (let j = 0; j < keys.length; j++) {
         const key = keys[j];
+        if (!(key in wikiData)) continue;
+        if (XXX_decacheWikiData) thing[key] = [];
         thing[key] = wikiData[key];
       }
     }
@@ -1340,7 +1358,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) {
diff --git a/src/util/sugar.js b/src/util/sugar.js
index 5b1f3193..1ba3f3ae 100644
--- a/src/util/sugar.js
+++ b/src/util/sugar.js
@@ -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 ${object}`);
+  }
+
+  if (!Array.isArray(properties)) {
+    throw new TypeError(`Expected properties to be an array, got ${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) {