« 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/composite
diff options
context:
space:
mode:
Diffstat (limited to 'src/data/composite')
-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
25 files changed, 202 insertions, 647 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;