« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/data
diff options
context:
space:
mode:
Diffstat (limited to 'src/data')
-rw-r--r--src/data/composite/things/artist/artistTotalDuration.js17
-rw-r--r--src/data/composite/things/flash-act/withFlashSide.js6
-rw-r--r--src/data/composite/things/flash/withFlashAct.js6
-rw-r--r--src/data/composite/things/track-section/withAlbum.js6
-rw-r--r--src/data/composite/things/track/index.js1
-rw-r--r--src/data/composite/things/track/trackReverseReferenceList.js38
-rw-r--r--src/data/composite/things/track/withAlbum.js6
-rw-r--r--src/data/composite/things/track/withContainingTrackSection.js6
-rw-r--r--src/data/composite/wiki-data/gobbleSoupyReverse.js39
-rw-r--r--src/data/composite/wiki-data/helpers/withResolvedReverse.js40
-rw-r--r--src/data/composite/wiki-data/helpers/withReverseList-template.js193
-rw-r--r--src/data/composite/wiki-data/index.js5
-rw-r--r--src/data/composite/wiki-data/inputSoupyReverse.js32
-rw-r--r--src/data/composite/wiki-data/withReverseAnnotatedReferenceList.js116
-rw-r--r--src/data/composite/wiki-data/withReverseContributionList.js42
-rw-r--r--src/data/composite/wiki-data/withReverseReferenceList.js46
-rw-r--r--src/data/composite/wiki-data/withReverseSingleReferenceList.js50
-rw-r--r--src/data/composite/wiki-data/withUniqueReferencingThing.js41
-rw-r--r--src/data/composite/wiki-properties/index.js5
-rw-r--r--src/data/composite/wiki-properties/reverseAnnotatedReferenceList.js33
-rw-r--r--src/data/composite/wiki-properties/reverseContributionList.js24
-rw-r--r--src/data/composite/wiki-properties/reverseReferenceList.js12
-rw-r--r--src/data/composite/wiki-properties/reverseReferencedArtworkList.js39
-rw-r--r--src/data/composite/wiki-properties/reverseSingleReferenceList.js24
-rw-r--r--src/data/composite/wiki-properties/soupyReverse.js22
-rw-r--r--src/data/things/album.js71
-rw-r--r--src/data/things/art-tag.js21
-rw-r--r--src/data/things/artist.js83
-rw-r--r--src/data/things/flash.js50
-rw-r--r--src/data/things/group.js58
-rw-r--r--src/data/things/track.js54
-rw-r--r--src/data/yaml.js28
32 files changed, 414 insertions, 800 deletions
diff --git a/src/data/composite/things/artist/artistTotalDuration.js b/src/data/composite/things/artist/artistTotalDuration.js
index ff709f28..a4a33542 100644
--- a/src/data/composite/things/artist/artistTotalDuration.js
+++ b/src/data/composite/things/artist/artistTotalDuration.js
@@ -2,8 +2,9 @@ import {input, templateCompositeFrom} from '#composite';
 
 import {exposeDependency} from '#composite/control-flow';
 import {withFilteredList, withPropertyFromList} from '#composite/data';
-import {withContributionListSums, withReverseContributionList}
+import {withContributionListSums, withReverseReferenceList}
   from '#composite/wiki-data';
+import {soupyReverse} from '#composite/wiki-properties';
 
 export default templateCompositeFrom({
   annotation: `artistTotalDuration`,
@@ -11,18 +12,16 @@ export default templateCompositeFrom({
   compose: false,
 
   steps: () => [
-    withReverseContributionList({
-      data: 'trackData',
-      list: input.value('artistContribs'),
+    withReverseReferenceList({
+      reverse: soupyReverse.input('trackArtistContributionsBy'),
     }).outputs({
-      '#reverseContributionList': '#contributionsAsArtist',
+      '#reverseReferenceList': '#contributionsAsArtist',
     }),
 
-    withReverseContributionList({
-      data: 'trackData',
-      list: input.value('contributorContribs'),
+    withReverseReferenceList({
+      reverse: soupyReverse.input('trackContributorContributionsBy'),
     }).outputs({
-      '#reverseContributionList': '#contributionsAsContributor',
+      '#reverseReferenceList': '#contributionsAsContributor',
     }),
 
     {
diff --git a/src/data/composite/things/flash-act/withFlashSide.js b/src/data/composite/things/flash-act/withFlashSide.js
index 64daa1fb..e09f06e6 100644
--- a/src/data/composite/things/flash-act/withFlashSide.js
+++ b/src/data/composite/things/flash-act/withFlashSide.js
@@ -2,9 +2,10 @@
 // If there's no side whose list of flash acts includes this act, the output
 // dependency will be null.
 
-import {input, templateCompositeFrom} from '#composite';
+import {templateCompositeFrom} from '#composite';
 
 import {withUniqueReferencingThing} from '#composite/wiki-data';
+import {soupyReverse} from '#composite/wiki-properties';
 
 export default templateCompositeFrom({
   annotation: `withFlashSide`,
@@ -13,8 +14,7 @@ export default templateCompositeFrom({
 
   steps: () => [
     withUniqueReferencingThing({
-      data: 'flashSideData',
-      list: input.value('acts'),
+      reverse: soupyReverse.input('flashSidesWhoseActsInclude'),
     }).outputs({
       ['#uniqueReferencingThing']: '#flashSide',
     }),
diff --git a/src/data/composite/things/flash/withFlashAct.js b/src/data/composite/things/flash/withFlashAct.js
index 652b8bfb..87922aff 100644
--- a/src/data/composite/things/flash/withFlashAct.js
+++ b/src/data/composite/things/flash/withFlashAct.js
@@ -2,9 +2,10 @@
 // If there's no flash whose list of flashes includes this flash, the output
 // dependency will be null.
 
-import {input, templateCompositeFrom} from '#composite';
+import {templateCompositeFrom} from '#composite';
 
 import {withUniqueReferencingThing} from '#composite/wiki-data';
+import {soupyReverse} from '#composite/wiki-properties';
 
 export default templateCompositeFrom({
   annotation: `withFlashAct`,
@@ -13,8 +14,7 @@ export default templateCompositeFrom({
 
   steps: () => [
     withUniqueReferencingThing({
-      data: 'flashActData',
-      list: input.value('flashes'),
+      reverse: soupyReverse.input('flashActsWhoseFlashesInclude'),
     }).outputs({
       ['#uniqueReferencingThing']: '#flashAct',
     }),
diff --git a/src/data/composite/things/track-section/withAlbum.js b/src/data/composite/things/track-section/withAlbum.js
index a4dfff0d..e257062e 100644
--- a/src/data/composite/things/track-section/withAlbum.js
+++ b/src/data/composite/things/track-section/withAlbum.js
@@ -1,8 +1,9 @@
 // Gets the track section's album.
 
-import {input, templateCompositeFrom} from '#composite';
+import {templateCompositeFrom} from '#composite';
 
 import {withUniqueReferencingThing} from '#composite/wiki-data';
+import {soupyReverse} from '#composite/wiki-properties';
 
 export default templateCompositeFrom({
   annotation: `withAlbum`,
@@ -11,8 +12,7 @@ export default templateCompositeFrom({
 
   steps: () => [
     withUniqueReferencingThing({
-      data: 'albumData',
-      list: input.value('trackSections'),
+      reverse: soupyReverse.input('albumsWhoseTrackSectionsInclude'),
     }).outputs({
       ['#uniqueReferencingThing']: '#album',
     }),
diff --git a/src/data/composite/things/track/index.js b/src/data/composite/things/track/index.js
index 05ccaaba..32c72f78 100644
--- a/src/data/composite/things/track/index.js
+++ b/src/data/composite/things/track/index.js
@@ -1,7 +1,6 @@
 export {default as exitWithoutUniqueCoverArt} from './exitWithoutUniqueCoverArt.js';
 export {default as inheritContributionListFromOriginalRelease} from './inheritContributionListFromOriginalRelease.js';
 export {default as inheritFromOriginalRelease} from './inheritFromOriginalRelease.js';
-export {default as trackReverseReferenceList} from './trackReverseReferenceList.js';
 export {default as withAlbum} from './withAlbum.js';
 export {default as withAlwaysReferenceByDirectory} from './withAlwaysReferenceByDirectory.js';
 export {default as withContainingTrackSection} from './withContainingTrackSection.js';
diff --git a/src/data/composite/things/track/trackReverseReferenceList.js b/src/data/composite/things/track/trackReverseReferenceList.js
deleted file mode 100644
index 44940ae7..00000000
--- a/src/data/composite/things/track/trackReverseReferenceList.js
+++ /dev/null
@@ -1,38 +0,0 @@
-// Like a normal reverse reference list ("objects which reference this object
-// under a specified property"), only excluding rereleases from the possible
-// outputs. While it's useful to travel from a rerelease to the tracks it
-// references, rereleases aren't generally relevant from the perspective of
-// the tracks *being* referenced. Apart from hiding rereleases from lists on
-// the site, it also excludes keeps them from relational data processing, such
-// as on the "Tracks - by Times Referenced" listing page.
-
-import {input, templateCompositeFrom} from '#composite';
-import {withReverseReferenceList} from '#composite/wiki-data';
-
-export default templateCompositeFrom({
-  annotation: `trackReverseReferenceList`,
-
-  compose: false,
-
-  inputs: {
-    list: input({type: 'string'}),
-  },
-
-  steps: () => [
-    withReverseReferenceList({
-      data: 'trackData',
-      list: input('list'),
-    }),
-
-    {
-      flags: {expose: true},
-      expose: {
-        dependencies: ['#reverseReferenceList'],
-        compute: ({
-          ['#reverseReferenceList']: reverseReferenceList,
-        }) =>
-          reverseReferenceList.filter(track => !track.originalReleaseTrack),
-      },
-    },
-  ],
-});
diff --git a/src/data/composite/things/track/withAlbum.js b/src/data/composite/things/track/withAlbum.js
index 03b840d4..4c55e1f4 100644
--- a/src/data/composite/things/track/withAlbum.js
+++ b/src/data/composite/things/track/withAlbum.js
@@ -2,9 +2,10 @@
 // If there's no album whose list of tracks includes this track, the output
 // dependency will be null.
 
-import {input, templateCompositeFrom} from '#composite';
+import {templateCompositeFrom} from '#composite';
 
 import {withUniqueReferencingThing} from '#composite/wiki-data';
+import {soupyReverse} from '#composite/wiki-properties';
 
 export default templateCompositeFrom({
   annotation: `withAlbum`,
@@ -13,8 +14,7 @@ export default templateCompositeFrom({
 
   steps: () => [
     withUniqueReferencingThing({
-      data: 'albumData',
-      list: input.value('tracks'),
+      reverse: soupyReverse.input('albumsWhoseTracksInclude'),
     }).outputs({
       ['#uniqueReferencingThing']: '#album',
     }),
diff --git a/src/data/composite/things/track/withContainingTrackSection.js b/src/data/composite/things/track/withContainingTrackSection.js
index 9bbd9bd5..3d4d081e 100644
--- a/src/data/composite/things/track/withContainingTrackSection.js
+++ b/src/data/composite/things/track/withContainingTrackSection.js
@@ -1,8 +1,9 @@
 // Gets the track section containing this track from its album's track list.
 
-import {input, templateCompositeFrom} from '#composite';
+import {templateCompositeFrom} from '#composite';
 
 import {withUniqueReferencingThing} from '#composite/wiki-data';
+import {soupyReverse} from '#composite/wiki-properties';
 
 export default templateCompositeFrom({
   annotation: `withContainingTrackSection`,
@@ -11,8 +12,7 @@ export default templateCompositeFrom({
 
   steps: () => [
     withUniqueReferencingThing({
-      data: 'trackSectionData',
-      list: input.value('tracks'),
+      reverse: soupyReverse.input('trackSectionsWhichInclude'),
     }).outputs({
       ['#uniqueReferencingThing']: '#trackSection',
     }),
diff --git a/src/data/composite/wiki-data/gobbleSoupyReverse.js b/src/data/composite/wiki-data/gobbleSoupyReverse.js
new file mode 100644
index 00000000..86a1061c
--- /dev/null
+++ b/src/data/composite/wiki-data/gobbleSoupyReverse.js
@@ -0,0 +1,39 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {withPropertyFromObject} from '#composite/data';
+
+import inputSoupyReverse, {getSoupyReverseInputKey} from './inputSoupyReverse.js';
+
+export default templateCompositeFrom({
+  annotation: `gobbleSoupyReverse`,
+
+  inputs: {
+    reverse: inputSoupyReverse(),
+  },
+
+  outputs: ['#reverse'],
+
+  steps: () => [
+    {
+      dependencies: [input('reverse')],
+      compute: (continuation, {
+        [input('reverse')]: reverse,
+      }) =>
+        (typeof reverse === 'function'
+          ? continuation.raiseOutput({
+              ['#reverse']: reverse,
+            })
+          : continuation({
+              ['#key']:
+                getSoupyReverseInputKey(reverse),
+            })),
+    },
+
+    withPropertyFromObject({
+      object: 'reverse',
+      property: '#key',
+    }).outputs({
+      '#value': '#reverse',
+    }),
+  ],
+});
diff --git a/src/data/composite/wiki-data/helpers/withResolvedReverse.js b/src/data/composite/wiki-data/helpers/withResolvedReverse.js
new file mode 100644
index 00000000..818f60b7
--- /dev/null
+++ b/src/data/composite/wiki-data/helpers/withResolvedReverse.js
@@ -0,0 +1,40 @@
+// Actually execute a reverse function.
+
+import {input, templateCompositeFrom} from '#composite';
+
+import inputWikiData from '../inputWikiData.js';
+
+export default templateCompositeFrom({
+  annotation: `withReverseReferenceList`,
+
+  inputs: {
+    data: inputWikiData({allowMixedTypes: true}),
+    reverse: input({type: 'function'}),
+    options: input({type: 'object', defaultValue: null}),
+  },
+
+  outputs: ['#resolvedReverse'],
+
+  steps: () => [
+    {
+      dependencies: [
+        input.myself(),
+        input('data'),
+        input('reverse'),
+        input('options'),
+      ],
+
+      compute: (continuation, {
+        [input.myself()]: myself,
+        [input('data')]: data,
+        [input('reverse')]: reverseFunction,
+        [input('options')]: opts,
+      }) => continuation({
+        ['#resolvedReverse']:
+          (data
+            ? reverseFunction(myself, data, opts)
+            : reverseFunction(myself, opts)),
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/wiki-data/helpers/withReverseList-template.js b/src/data/composite/wiki-data/helpers/withReverseList-template.js
deleted file mode 100644
index 6ffd5d70..00000000
--- a/src/data/composite/wiki-data/helpers/withReverseList-template.js
+++ /dev/null
@@ -1,193 +0,0 @@
-// Baseline implementation shared by or underlying reverse lists.
-//
-// This is a very rudimentary "these compositions have basically the same
-// shape but slightly different guts midway through" kind of solution,
-// and should use compositional subroutines instead, once those are ready.
-//
-// But, until then, this has the same effect of avoiding code duplication
-// and clearly identifying differences.
-//
-// ---
-//
-// This implementation uses a global cache (via WeakMap) to attempt to speed
-// up subsequent similar accesses.
-//
-// This has absolutely not been rigorously tested with altering properties of
-// data objects in a wiki data array which is reused. If a new wiki data array
-// is used, a fresh cache will always be created.
-//
-
-import {input, templateCompositeFrom} from '#composite';
-import {sortByDate} from '#sort';
-import {stitchArrays} from '#sugar';
-
-import {exitWithoutDependency, raiseOutputWithoutDependency}
-  from '#composite/control-flow';
-import {withFlattenedList, withMappedList} from '#composite/data';
-
-import inputWikiData from '../inputWikiData.js';
-
-export default function withReverseList_template({
-  annotation,
-
-  propertyInputName,
-  outputName,
-
-  additionalInputs = {},
-
-  customCompositionSteps,
-}) {
-  // Mapping of reference list property to WeakMap.
-  // Each WeakMap maps a wiki data array to another weak map,
-  // which in turn maps each referenced thing to an array of
-  // things referencing it.
-  const caches = new Map();
-
-  return templateCompositeFrom({
-    annotation,
-
-    inputs: {
-      data: inputWikiData({
-        allowMixedTypes: true,
-      }),
-
-      [propertyInputName]: input({
-        type: 'string',
-      }),
-
-      ...additionalInputs,
-    },
-
-    outputs: [outputName],
-
-    steps: () => [
-      // Early exit with an empty array if the data list isn't available.
-      exitWithoutDependency({
-        dependency: input('data'),
-        value: input.value([]),
-      }),
-
-      // Raise an empty array (don't early exit) if the data list is empty.
-      raiseOutputWithoutDependency({
-        dependency: input('data'),
-        mode: input.value('empty'),
-        output: input.value({[outputName]: []}),
-      }),
-
-      // Check for an existing cache record which corresponds to this
-      // property input and input('data'). If it exists, query it for the
-      // current thing, and raise that; if it doesn't, create it, put it
-      // where it needs to be, and provide it so the next steps can fill
-      // it in.
-      {
-        dependencies: [input(propertyInputName), input('data'), input.myself()],
-
-        compute: (continuation, {
-          [input(propertyInputName)]: property,
-          [input('data')]: data,
-          [input.myself()]: myself,
-        }) => {
-          if (!caches.has(property)) {
-            const cache = new WeakMap();
-            caches.set(property, cache);
-
-            const cacheRecord = new WeakMap();
-            cache.set(data, cacheRecord);
-
-            return continuation({
-              ['#cacheRecord']: cacheRecord,
-            });
-          }
-
-          const cache = caches.get(property);
-
-          if (!cache.has(data)) {
-            const cacheRecord = new WeakMap();
-            cache.set(data, cacheRecord);
-
-            return continuation({
-              ['#cacheRecord']: cacheRecord,
-            });
-          }
-
-          return continuation.raiseOutput({
-            [outputName]:
-              cache.get(data).get(myself) ?? [],
-          });
-        },
-      },
-
-      ...customCompositionSteps(),
-
-      // Actually fill in the cache record. Since we're building up a *reverse*
-      // reference list, track connections in terms of the referenced thing.
-      // Although we gather all referenced things into a set and provide that
-      // for sorting purposes in the next step, we *don't* reprovide the cache
-      // record, because we're mutating that in-place - we'll just reuse its
-      // existing '#cacheRecord' dependency.
-      {
-        dependencies: ['#cacheRecord', '#referencingThings', '#referencedThings'],
-        compute: (continuation, {
-          ['#cacheRecord']: cacheRecord,
-          ['#referencingThings']: referencingThings,
-          ['#referencedThings']: referencedThings,
-        }) => {
-          const allReferencedThings = new Set();
-
-          stitchArrays({
-            referencingThing: referencingThings,
-            referencedThings: referencedThings,
-          }).forEach(({referencingThing, referencedThings}) => {
-              for (const referencedThing of referencedThings) {
-                if (cacheRecord.has(referencedThing)) {
-                  cacheRecord.get(referencedThing).push(referencingThing);
-                } else {
-                  cacheRecord.set(referencedThing, [referencingThing]);
-                  allReferencedThings.add(referencedThing);
-                }
-              }
-            });
-
-          return continuation({
-            ['#allReferencedThings']:
-              allReferencedThings,
-          });
-        },
-      },
-
-      // Sort the entries in the cache records, too, just by date - the rest of
-      // sorting should be handled outside of this composition, either preceding
-      // (changing the 'data' input) or following (sorting the output).
-      // Again we're mutating in place, so no need to reprovide '#cacheRecord'
-      // here.
-      {
-        dependencies: ['#cacheRecord', '#allReferencedThings'],
-        compute: (continuation, {
-          ['#cacheRecord']: cacheRecord,
-          ['#allReferencedThings']: allReferencedThings,
-        }) => {
-          for (const referencedThing of allReferencedThings) {
-            if (cacheRecord.has(referencedThing)) {
-              const referencingThings = cacheRecord.get(referencedThing);
-              sortByDate(referencingThings);
-            }
-          }
-
-          return continuation();
-        },
-      },
-
-      // Then just pluck out the current object from the now-filled cache record!
-      {
-        dependencies: ['#cacheRecord', input.myself()],
-        compute: (continuation, {
-          ['#cacheRecord']: cacheRecord,
-          [input.myself()]: myself,
-        }) => continuation({
-          [outputName]:
-            cacheRecord.get(myself) ?? [],
-        }),
-      },
-    ],
-  });
-}
diff --git a/src/data/composite/wiki-data/index.js b/src/data/composite/wiki-data/index.js
index 294ddb2a..be83e4c9 100644
--- a/src/data/composite/wiki-data/index.js
+++ b/src/data/composite/wiki-data/index.js
@@ -6,8 +6,10 @@
 
 export {default as exitWithoutContribs} from './exitWithoutContribs.js';
 export {default as gobbleSoupyFind} from './gobbleSoupyFind.js';
+export {default as gobbleSoupyReverse} from './gobbleSoupyReverse.js';
 export {default as inputNotFoundMode} from './inputNotFoundMode.js';
 export {default as inputSoupyFind} from './inputSoupyFind.js';
+export {default as inputSoupyReverse} from './inputSoupyReverse.js';
 export {default as inputWikiData} from './inputWikiData.js';
 export {default as withClonedThings} from './withClonedThings.js';
 export {default as withContributionListSums} from './withContributionListSums.js';
@@ -21,9 +23,6 @@ export {default as withResolvedContribs} from './withResolvedContribs.js';
 export {default as withResolvedReference} from './withResolvedReference.js';
 export {default as withResolvedReferenceList} from './withResolvedReferenceList.js';
 export {default as withResolvedSeriesList} from './withResolvedSeriesList.js';
-export {default as withReverseAnnotatedReferenceList} from './withReverseAnnotatedReferenceList.js';
-export {default as withReverseContributionList} from './withReverseContributionList.js';
 export {default as withReverseReferenceList} from './withReverseReferenceList.js';
-export {default as withReverseSingleReferenceList} from './withReverseSingleReferenceList.js';
 export {default as withThingsSortedAlphabetically} from './withThingsSortedAlphabetically.js';
 export {default as withUniqueReferencingThing} from './withUniqueReferencingThing.js';
diff --git a/src/data/composite/wiki-data/inputSoupyReverse.js b/src/data/composite/wiki-data/inputSoupyReverse.js
new file mode 100644
index 00000000..0b0a23fe
--- /dev/null
+++ b/src/data/composite/wiki-data/inputSoupyReverse.js
@@ -0,0 +1,32 @@
+import {input} from '#composite';
+import {anyOf, isFunction, isString} from '#validators';
+
+function inputSoupyReverse() {
+  return input({
+    validate:
+      anyOf(
+        isFunction,
+        val => {
+          isString(val);
+
+          if (!val.startsWith('_soupyReverse:')) {
+            throw new Error(`Expected soupyReverse.input() token`);
+          }
+
+          return true;
+        }),
+  });
+}
+
+inputSoupyReverse.input = key =>
+  input.value('_soupyReverse:' + key);
+
+export default inputSoupyReverse;
+
+export function getSoupyReverseInputKey(value) {
+  return value.slice('_soupyReverse:'.length).replace(/\.unique$/, '');
+}
+
+export function doesSoupyReverseInputWantUnique(value) {
+  return value.endsWith('.unique');
+}
diff --git a/src/data/composite/wiki-data/withReverseAnnotatedReferenceList.js b/src/data/composite/wiki-data/withReverseAnnotatedReferenceList.js
deleted file mode 100644
index feae9ccb..00000000
--- a/src/data/composite/wiki-data/withReverseAnnotatedReferenceList.js
+++ /dev/null
@@ -1,116 +0,0 @@
-// Analogous implementation for withReverseReferenceList, for annotated
-// references.
-//
-// Unlike withReverseContributionList, this composition is responsible for
-// "flipping" the directionality of references: in a forward reference list,
-// `thing` points to the thing being referenced, while here, it points to the
-// referencing thing.
-//
-// This behavior can be customized to respect reference lists which are shaped
-// differently than the default and/or to customize the reversed property and
-// provide a less generic label than just "thing".
-
-import withReverseList_template from './helpers/withReverseList-template.js';
-
-import {input} from '#composite';
-import {stitchArrays} from '#sugar';
-
-import {
-  withFlattenedList,
-  withMappedList,
-  withPropertyFromList,
-  withStretchedList,
-} from '#composite/data';
-
-export default withReverseList_template({
-  annotation: `withReverseAnnotatedReferenceList`,
-
-  propertyInputName: 'list',
-  outputName: '#reverseAnnotatedReferenceList',
-
-  additionalInputs: {
-    forward: input({type: 'string', defaultValue: 'thing'}),
-    backward: input({type: 'string', defaultValue: 'thing'}),
-    annotation: input({type: 'string', defaultValue: 'annotation'}),
-  },
-
-  customCompositionSteps: () => [
-    withPropertyFromList({
-      list: input('data'),
-      property: input('list'),
-    }).outputs({
-      '#values': '#referenceLists',
-    }),
-
-    withPropertyFromList({
-      list: '#referenceLists',
-      property: input.value('length'),
-    }),
-
-    withFlattenedList({
-      list: '#referenceLists',
-    }).outputs({
-      '#flattenedList': '#references',
-    }),
-
-    withStretchedList({
-      list: input('data'),
-      lengths: '#referenceLists.length',
-    }).outputs({
-      '#stretchedList': '#things',
-    }),
-
-    withPropertyFromList({
-      list: '#references',
-      property: input('annotation'),
-    }).outputs({
-      '#values': '#annotations',
-    }),
-
-    withPropertyFromList({
-      list: '#references',
-      property: input.value('date'),
-    }).outputs({
-      '#references.date': '#dates',
-    }),
-
-    {
-      dependencies: [
-        input('backward'),
-        input('annotation'),
-        '#things',
-        '#annotations',
-        '#dates',
-      ],
-
-      compute: (continuation, {
-        [input('backward')]: thingProperty,
-        [input('annotation')]: annotationProperty,
-        ['#things']: things,
-        ['#annotations']: annotations,
-        ['#dates']: dates,
-      }) => continuation({
-        '#referencingThings':
-          stitchArrays({
-            [thingProperty]: things,
-            [annotationProperty]: annotations,
-            date: dates,
-          }),
-      }),
-    },
-
-    withPropertyFromList({
-      list: '#references',
-      property: input('forward'),
-    }).outputs({
-      '#values': '#individualReferencedThings',
-    }),
-
-    withMappedList({
-      list: '#individualReferencedThings',
-      map: input.value(thing => [thing]),
-    }).outputs({
-      '#mappedList': '#referencedThings',
-    }),
-  ],
-});
diff --git a/src/data/composite/wiki-data/withReverseContributionList.js b/src/data/composite/wiki-data/withReverseContributionList.js
deleted file mode 100644
index 04dc52d7..00000000
--- a/src/data/composite/wiki-data/withReverseContributionList.js
+++ /dev/null
@@ -1,42 +0,0 @@
-// Analogous implementation for withReverseReferenceList, for contributions.
-
-import withReverseList_template from './helpers/withReverseList-template.js';
-
-import {input} from '#composite';
-
-import {withFlattenedList, withMappedList, withPropertyFromList}
-  from '#composite/data';
-
-export default withReverseList_template({
-  annotation: `withReverseContributionList`,
-
-  propertyInputName: 'list',
-  outputName: '#reverseContributionList',
-
-  customCompositionSteps: () => [
-    withPropertyFromList({
-      list: input('data'),
-      property: input('list'),
-    }).outputs({
-      '#values': '#contributionLists',
-    }),
-
-    withFlattenedList({
-      list: '#contributionLists',
-    }).outputs({
-      '#flattenedList': '#referencingThings',
-    }),
-
-    withPropertyFromList({
-      list: '#referencingThings',
-      property: input.value('artist'),
-    }),
-
-    withMappedList({
-      list: '#referencingThings.artist',
-      map: input.value(artist => [artist]),
-    }).outputs({
-      '#mappedList': '#referencedThings',
-    }),
-  ],
-});
diff --git a/src/data/composite/wiki-data/withReverseReferenceList.js b/src/data/composite/wiki-data/withReverseReferenceList.js
index c62408cf..906f5bc5 100644
--- a/src/data/composite/wiki-data/withReverseReferenceList.js
+++ b/src/data/composite/wiki-data/withReverseReferenceList.js
@@ -1,34 +1,36 @@
 // Check out the info on reverseReferenceList!
 // This is its composable form.
 
-import withReverseList_template from './helpers/withReverseList-template.js';
+import {input, templateCompositeFrom} from '#composite';
 
-import {input} from '#composite';
+import gobbleSoupyReverse from './gobbleSoupyReverse.js';
+import inputSoupyReverse from './inputSoupyReverse.js';
+import inputWikiData from './inputWikiData.js';
 
-import {withPropertyFromList} from '#composite/data';
+import withResolvedReverse from './helpers/withResolvedReverse.js';
 
-export default withReverseList_template({
+export default templateCompositeFrom({
   annotation: `withReverseReferenceList`,
 
-  propertyInputName: 'list',
-  outputName: '#reverseReferenceList',
-
-  customCompositionSteps: () => [
-    {
-      dependencies: [input('data')],
-      compute: (continuation, {
-        [input('data')]: data,
-      }) => continuation({
-        ['#referencingThings']:
-          data,
-      }),
-    },
-
-    withPropertyFromList({
-      list: '#referencingThings',
-      property: input('list'),
+  inputs: {
+    data: inputWikiData({allowMixedTypes: true}),
+    reverse: inputSoupyReverse(),
+  },
+
+  outputs: ['#reverseReferenceList'],
+
+  steps: () => [
+    gobbleSoupyReverse({
+      reverse: input('reverse'),
+    }),
+
+    // TODO: Check that the reverse spec returns a list.
+
+    withResolvedReverse({
+      data: input('data'),
+      reverse: '#reverse',
     }).outputs({
-      '#values': '#referencedThings',
+      '#resolvedReverse': '#reverseReferenceList',
     }),
   ],
 });
diff --git a/src/data/composite/wiki-data/withReverseSingleReferenceList.js b/src/data/composite/wiki-data/withReverseSingleReferenceList.js
deleted file mode 100644
index 569e9ba0..00000000
--- a/src/data/composite/wiki-data/withReverseSingleReferenceList.js
+++ /dev/null
@@ -1,50 +0,0 @@
-// Like withReverseReferenceList, but for finding all things which reference
-// the current thing by a property that contains a single reference, rather
-// than within a reference list.
-
-import withReverseList_template from './helpers/withReverseList-template.js';
-
-import {input} from '#composite';
-
-import {withAvailabilityFilter} from '#composite/control-flow';
-import {withMappedList, withPropertyFromList} from '#composite/data';
-
-export default withReverseList_template({
-  annotation: `withReverseSingleReferenceList`,
-
-  propertyInputName: 'ref',
-  outputName: '#reverseSingleReferenceList',
-
-  customCompositionSteps: () => [
-    {
-      dependencies: [input('data')],
-      compute: (continuation, {
-        [input('data')]: data,
-      }) => continuation({
-        ['#referencingThings']:
-          data,
-      }),
-    },
-
-    withPropertyFromList({
-      list: '#referencingThings',
-      property: input('ref'),
-    }).outputs({
-      '#values': '#individualReferencedThings',
-    }),
-
-    withAvailabilityFilter({
-      from: '#individualReferencedThings',
-    }),
-
-    // This map wraps each referenced thing in a single-item array.
-    // Each referencing thing references exactly one thing, if any.
-    withMappedList({
-      list: '#individualReferencedThings',
-      filter: '#availabilityFilter',
-      map: input.value(thing => [thing]),
-    }).outputs({
-      '#mappedList': '#referencedThings',
-    }),
-  ],
-});
diff --git a/src/data/composite/wiki-data/withUniqueReferencingThing.js b/src/data/composite/wiki-data/withUniqueReferencingThing.js
index 61c10618..7c267038 100644
--- a/src/data/composite/wiki-data/withUniqueReferencingThing.js
+++ b/src/data/composite/wiki-data/withUniqueReferencingThing.js
@@ -4,48 +4,33 @@
 
 import {input, templateCompositeFrom} from '#composite';
 
-import {exitWithoutDependency, raiseOutputWithoutDependency}
-  from '#composite/control-flow';
-
+import gobbleSoupyReverse from './gobbleSoupyReverse.js';
+import inputSoupyReverse from './inputSoupyReverse.js';
 import inputWikiData from './inputWikiData.js';
-import withReverseReferenceList from './withReverseReferenceList.js';
+
+import withResolvedReverse from './helpers/withResolvedReverse.js';
 
 export default templateCompositeFrom({
   annotation: `withUniqueReferencingThing`,
 
   inputs: {
-    data: inputWikiData({allowMixedTypes: false}),
-    list: input({type: 'string'}),
+    data: inputWikiData({allowMixedTypes: true}),
+    reverse: inputSoupyReverse(),
   },
 
   outputs: ['#uniqueReferencingThing'],
 
   steps: () => [
-    // Early exit with null (not an empty array) if the data list
-    // isn't available.
-    exitWithoutDependency({
-      dependency: input('data'),
+    gobbleSoupyReverse({
+      reverse: input('reverse'),
     }),
 
-    withReverseReferenceList({
+    withResolvedReverse({
       data: input('data'),
-      list: input('list'),
+      reverse: '#reverse',
+      options: input.value({unique: true}),
+    }).outputs({
+      '#resolvedReverse': '#uniqueReferencingThing',
     }),
-
-    raiseOutputWithoutDependency({
-      dependency: '#reverseReferenceList',
-      mode: input.value('empty'),
-      output: input.value({'#uniqueReferencingThing': null}),
-    }),
-
-    {
-      dependencies: ['#reverseReferenceList'],
-      compute: (continuation, {
-        ['#reverseReferenceList']: reverseReferenceList,
-      }) => continuation({
-        ['#uniqueReferencingThing']:
-          reverseReferenceList[0],
-      }),
-    },
   ],
 });
diff --git a/src/data/composite/wiki-properties/index.js b/src/data/composite/wiki-properties/index.js
index d2e32e6d..4aaaeb72 100644
--- a/src/data/composite/wiki-properties/index.js
+++ b/src/data/composite/wiki-properties/index.js
@@ -21,16 +21,13 @@ export {default as flag} from './flag.js';
 export {default as name} from './name.js';
 export {default as referenceList} from './referenceList.js';
 export {default as referencedArtworkList} from './referencedArtworkList.js';
-export {default as reverseAnnotatedReferenceList} from './reverseAnnotatedReferenceList.js';
-export {default as reverseContributionList} from './reverseContributionList.js';
 export {default as reverseReferenceList} from './reverseReferenceList.js';
-export {default as reverseReferencedArtworkList} from './reverseReferencedArtworkList.js';
-export {default as reverseSingleReferenceList} from './reverseSingleReferenceList.js';
 export {default as seriesList} from './seriesList.js';
 export {default as simpleDate} from './simpleDate.js';
 export {default as simpleString} from './simpleString.js';
 export {default as singleReference} from './singleReference.js';
 export {default as soupyFind} from './soupyFind.js';
+export {default as soupyReverse} from './soupyReverse.js';
 export {default as thing} from './thing.js';
 export {default as thingList} from './thingList.js';
 export {default as urls} from './urls.js';
diff --git a/src/data/composite/wiki-properties/reverseAnnotatedReferenceList.js b/src/data/composite/wiki-properties/reverseAnnotatedReferenceList.js
deleted file mode 100644
index ba7166b9..00000000
--- a/src/data/composite/wiki-properties/reverseAnnotatedReferenceList.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import {input, templateCompositeFrom} from '#composite';
-
-import {exposeDependency} from '#composite/control-flow';
-import {inputWikiData, withReverseAnnotatedReferenceList}
-  from '#composite/wiki-data';
-
-export default templateCompositeFrom({
-  annotation: `reverseAnnotatedReferenceList`,
-
-  compose: false,
-
-  inputs: {
-    data: inputWikiData({allowMixedTypes: false}),
-    list: input({type: 'string'}),
-
-    forward: input({type: 'string', defaultValue: 'thing'}),
-    backward: input({type: 'string', defaultValue: 'thing'}),
-    annotation: input({type: 'string', defaultValue: 'annotation'}),
-  },
-
-  steps: () => [
-    withReverseAnnotatedReferenceList({
-      data: input('data'),
-      list: input('list'),
-
-      forward: input('forward'),
-      backward: input('backward'),
-      annotation: input('annotation'),
-    }),
-
-    exposeDependency({dependency: '#reverseAnnotatedReferenceList'}),
-  ],
-});
diff --git a/src/data/composite/wiki-properties/reverseContributionList.js b/src/data/composite/wiki-properties/reverseContributionList.js
deleted file mode 100644
index 7f3f9c81..00000000
--- a/src/data/composite/wiki-properties/reverseContributionList.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import {input, templateCompositeFrom} from '#composite';
-
-import {exposeDependency} from '#composite/control-flow';
-import {inputWikiData, withReverseContributionList} from '#composite/wiki-data';
-
-export default templateCompositeFrom({
-  annotation: `reverseContributionList`,
-
-  compose: false,
-
-  inputs: {
-    data: inputWikiData({allowMixedTypes: false}),
-    list: input({type: 'string'}),
-  },
-
-  steps: () => [
-    withReverseContributionList({
-      data: input('data'),
-      list: input('list'),
-    }),
-
-    exposeDependency({dependency: '#reverseContributionList'}),
-  ],
-});
diff --git a/src/data/composite/wiki-properties/reverseReferenceList.js b/src/data/composite/wiki-properties/reverseReferenceList.js
index 84ba67df..6d590a67 100644
--- a/src/data/composite/wiki-properties/reverseReferenceList.js
+++ b/src/data/composite/wiki-properties/reverseReferenceList.js
@@ -1,13 +1,13 @@
 // Neat little shortcut for "reversing" the reference lists stored on other
 // things - for example, tracks specify a "referenced tracks" property, and
 // you would use this to compute a corresponding "referenced *by* tracks"
-// property. Naturally, the passed ref list property is of the things in the
-// wiki data provided, not the requesting Thing itself.
+// property.
 
 import {input, templateCompositeFrom} from '#composite';
 
 import {exposeDependency} from '#composite/control-flow';
-import {inputWikiData, withReverseReferenceList} from '#composite/wiki-data';
+import {inputSoupyReverse, inputWikiData, withReverseReferenceList}
+  from '#composite/wiki-data';
 
 export default templateCompositeFrom({
   annotation: `reverseReferenceList`,
@@ -15,14 +15,14 @@ export default templateCompositeFrom({
   compose: false,
 
   inputs: {
-    data: inputWikiData({allowMixedTypes: false}),
-    list: input({type: 'string'}),
+    data: inputWikiData({allowMixedTypes: true}),
+    reverse: inputSoupyReverse(),
   },
 
   steps: () => [
     withReverseReferenceList({
       data: input('data'),
-      list: input('list'),
+      reverse: input('reverse'),
     }),
 
     exposeDependency({dependency: '#reverseReferenceList'}),
diff --git a/src/data/composite/wiki-properties/reverseReferencedArtworkList.js b/src/data/composite/wiki-properties/reverseReferencedArtworkList.js
deleted file mode 100644
index 2950bdb9..00000000
--- a/src/data/composite/wiki-properties/reverseReferencedArtworkList.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import {input, templateCompositeFrom} from '#composite';
-import {combineWikiDataArrays} from '#wiki-data';
-
-import {exposeDependency} from '#composite/control-flow';
-import {inputWikiData, withReverseAnnotatedReferenceList}
-  from '#composite/wiki-data';
-
-export default templateCompositeFrom({
-  annotation: `reverseReferencedArtworkList`,
-
-  compose: false,
-
-  steps: () => [
-    {
-      dependencies: [
-        'albumData',
-        'trackData',
-      ],
-
-      compute: (continuation, {
-        albumData,
-        trackData,
-      }) => continuation({
-        ['#data']:
-          combineWikiDataArrays([
-            albumData,
-            trackData,
-          ]),
-      }),
-    },
-
-    withReverseAnnotatedReferenceList({
-      data: '#data',
-      list: input.value('referencedArtworks'),
-    }),
-
-    exposeDependency({dependency: '#reverseAnnotatedReferenceList'}),
-  ],
-});
diff --git a/src/data/composite/wiki-properties/reverseSingleReferenceList.js b/src/data/composite/wiki-properties/reverseSingleReferenceList.js
deleted file mode 100644
index d180b12d..00000000
--- a/src/data/composite/wiki-properties/reverseSingleReferenceList.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import {input, templateCompositeFrom} from '#composite';
-
-import {exposeDependency} from '#composite/control-flow';
-import {inputWikiData, withReverseSingleReferenceList} from '#composite/wiki-data';
-
-export default templateCompositeFrom({
-  annotation: `reverseSingleReferenceList`,
-
-  compose: false,
-
-  inputs: {
-    data: inputWikiData({allowMixedTypes: false}),
-    ref: input({type: 'string'}),
-  },
-
-  steps: () => [
-    withReverseSingleReferenceList({
-      data: input('data'),
-      ref: input('ref'),
-    }),
-
-    exposeDependency({dependency: '#reverseSingleReferenceList'}),
-  ],
-});
diff --git a/src/data/composite/wiki-properties/soupyReverse.js b/src/data/composite/wiki-properties/soupyReverse.js
new file mode 100644
index 00000000..269ccd6f
--- /dev/null
+++ b/src/data/composite/wiki-properties/soupyReverse.js
@@ -0,0 +1,22 @@
+import {isObject} from '#validators';
+
+import {inputSoupyReverse} from '#composite/wiki-data';
+
+function soupyReverse() {
+  return {
+    flags: {update: true},
+    update: {validate: isObject},
+  };
+}
+
+soupyReverse.input = inputSoupyReverse.input;
+
+soupyReverse.contributionsBy =
+  (bindTo, contributionsProperty) => ({
+    bindTo,
+
+    referencing: thing => thing[contributionsProperty],
+    referenced: contrib => [contrib.artist],
+  });
+
+export default soupyReverse;
diff --git a/src/data/things/album.js b/src/data/things/album.js
index e4463d27..151b5087 100644
--- a/src/data/things/album.js
+++ b/src/data/things/album.js
@@ -44,10 +44,11 @@ import {
   name,
   referencedArtworkList,
   referenceList,
-  reverseReferencedArtworkList,
+  reverseReferenceList,
   simpleDate,
   simpleString,
   soupyFind,
+  soupyReverse,
   thing,
   thingList,
   urls,
@@ -262,6 +263,7 @@ export class Album extends Thing {
     // Update only
 
     find: soupyFind(),
+    reverse: soupyReverse(),
 
     // used for referencedArtworkList (mixedFind)
     albumData: wikiData({
@@ -297,7 +299,9 @@ export class Album extends Thing {
         value: input.value([]),
       }),
 
-      reverseReferencedArtworkList(),
+      reverseReferenceList({
+        reverse: soupyReverse.input('artworksWhichReference'),
+      }),
     ],
   });
 
@@ -356,6 +360,55 @@ export class Album extends Thing {
     },
   };
 
+  static [Thing.reverseSpecs] = {
+    albumsWhoseTracksInclude: {
+      bindTo: 'albumData',
+
+      referencing: album => [album],
+      referenced: album => album.tracks,
+    },
+
+    albumsWhoseTrackSectionsInclude: {
+      bindTo: 'albumData',
+
+      referencing: album => [album],
+      referenced: album => album.trackSections,
+    },
+
+    albumsWhoseArtworksFeature: {
+      bindTo: 'albumData',
+
+      referencing: album => [album],
+      referenced: album => album.artTags,
+    },
+
+    albumsWhoseGroupsInclude: {
+      bindTo: 'albumData',
+
+      referencing: album => [album],
+      referenced: album => album.groups,
+    },
+
+    albumArtistContributionsBy:
+      soupyReverse.contributionsBy('albumData', 'artistContribs'),
+
+    albumCoverArtistContributionsBy:
+      soupyReverse.contributionsBy('albumData', 'coverArtistContribs'),
+
+    albumWallpaperArtistContributionsBy:
+      soupyReverse.contributionsBy('albumData', 'wallpaperArtistContribs'),
+
+    albumBannerArtistContributionsBy:
+      soupyReverse.contributionsBy('albumData', 'bannerArtistContribs'),
+
+    albumsWithCommentaryBy: {
+      bindTo: 'albumData',
+
+      referencing: album => [album],
+      referenced: album => album.commentatorArtists,
+    },
+  };
+
   static [Thing.yamlDocumentSpec] = {
     fields: {
       'Album': {property: 'name'},
@@ -624,10 +677,7 @@ export class TrackSection extends Thing {
 
     // Update only
 
-    // used for withAlbum
-    albumData: wikiData({
-      class: input.value(Album),
-    }),
+    reverse: soupyReverse(),
 
     // Expose only
 
@@ -712,6 +762,15 @@ export class TrackSection extends Thing {
     },
   };
 
+  static [Thing.reverseSpecs] = {
+    trackSectionsWhichInclude: {
+      bindTo: 'trackSectionData',
+
+      referencing: trackSection => [trackSection],
+      referenced: trackSection => trackSection.tracks,
+    },
+  };
+
   static [Thing.yamlDocumentSpec] = {
     fields: {
       'Section': {property: 'name'},
diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js
index b7d79e8c..9842c887 100644
--- a/src/data/things/art-tag.js
+++ b/src/data/things/art-tag.js
@@ -12,6 +12,7 @@ import {
   directory,
   flag,
   name,
+  soupyReverse,
   wikiData,
 } from '#composite/wiki-properties';
 
@@ -41,15 +42,7 @@ export class ArtTag extends Thing {
 
     // Update only
 
-    // used for taggedInThings (reverse)
-    albumData: wikiData({
-      class: input.value(Album),
-    }),
-
-    // used for taggedInThings (reverse)
-    trackData: wikiData({
-      class: input.value(Track),
-    }),
+    reverse: soupyReverse(),
 
     // Expose only
 
@@ -57,11 +50,13 @@ export class ArtTag extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['this', 'albumData', 'trackData'],
-        compute: ({this: artTag, albumData, trackData}) =>
+        dependencies: ['this', 'reverse'],
+        compute: ({this: artTag, reverse}) =>
           sortAlbumsTracksChronologically(
-            [...albumData, ...trackData]
-              .filter(({artTags}) => artTags.includes(artTag)),
+            [
+              ...reverse.albumsWhoseArtworksFeature(artTag),
+              ...reverse.tracksWhoseArtworksFeature(artTag),
+            ],
             {getDate: thing => thing.coverArtDate ?? thing.date}),
       },
     },
diff --git a/src/data/things/artist.js b/src/data/things/artist.js
index 746e767a..7ed99a8e 100644
--- a/src/data/things/artist.js
+++ b/src/data/things/artist.js
@@ -11,19 +11,16 @@ import Thing from '#thing';
 import {isName, validateArrayItems} from '#validators';
 import {getKebabCase} from '#wiki-data';
 
-import {withReverseContributionList} from '#composite/wiki-data';
-
 import {
   contentString,
   directory,
   fileExtension,
   flag,
   name,
-  reverseAnnotatedReferenceList,
-  reverseContributionList,
   reverseReferenceList,
   singleReference,
   soupyFind,
+  soupyReverse,
   urls,
   wikiData,
 } from '#composite/wiki-properties';
@@ -62,90 +59,56 @@ export class Artist extends Thing {
     // Update only
 
     find: soupyFind(),
-
-    // used for reverse contribution lists
-    albumData: wikiData({
-      class: input.value(Album),
-    }),
-
-    // used for reverse contribution lists
-    flashData: wikiData({
-      class: input.value(Flash),
-    }),
-
-    // used for closelyLinkedGroups
-    groupData: wikiData({
-      class: input.value(Group),
-    }),
-
-    // used for reverse contribution lists
-    trackData: wikiData({
-      class: input.value(Track),
-    }),
+    reverse: soupyReverse(),
 
     // Expose only
 
-    trackArtistContributions: reverseContributionList({
-      data: 'trackData',
-      list: input.value('artistContribs'),
+    trackArtistContributions: reverseReferenceList({
+      reverse: soupyReverse.input('trackArtistContributionsBy'),
     }),
 
-    trackContributorContributions: reverseContributionList({
-      data: 'trackData',
-      list: input.value('contributorContribs'),
+    trackContributorContributions: reverseReferenceList({
+      reverse: soupyReverse.input('trackContributorContributionsBy'),
     }),
 
-    trackCoverArtistContributions: reverseContributionList({
-      data: 'trackData',
-      list: input.value('coverArtistContribs'),
+    trackCoverArtistContributions: reverseReferenceList({
+      reverse: soupyReverse.input('trackCoverArtistContributionsBy'),
     }),
 
     tracksAsCommentator: reverseReferenceList({
-      data: 'trackData',
-      list: input.value('commentatorArtists'),
+      reverse: soupyReverse.input('tracksWithCommentaryBy'),
     }),
 
-    albumArtistContributions: reverseContributionList({
-      data: 'albumData',
-      list: input.value('artistContribs'),
+    albumArtistContributions: reverseReferenceList({
+      reverse: soupyReverse.input('albumArtistContributionsBy'),
     }),
 
-    albumCoverArtistContributions: reverseContributionList({
-      data: 'albumData',
-      list: input.value('coverArtistContribs'),
+    albumCoverArtistContributions: reverseReferenceList({
+      reverse: soupyReverse.input('albumCoverArtistContributionsBy'),
     }),
 
-    albumWallpaperArtistContributions: reverseContributionList({
-      data: 'albumData',
-      list: input.value('wallpaperArtistContribs'),
+    albumWallpaperArtistContributions: reverseReferenceList({
+      reverse: soupyReverse.input('albumWallpaperArtistContributionsBy'),
     }),
 
-    albumBannerArtistContributions: reverseContributionList({
-      data: 'albumData',
-      list: input.value('bannerArtistContribs'),
+    albumBannerArtistContributions: reverseReferenceList({
+      reverse: soupyReverse.input('albumBannerArtistContributionsBy'),
     }),
 
     albumsAsCommentator: reverseReferenceList({
-      data: 'albumData',
-      list: input.value('commentatorArtists'),
+      reverse: soupyReverse.input('albumsWithCommentaryBy'),
     }),
 
-    flashContributorContributions: reverseContributionList({
-      data: 'flashData',
-      list: input.value('contributorContribs'),
+    flashContributorContributions: reverseReferenceList({
+      reverse: soupyReverse.input('flashContributorContributionsBy'),
     }),
 
     flashesAsCommentator: reverseReferenceList({
-      data: 'flashData',
-      list: input.value('commentatorArtists'),
+      reverse: soupyReverse.input('flashesWithCommentaryBy'),
     }),
 
-    closelyLinkedGroups: reverseAnnotatedReferenceList({
-      data: 'groupData',
-      list: input.value('closelyLinkedArtists'),
-
-      forward: input.value('artist'),
-      backward: input.value('group'),
+    closelyLinkedGroups: reverseReferenceList({
+      reverse: soupyReverse.input('groupsCloselyLinkedTo'),
     }),
 
     totalDuration: artistTotalDuration(),
diff --git a/src/data/things/flash.js b/src/data/things/flash.js
index 8a3fc374..b143b560 100644
--- a/src/data/things/flash.js
+++ b/src/data/things/flash.js
@@ -30,6 +30,7 @@ import {
   referenceList,
   simpleDate,
   soupyFind,
+  soupyReverse,
   thing,
   urls,
   wikiData,
@@ -115,11 +116,7 @@ export class Flash extends Thing {
     // Update only
 
     find: soupyFind(),
-
-    // used for withFlashAct (reverse)
-    flashActData: wikiData({
-      class: input.value(FlashAct),
-    }),
+    reverse: soupyReverse(),
 
     // used for withMatchingContributionPresets (indirectly by Contribution)
     wikiInfo: thing({
@@ -167,6 +164,25 @@ export class Flash extends Thing {
     },
   };
 
+  static [Thing.reverseSpecs] = {
+    flashesWhichFeature: {
+      bindTo: 'flashData',
+
+      referencing: flash => [flash],
+      referenced: flash => flash.featuredTracks,
+    },
+
+    flashContributorContributionsBy:
+      soupyReverse.contributionsBy('flashData', 'contributorContribs'),
+
+    flashesWithCommentaryBy: {
+      bindTo: 'flashData',
+
+      referencing: flash => [flash],
+      referenced: flash => flash.commentatorArtists,
+    },
+  };
+
   static [Thing.yamlDocumentSpec] = {
     fields: {
       'Flash': {property: 'name'},
@@ -242,11 +258,7 @@ export class FlashAct extends Thing {
     // Update only
 
     find: soupyFind(),
-
-    // used for withFlashSide
-    flashSideData: wikiData({
-      class: input.value(FlashSide),
-    }),
+    reverse: soupyReverse(),
 
     // Expose only
 
@@ -263,6 +275,15 @@ export class FlashAct extends Thing {
     },
   };
 
+  static [Thing.reverseSpecs] = {
+    flashActsWhoseFlashesInclude: {
+      bindTo: 'flashActData',
+
+      referencing: flashAct => [flashAct],
+      referenced: flashAct => flashAct.flashes,
+    },
+  };
+
   static [Thing.yamlDocumentSpec] = {
     fields: {
       'Act': {property: 'name'},
@@ -314,6 +335,15 @@ export class FlashSide extends Thing {
     },
   };
 
+  static [Thing.reverseSpecs] = {
+    flashSidesWhoseActsInclude: {
+      bindTo: 'flashSideData',
+
+      referencing: flashSide => [flashSide],
+      referenced: flashSide => flashSide.acts,
+    },
+  };
+
   static [Thing.getYamlLoadingSpec] = ({
     documentModes: {allInOne},
     thingConstructors: {Flash, FlashAct},
diff --git a/src/data/things/group.js b/src/data/things/group.js
index 8bc71931..ed3c59bb 100644
--- a/src/data/things/group.js
+++ b/src/data/things/group.js
@@ -52,17 +52,7 @@ export class Group extends Thing {
     // Update only
 
     find: soupyFind(),
-
-    // used for albums (reverse)
-    albumData: wikiData({
-      class: input.value(Album),
-    }),
-
-    // used for category (reverse)
-    // used for color (reverse)
-    groupCategoryData: wikiData({
-      class: input.value(GroupCategory),
-    }),
+    reverse: soupyFind(),
 
     // Expose only
 
@@ -82,9 +72,9 @@ export class Group extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['this', 'albumData'],
-        compute: ({this: group, albumData}) =>
-          albumData?.filter((album) => album.groups.includes(group)) ?? [],
+        dependencies: ['this', 'reverse'],
+        compute: ({this: group, reverse}) =>
+          reverse.albumsWhoseGroupsInclude(group),
       },
     },
 
@@ -92,9 +82,9 @@ export class Group extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['this', 'groupCategoryData'],
-        compute: ({this: group, groupCategoryData}) =>
-          groupCategoryData.find((category) => category.groups.includes(group))
+        dependencies: ['this', 'reverse'],
+        compute: ({this: group, reverse}) =>
+          reverse.groupCategoriesWhichInclude(group, {unique: true})
             ?.color,
       },
     },
@@ -103,9 +93,9 @@ export class Group extends Thing {
       flags: {expose: true},
 
       expose: {
-        dependencies: ['this', 'groupCategoryData'],
-        compute: ({this: group, groupCategoryData}) =>
-          groupCategoryData.find((category) => category.groups.includes(group)) ??
+        dependencies: ['this', 'reverse'],
+        compute: ({this: group, reverse}) =>
+          reverse.groupCategoriesWhichInclude(group, {unique: true}) ??
           null,
       },
     },
@@ -118,6 +108,25 @@ export class Group extends Thing {
     },
   };
 
+  static [Thing.reverseSpecs] = {
+    groupsCloselyLinkedTo: {
+      bindTo: 'groupData',
+
+      referencing: group =>
+        group.closelyLinkedArtists
+          .map(({artist, ...referenceDetails}) => ({
+            group,
+            artist,
+            referenceDetails,
+          })),
+
+      referenced: ({artist}) => [artist],
+
+      tidy: ({group, referenceDetails}) =>
+        ({group, ...referenceDetails}),
+    },
+  };
+
   static [Thing.yamlDocumentSpec] = {
     fields: {
       'Group': {property: 'name'},
@@ -217,6 +226,15 @@ export class GroupCategory extends Thing {
     find: soupyFind(),
   });
 
+  static [Thing.reverseSpecs] = {
+    groupCategoriesWhichInclude: {
+      bindTo: 'groupCategoryData',
+
+      referencing: groupCategory => [groupCategory],
+      referenced: groupCategory => groupCategory.groups,
+    },
+  };
+
   static [Thing.yamlDocumentSpec] = {
     fields: {
       'Category': {property: 'name'},
diff --git a/src/data/things/track.js b/src/data/things/track.js
index 81ba35bb..ff4750db 100644
--- a/src/data/things/track.js
+++ b/src/data/things/track.js
@@ -48,11 +48,11 @@ import {
   referenceList,
   referencedArtworkList,
   reverseReferenceList,
-  reverseReferencedArtworkList,
   simpleDate,
   simpleString,
   singleReference,
   soupyFind,
+  soupyReverse,
   thing,
   urls,
   wikiData,
@@ -62,7 +62,6 @@ import {
   exitWithoutUniqueCoverArt,
   inheritContributionListFromOriginalRelease,
   inheritFromOriginalRelease,
-  trackReverseReferenceList,
   withAlbum,
   withAlwaysReferenceByDirectory,
   withContainingTrackSection,
@@ -370,29 +369,18 @@ export class Track extends Thing {
     // Update only
 
     find: soupyFind(),
+    reverse: soupyReverse(),
 
     // used for referencedArtworkList (mixedFind)
-    // used for withAlbum (reverse)
     albumData: wikiData({
       class: input.value(Album),
     }),
 
-    // used for featuredInFlashes (reverse)
-    flashData: wikiData({
-      class: input.value(Flash),
-    }),
-
     // used for referencedArtworkList (mixedFind)
-    // used for trackReverseReferenceList (reverse)
     trackData: wikiData({
       class: input.value(Track),
     }),
 
-    // used for withContainingTrackSection (reverse)
-    trackSectionData: wikiData({
-      class: input.value(TrackSection),
-    }),
-
     // used for withMatchingContributionPresets (indirectly by Contribution)
     wikiInfo: thing({
       class: input.value(WikiInfo),
@@ -439,17 +427,16 @@ export class Track extends Thing {
       exposeDependency({dependency: '#otherReleases'}),
     ],
 
-    referencedByTracks: trackReverseReferenceList({
-      list: input.value('referencedTracks'),
+    referencedByTracks: reverseReferenceList({
+      reverse: soupyReverse.input('tracksWhichReference'),
     }),
 
-    sampledByTracks: trackReverseReferenceList({
-      list: input.value('sampledTracks'),
+    sampledByTracks: reverseReferenceList({
+      reverse: soupyReverse.input('tracksWhichSample'),
     }),
 
     featuredInFlashes: reverseReferenceList({
-      data: 'flashData',
-      list: input.value('featuredTracks'),
+      reverse: soupyReverse.input('flashesWhichFeature'),
     }),
 
     referencedByArtworks: [
@@ -457,7 +444,9 @@ export class Track extends Thing {
         value: input.value([]),
       }),
 
-      reverseReferencedArtworkList(),
+      reverseReferenceList({
+        reverse: soupyReverse.input('artworksWhichReference'),
+      }),
     ],
   });
 
@@ -664,6 +653,29 @@ export class Track extends Thing {
       referencing: track => track.isOriginalRelease ? [track] : [],
       referenced: track => track.sampledTracks,
     },
+
+    tracksWhoseArtworksFeature: {
+      bindTo: 'trackData',
+
+      referencing: track => [track],
+      referenced: track => track.artTags,
+    },
+
+    trackArtistContributionsBy:
+      soupyReverse.contributionsBy('trackData', 'artistContribs'),
+
+    trackContributorContributionsBy:
+      soupyReverse.contributionsBy('trackData', 'contributorContribs'),
+
+    trackCoverArtistContributionsBy:
+      soupyReverse.contributionsBy('trackData', 'coverArtistContribs'),
+
+    tracksWithCommentaryBy: {
+      bindTo: 'trackData',
+
+      referencing: track => [track],
+      referenced: track => track.commentatorArtists,
+    },
   };
 
   // Track YAML loading is handled in album.js.
diff --git a/src/data/yaml.js b/src/data/yaml.js
index 0d92b21a..a1eb73fb 100644
--- a/src/data/yaml.js
+++ b/src/data/yaml.js
@@ -1236,33 +1236,19 @@ export function linkWikiDataArrays(wikiData, {bindFind, bindReverse}) {
       'wikiInfo',
     ]],
 
-    [wikiData.artTagData, [
-      'albumData',
-      'trackData',
-    ]],
+    [wikiData.artTagData, [/* reverse */]],
 
-    [wikiData.artistData, [
-      'albumData',
-      'flashData',
-      'groupData',
-      'trackData',
-    ]],
+    [wikiData.artistData, [/* find, reverse */]],
 
     [wikiData.flashData, [
-      'flashActData',
       'wikiInfo',
     ]],
 
-    [wikiData.flashActData, [
-      'flashSideData',
-    ]],
+    [wikiData.flashActData, [/* find, reverse */]],
 
     [wikiData.flashSideData, [/* find */]],
 
-    [wikiData.groupData, [
-      'albumData',
-      'groupCategoryData',
-    ]],
+    [wikiData.groupData, [/* find, reverse */]],
 
     [wikiData.groupCategoryData, [/* find */]],
 
@@ -1270,15 +1256,11 @@ export function linkWikiDataArrays(wikiData, {bindFind, bindReverse}) {
 
     [wikiData.trackData, [
       'albumData',
-      'flashData',
       'trackData',
-      'trackSectionData',
       'wikiInfo',
     ]],
 
-    [wikiData.trackSectionData, [
-      'albumData',
-    ]],
+    [wikiData.trackSectionData, [/* reverse */]],
 
     [[wikiData.wikiInfo], [/* find */]],
   ]);