« 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/album/index.js2
-rw-r--r--src/data/composite/things/album/withTrackSections.js116
-rw-r--r--src/data/composite/things/album/withTracks.js46
-rw-r--r--src/data/composite/things/track-section/index.js1
-rw-r--r--src/data/composite/things/track-section/withAlbum.js22
-rw-r--r--src/data/composite/things/track/index.js1
-rw-r--r--src/data/composite/things/track/inheritFromOriginalRelease.js51
-rw-r--r--src/data/composite/things/track/withAlwaysReferenceByDirectory.js31
-rw-r--r--src/data/composite/things/track/withContainingTrackSection.js1
-rw-r--r--src/data/composite/things/track/withPropertyFromOriginalRelease.js86
-rw-r--r--src/data/composite/wiki-data/index.js2
-rw-r--r--src/data/composite/wiki-data/withDirectory.js55
-rw-r--r--src/data/composite/wiki-data/withDirectoryFromName.js42
-rw-r--r--src/data/composite/wiki-data/withResolvedContribs.js22
-rw-r--r--src/data/composite/wiki-data/withReverseContributionList.js14
-rw-r--r--src/data/composite/wiki-data/withReverseReferenceList.js10
-rw-r--r--src/data/composite/wiki-data/withUniqueReferencingThing.js5
-rw-r--r--src/data/composite/wiki-properties/contributionList.js10
-rw-r--r--src/data/composite/wiki-properties/directory.js48
-rw-r--r--src/data/composite/wiki-properties/referenceList.js20
20 files changed, 361 insertions, 224 deletions
diff --git a/src/data/composite/things/album/index.js b/src/data/composite/things/album/index.js
index 8139f10e..0ef91b87 100644
--- a/src/data/composite/things/album/index.js
+++ b/src/data/composite/things/album/index.js
@@ -1,2 +1,2 @@
-export {default as withTracks} from './withTracks.js';
 export {default as withTrackSections} from './withTrackSections.js';
+export {default as withTracks} from './withTracks.js';
diff --git a/src/data/composite/things/album/withTrackSections.js b/src/data/composite/things/album/withTrackSections.js
index 0a1ebebc..a56bda31 100644
--- a/src/data/composite/things/album/withTrackSections.js
+++ b/src/data/composite/things/album/withTrackSections.js
@@ -1,127 +1,21 @@
 import {input, templateCompositeFrom} from '#composite';
+
 import find from '#find';
-import {empty, filterMultipleArrays, stitchArrays} from '#sugar';
-import {isTrackSectionList} from '#validators';
 
-import {exitWithoutDependency, exitWithoutUpdateValue}
-  from '#composite/control-flow';
 import {withResolvedReferenceList} from '#composite/wiki-data';
 
-import {
-  fillMissingListItems,
-  withFlattenedList,
-  withPropertiesFromList,
-  withUnflattenedList,
-} from '#composite/data';
-
 export default templateCompositeFrom({
   annotation: `withTrackSections`,
 
   outputs: ['#trackSections'],
 
   steps: () => [
-    exitWithoutDependency({
-      dependency: 'ownTrackData',
-      value: input.value([]),
-    }),
-
-    exitWithoutUpdateValue({
-      mode: input.value('empty'),
-      value: input.value([]),
-    }),
-
-    // TODO: input.updateValue description down here is a kludge.
-    withPropertiesFromList({
-      list: input.updateValue({
-        validate: isTrackSectionList,
-      }),
-      prefix: input.value('#sections'),
-      properties: input.value([
-        'tracks',
-        'dateOriginallyReleased',
-        'isDefaultTrackSection',
-        'name',
-        'color',
-      ]),
-    }),
-
-    fillMissingListItems({
-      list: '#sections.tracks',
-      fill: input.value([]),
-    }),
-
-    fillMissingListItems({
-      list: '#sections.isDefaultTrackSection',
-      fill: input.value(false),
-    }),
-
-    fillMissingListItems({
-      list: '#sections.name',
-      fill: input.value('Unnamed Track Section'),
-    }),
-
-    fillMissingListItems({
-      list: '#sections.color',
-      fill: input.dependency('color'),
-    }),
-
-    withFlattenedList({
-      list: '#sections.tracks',
-    }).outputs({
-      ['#flattenedList']: '#trackRefs',
-      ['#flattenedIndices']: '#sections.startIndex',
-    }),
-
     withResolvedReferenceList({
-      list: '#trackRefs',
-      data: 'ownTrackData',
-      notFoundMode: input.value('null'),
-      find: input.value(find.track),
+      list: 'trackSections',
+      data: 'ownTrackSectionData',
+      find: input.value(find.unqualifiedTrackSection),
     }).outputs({
-      ['#resolvedReferenceList']: '#tracks',
+      ['#resolvedReferenceList']: '#trackSections',
     }),
-
-    withUnflattenedList({
-      list: '#tracks',
-      indices: '#sections.startIndex',
-    }).outputs({
-      ['#unflattenedList']: '#sections.tracks',
-    }),
-
-    {
-      dependencies: [
-        '#sections.tracks',
-        '#sections.name',
-        '#sections.color',
-        '#sections.dateOriginallyReleased',
-        '#sections.isDefaultTrackSection',
-        '#sections.startIndex',
-      ],
-
-      compute: (continuation, {
-        '#sections.tracks': tracks,
-        '#sections.name': name,
-        '#sections.color': color,
-        '#sections.dateOriginallyReleased': dateOriginallyReleased,
-        '#sections.isDefaultTrackSection': isDefaultTrackSection,
-        '#sections.startIndex': startIndex,
-      }) => {
-        filterMultipleArrays(
-          tracks, name, color, dateOriginallyReleased, isDefaultTrackSection, startIndex,
-          tracks => !empty(tracks));
-
-        return continuation({
-          ['#trackSections']:
-            stitchArrays({
-              tracks,
-              name,
-              color,
-              dateOriginallyReleased,
-              isDefaultTrackSection,
-              startIndex,
-            }),
-        });
-      },
-    },
   ],
 });
diff --git a/src/data/composite/things/album/withTracks.js b/src/data/composite/things/album/withTracks.js
index fff3d5ae..c8d27c4c 100644
--- a/src/data/composite/things/album/withTracks.js
+++ b/src/data/composite/things/album/withTracks.js
@@ -1,51 +1,27 @@
 import {input, templateCompositeFrom} from '#composite';
-import find from '#find';
 
-import {exitWithoutDependency, raiseOutputWithoutDependency}
-  from '#composite/control-flow';
+import {withFlattenedList, withPropertyFromList} from '#composite/data';
 import {withResolvedReferenceList} from '#composite/wiki-data';
 
+import withTrackSections from './withTrackSections.js';
+
 export default templateCompositeFrom({
   annotation: `withTracks`,
 
   outputs: ['#tracks'],
 
   steps: () => [
-    exitWithoutDependency({
-      dependency: 'ownTrackData',
-      value: input.value([]),
-    }),
+    withTrackSections(),
 
-    raiseOutputWithoutDependency({
-      dependency: 'trackSections',
-      mode: input.value('empty'),
-      output: input.value({
-        ['#tracks']: [],
-      }),
+    withPropertyFromList({
+      list: '#trackSections',
+      property: input.value('tracks'),
     }),
 
-    {
-      dependencies: ['trackSections'],
-      compute: (continuation, {trackSections}) =>
-        continuation({
-          '#trackRefs': trackSections
-            .flatMap(section => section.tracks ?? []),
-        }),
-    },
-
-    withResolvedReferenceList({
-      list: '#trackRefs',
-      data: 'ownTrackData',
-      find: input.value(find.track),
+    withFlattenedList({
+      list: '#trackSections.tracks',
+    }).outputs({
+      ['#flattenedList']: '#tracks',
     }),
-
-    {
-      dependencies: ['#resolvedReferenceList'],
-      compute: (continuation, {
-        ['#resolvedReferenceList']: resolvedReferenceList,
-      }) => continuation({
-        ['#tracks']: resolvedReferenceList,
-      })
-    },
   ],
 });
diff --git a/src/data/composite/things/track-section/index.js b/src/data/composite/things/track-section/index.js
new file mode 100644
index 00000000..3202ed49
--- /dev/null
+++ b/src/data/composite/things/track-section/index.js
@@ -0,0 +1 @@
+export {default as withAlbum} from './withAlbum.js';
diff --git a/src/data/composite/things/track-section/withAlbum.js b/src/data/composite/things/track-section/withAlbum.js
new file mode 100644
index 00000000..608cc0cd
--- /dev/null
+++ b/src/data/composite/things/track-section/withAlbum.js
@@ -0,0 +1,22 @@
+// Gets the track section's album. This will early exit if ownAlbumData is
+// missing. If there's no album whose list of track sections includes this one,
+// the output dependency will be null.
+
+import {input, templateCompositeFrom} from '#composite';
+
+import {withUniqueReferencingThing} from '#composite/wiki-data';
+
+export default templateCompositeFrom({
+  annotation: `withAlbum`,
+
+  outputs: ['#album'],
+
+  steps: () => [
+    withUniqueReferencingThing({
+      data: 'ownAlbumData',
+      list: input.value('trackSections'),
+    }).outputs({
+      ['#uniqueReferencingThing']: '#album',
+    }),
+  ],
+});
diff --git a/src/data/composite/things/track/index.js b/src/data/composite/things/track/index.js
index cc723a24..8959de9f 100644
--- a/src/data/composite/things/track/index.js
+++ b/src/data/composite/things/track/index.js
@@ -9,3 +9,4 @@ export {default as withContainingTrackSection} from './withContainingTrackSectio
 export {default as withHasUniqueCoverArt} from './withHasUniqueCoverArt.js';
 export {default as withOtherReleases} from './withOtherReleases.js';
 export {default as withPropertyFromAlbum} from './withPropertyFromAlbum.js';
+export {default as withPropertyFromOriginalRelease} from './withPropertyFromOriginalRelease.js';
diff --git a/src/data/composite/things/track/inheritFromOriginalRelease.js b/src/data/composite/things/track/inheritFromOriginalRelease.js
index 27ed1387..38ab06be 100644
--- a/src/data/composite/things/track/inheritFromOriginalRelease.js
+++ b/src/data/composite/things/track/inheritFromOriginalRelease.js
@@ -1,8 +1,6 @@
-// Early exits with a value inherited from the original release, if
-// this track is a rerelease, and otherwise continues with no further
-// dependencies provided. If allowOverride is true, then the continuation
-// will also be called if the original release exposed the requested
-// property as null.
+// Early exits with the value for the same property as specified on the
+// original release, if this track is a rerelease, and otherwise continues
+// without providing any further dependencies.
 //
 // Like withOriginalRelease, this will early exit (with notFoundValue) if the
 // original release is specified by reference and that reference doesn't
@@ -10,41 +8,34 @@
 
 import {input, templateCompositeFrom} from '#composite';
 
-import withOriginalRelease from './withOriginalRelease.js';
+import {exposeDependency, raiseOutputWithoutDependency}
+  from '#composite/control-flow';
+
+import withPropertyFromOriginalRelease
+  from './withPropertyFromOriginalRelease.js';
 
 export default templateCompositeFrom({
   annotation: `inheritFromOriginalRelease`,
 
   inputs: {
-    property: input({type: 'string'}),
-    allowOverride: input({type: 'boolean', defaultValue: false}),
-    notFoundValue: input({defaultValue: null}),
+    notFoundValue: input({
+      defaultValue: null,
+    }),
   },
 
   steps: () => [
-    withOriginalRelease({
+    withPropertyFromOriginalRelease({
+      property: input.thisProperty(),
       notFoundValue: input('notFoundValue'),
     }),
 
-    {
-      dependencies: [
-        '#originalRelease',
-        input('property'),
-        input('allowOverride'),
-      ],
-
-      compute: (continuation, {
-        ['#originalRelease']: originalRelease,
-        [input('property')]: originalProperty,
-        [input('allowOverride')]: allowOverride,
-      }) => {
-        if (!originalRelease) return continuation();
-
-        const value = originalRelease[originalProperty];
-        if (allowOverride && value === null) return continuation();
-
-        return continuation.exit(value);
-      },
-    },
+    raiseOutputWithoutDependency({
+      dependency: '#isRerelease',
+      mode: input.value('falsy'),
+    }),
+
+    exposeDependency({
+      dependency: '#originalValue',
+    }),
   ],
 });
diff --git a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js
index fac8e213..e01720b4 100644
--- a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js
+++ b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js
@@ -7,11 +7,15 @@ import {input, templateCompositeFrom} from '#composite';
 import find from '#find';
 import {isBoolean} from '#validators';
 
-import {exitWithoutDependency, exposeUpdateValueOrContinue}
-  from '#composite/control-flow';
 import {withPropertyFromObject} from '#composite/data';
 import {withResolvedReference} from '#composite/wiki-data';
 
+import {
+  exitWithoutDependency,
+  exposeDependencyOrContinue,
+  exposeUpdateValueOrContinue,
+} from '#composite/control-flow';
+
 export default templateCompositeFrom({
   annotation: `withAlwaysReferenceByDirectory`,
 
@@ -22,6 +26,29 @@ export default templateCompositeFrom({
       validate: input.value(isBoolean),
     }),
 
+    // withAlwaysReferenceByDirectory is sort of a fragile area - we can't
+    // find the track's album the normal way because albums' track lists
+    // recurse back into alwaysReferenceByDirectory!
+    withResolvedReference({
+      ref: 'dataSourceAlbum',
+      data: 'albumData',
+      find: input.value(find.album),
+    }).outputs({
+      '#resolvedReference': '#album',
+    }),
+
+    withPropertyFromObject({
+      object: '#album',
+      property: input.value('alwaysReferenceTracksByDirectory'),
+    }),
+
+    // Falsy mode means this exposes true if the album's property is true,
+    // but continues if the property is false (which is also the default).
+    exposeDependencyOrContinue({
+      dependency: '#album.alwaysReferenceTracksByDirectory',
+      mode: input.value('falsy'),
+    }),
+
     // Remaining code is for defaulting to true if this track is a rerelease of
     // another with the same name, so everything further depends on access to
     // trackData as well as originalReleaseTrack.
diff --git a/src/data/composite/things/track/withContainingTrackSection.js b/src/data/composite/things/track/withContainingTrackSection.js
index eaac14de..2c42709b 100644
--- a/src/data/composite/things/track/withContainingTrackSection.js
+++ b/src/data/composite/things/track/withContainingTrackSection.js
@@ -30,7 +30,6 @@ export default templateCompositeFrom({
 
       compute: (continuation, {
         [input.myself()]: track,
-        [input('notFoundMode')]: notFoundMode,
         ['#album.trackSections']: trackSections,
       }) => continuation({
         ['#trackSection']:
diff --git a/src/data/composite/things/track/withPropertyFromOriginalRelease.js b/src/data/composite/things/track/withPropertyFromOriginalRelease.js
new file mode 100644
index 00000000..fd37f6de
--- /dev/null
+++ b/src/data/composite/things/track/withPropertyFromOriginalRelease.js
@@ -0,0 +1,86 @@
+// Provides a value inherited from the original release, if applicable, and a
+// flag indicating if this track is a rerelase or not.
+//
+// Like withOriginalRelease, this will early exit (with notFoundValue) if the
+// original release is specified by reference and that reference doesn't
+// resolve to anything.
+
+import {input, templateCompositeFrom} from '#composite';
+
+import {withResultOfAvailabilityCheck} from '#composite/control-flow';
+import {withPropertyFromObject} from '#composite/data';
+
+import withOriginalRelease from './withOriginalRelease.js';
+
+export default templateCompositeFrom({
+  annotation: `inheritFromOriginalRelease`,
+
+  inputs: {
+    property: input({type: 'string'}),
+
+    notFoundValue: input({
+      defaultValue: null,
+    }),
+  },
+
+  outputs: ({
+    [input.staticValue('property')]: property,
+  }) =>
+    ['#isRerelease'].concat(
+      (property
+        ? ['#original.' + property]
+        : ['#originalValue'])),
+
+  steps: () => [
+    withOriginalRelease({
+      notFoundValue: input('notFoundValue'),
+    }),
+
+    withResultOfAvailabilityCheck({
+      from: '#originalRelease',
+    }),
+
+    {
+      dependencies: [
+        '#availability',
+        input.staticValue('property'),
+      ],
+
+      compute: (continuation, {
+        ['#availability']: availability,
+        [input.staticValue('property')]: property,
+      }) =>
+        (availability
+          ? continuation()
+          : continuation.raiseOutput(
+              Object.assign(
+                {'#isRerelease': false},
+                (property
+                  ? {['#original.' + property]: null}
+                  : {'#originalValue': null})))),
+    },
+
+    withPropertyFromObject({
+      object: '#originalRelease',
+      property: input('property'),
+    }),
+
+    {
+      dependencies: [
+        '#value',
+        input.staticValue('property'),
+      ],
+
+      compute: (continuation, {
+        ['#value']: value,
+        [input.staticValue('property')]: property,
+      }) =>
+        continuation.raiseOutput(
+          Object.assign(
+            {'#isRerelease': true},
+            (property
+              ? {['#original.' + property]: value}
+              : {'#originalValue': value}))),
+    },
+  ],
+});
diff --git a/src/data/composite/wiki-data/index.js b/src/data/composite/wiki-data/index.js
index b4cf6d13..15ebaffa 100644
--- a/src/data/composite/wiki-data/index.js
+++ b/src/data/composite/wiki-data/index.js
@@ -6,6 +6,8 @@
 
 export {default as exitWithoutContribs} from './exitWithoutContribs.js';
 export {default as inputWikiData} from './inputWikiData.js';
+export {default as withDirectory} from './withDirectory.js';
+export {default as withDirectoryFromName} from './withDirectoryFromName.js';
 export {default as withParsedCommentaryEntries} from './withParsedCommentaryEntries.js';
 export {default as withResolvedContribs} from './withResolvedContribs.js';
 export {default as withResolvedReference} from './withResolvedReference.js';
diff --git a/src/data/composite/wiki-data/withDirectory.js b/src/data/composite/wiki-data/withDirectory.js
new file mode 100644
index 00000000..b08b6153
--- /dev/null
+++ b/src/data/composite/wiki-data/withDirectory.js
@@ -0,0 +1,55 @@
+// Select a directory, either using a manually specified directory, or
+// computing it from a name. By default these values are the current thing's
+// 'directory' and 'name' properties, so it can be used without any options
+// to get the current thing's effective directory (assuming no custom rules).
+
+import {input, templateCompositeFrom} from '#composite';
+
+import {isDirectory, isName} from '#validators';
+
+import {withResultOfAvailabilityCheck} from '#composite/control-flow';
+
+import withDirectoryFromName from './withDirectoryFromName.js';
+
+export default templateCompositeFrom({
+  annotation: `withDirectory`,
+
+  inputs: {
+    directory: input({
+      validate: isDirectory,
+      defaultDependency: 'directory',
+      acceptsNull: true,
+    }),
+
+    name: input({
+      validate: isName,
+      defaultDependency: 'name',
+      acceptsNull: true,
+    }),
+  },
+
+  outputs: ['#directory'],
+
+  steps: () => [
+    withResultOfAvailabilityCheck({
+      from: input('directory'),
+    }),
+
+    {
+      dependencies: ['#availability', input('directory')],
+      compute: (continuation, {
+        ['#availability']: availability,
+        [input('directory')]: directory,
+      }) =>
+        (availability
+          ? continuation.raiseOutput({
+              ['#directory']: directory
+            })
+          : continuation()),
+    },
+
+    withDirectoryFromName({
+      name: input('name'),
+    }),
+  ],
+});
diff --git a/src/data/composite/wiki-data/withDirectoryFromName.js b/src/data/composite/wiki-data/withDirectoryFromName.js
new file mode 100644
index 00000000..034464e4
--- /dev/null
+++ b/src/data/composite/wiki-data/withDirectoryFromName.js
@@ -0,0 +1,42 @@
+// Compute a directory from a name - by default the current thing's own name.
+
+import {input, templateCompositeFrom} from '#composite';
+
+import {isName} from '#validators';
+import {getKebabCase} from '#wiki-data';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+
+export default templateCompositeFrom({
+  annotation: `withDirectoryFromName`,
+
+  inputs: {
+    name: input({
+      validate: isName,
+      defaultDependency: 'name',
+      acceptsNull: true,
+    }),
+  },
+
+  outputs: ['#directory'],
+
+  steps: () => [
+    raiseOutputWithoutDependency({
+      dependency: input('name'),
+      mode: input.value('falsy'),
+      output: input.value({
+        ['#directory']: null,
+      }),
+    }),
+
+    {
+      dependencies: [input('name')],
+      compute: (continuation, {
+        [input('name')]: name,
+      }) => continuation({
+        ['#directory']:
+          getKebabCase(name),
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/wiki-data/withResolvedContribs.js b/src/data/composite/wiki-data/withResolvedContribs.js
index 77b0f96d..95266382 100644
--- a/src/data/composite/wiki-data/withResolvedContribs.js
+++ b/src/data/composite/wiki-data/withResolvedContribs.js
@@ -1,7 +1,8 @@
 // Resolves the contribsByRef contained in the provided dependency,
 // providing (named by the second argument) the result. "Resolving"
-// means mapping the "who" reference of each contribution to an artist
-// object, and filtering out those whose "who" doesn't match any artist.
+// means mapping the artist reference of each contribution to an artist
+// object, and filtering out those whose artist reference doesn't match
+// any artist.
 
 import {input, templateCompositeFrom} from '#composite';
 import find from '#find';
@@ -46,29 +47,30 @@ export default templateCompositeFrom({
 
     withPropertiesFromList({
       list: input('from'),
-      properties: input.value(['who', 'what']),
+      properties: input.value(['artist', 'annotation']),
       prefix: input.value('#contribs'),
     }),
 
     withResolvedReferenceList({
-      list: '#contribs.who',
+      list: '#contribs.artist',
       data: 'artistData',
       find: input.value(find.artist),
       notFoundMode: input('notFoundMode'),
     }).outputs({
-      ['#resolvedReferenceList']: '#contribs.who',
+      ['#resolvedReferenceList']: '#contribs.artist',
     }),
 
     {
-      dependencies: ['#contribs.who', '#contribs.what'],
+      dependencies: ['#contribs.artist', '#contribs.annotation'],
 
       compute(continuation, {
-        ['#contribs.who']: who,
-        ['#contribs.what']: what,
+        ['#contribs.artist']: artist,
+        ['#contribs.annotation']: annotation,
       }) {
-        filterMultipleArrays(who, what, (who, _what) => who);
+        filterMultipleArrays(artist, annotation, (artist, _annotation) => artist);
         return continuation({
-          ['#resolvedContribs']: stitchArrays({who, what}),
+          ['#resolvedContribs']:
+            stitchArrays({artist, annotation}),
         });
       },
     },
diff --git a/src/data/composite/wiki-data/withReverseContributionList.js b/src/data/composite/wiki-data/withReverseContributionList.js
index eccb58b7..91e125e4 100644
--- a/src/data/composite/wiki-data/withReverseContributionList.js
+++ b/src/data/composite/wiki-data/withReverseContributionList.js
@@ -11,7 +11,8 @@
 
 import {input, templateCompositeFrom} from '#composite';
 
-import {exitWithoutDependency} from '#composite/control-flow';
+import {exitWithoutDependency, raiseOutputWithoutDependency}
+  from '#composite/control-flow';
 
 import inputWikiData from './inputWikiData.js';
 
@@ -32,10 +33,17 @@ export default templateCompositeFrom({
   outputs: ['#reverseContributionList'],
 
   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({'#reverseContributionList': []}),
     }),
 
     {
@@ -58,10 +66,10 @@ export default templateCompositeFrom({
           for (const referencingThing of data) {
             const referenceList = referencingThing[list];
 
-            // Destructuring {who} is the only unique part of the
+            // Destructuring {artist} is the only unique part of the
             // withReverseContributionList implementation, compared to
             // withReverseReferneceList.
-            for (const {who: referencedThing} of referenceList) {
+            for (const {artist: referencedThing} of referenceList) {
               if (cacheRecord.has(referencedThing)) {
                 cacheRecord.get(referencedThing).push(referencingThing);
               } else {
diff --git a/src/data/composite/wiki-data/withReverseReferenceList.js b/src/data/composite/wiki-data/withReverseReferenceList.js
index 2d7a421b..8cd540a5 100644
--- a/src/data/composite/wiki-data/withReverseReferenceList.js
+++ b/src/data/composite/wiki-data/withReverseReferenceList.js
@@ -13,7 +13,8 @@
 
 import {input, templateCompositeFrom} from '#composite';
 
-import {exitWithoutDependency} from '#composite/control-flow';
+import {exitWithoutDependency, raiseOutputWithoutDependency}
+  from '#composite/control-flow';
 
 import inputWikiData from './inputWikiData.js';
 
@@ -34,10 +35,17 @@ export default templateCompositeFrom({
   outputs: ['#reverseReferenceList'],
 
   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({'#reverseReferenceList': []}),
     }),
 
     {
diff --git a/src/data/composite/wiki-data/withUniqueReferencingThing.js b/src/data/composite/wiki-data/withUniqueReferencingThing.js
index ce04f838..61c10618 100644
--- a/src/data/composite/wiki-data/withUniqueReferencingThing.js
+++ b/src/data/composite/wiki-data/withUniqueReferencingThing.js
@@ -21,11 +21,10 @@ export default templateCompositeFrom({
   outputs: ['#uniqueReferencingThing'],
 
   steps: () => [
-    // withReverseRefernceList does this check too, but it early exits with
-    // an empty array. That's no good here!
+    // Early exit with null (not an empty array) if the data list
+    // isn't available.
     exitWithoutDependency({
       dependency: input('data'),
-      mode: input.value('empty'),
     }),
 
     withReverseReferenceList({
diff --git a/src/data/composite/wiki-properties/contributionList.js b/src/data/composite/wiki-properties/contributionList.js
index 8fde2caa..aad12a2d 100644
--- a/src/data/composite/wiki-properties/contributionList.js
+++ b/src/data/composite/wiki-properties/contributionList.js
@@ -3,15 +3,15 @@
 // into one property. Update value will look something like this:
 //
 //   [
-//     {who: 'Artist Name', what: 'Viola'},
-//     {who: 'artist:john-cena', what: null},
+//     {artist: 'Artist Name', annotation: 'Viola'},
+//     {artist: 'artist:john-cena', annotation: null},
 //     ...
 //   ]
 //
 // ...typically as processed from YAML, spreadsheet, or elsewhere.
-// Exposes as the same, but with the "who" replaced with matches found in
-// artistData - which means this always depends on an `artistData` property
-// also existing on this object!
+// Exposes as the same, but with the artist property replaced with matches
+// found in artistData - which means this always depends on an `artistData`
+// property also existing on this object!
 //
 
 import {input, templateCompositeFrom} from '#composite';
diff --git a/src/data/composite/wiki-properties/directory.js b/src/data/composite/wiki-properties/directory.js
index 0b2181c9..41ce4b27 100644
--- a/src/data/composite/wiki-properties/directory.js
+++ b/src/data/composite/wiki-properties/directory.js
@@ -2,22 +2,32 @@
 // almost any data object. Also corresponds to a part of the URL which pages of
 // such objects are visited at.
 
-import {isDirectory} from '#validators';
-import {getKebabCase} from '#wiki-data';
-
-// TODO: Not templateCompositeFrom.
-
-export default function() {
-  return {
-    flags: {update: true, expose: true},
-    update: {validate: isDirectory},
-    expose: {
-      dependencies: ['name'],
-      transform(directory, {name}) {
-        if (directory === null && name === null) return null;
-        else if (directory === null) return getKebabCase(name);
-        else return directory;
-      },
-    },
-  };
-}
+import {input, templateCompositeFrom} from '#composite';
+
+import {isDirectory, isName} from '#validators';
+
+import {exposeDependency} from '#composite/control-flow';
+import {withDirectory} from '#composite/wiki-data';
+
+export default templateCompositeFrom({
+  annotation: `directory`,
+
+  compose: false,
+
+  inputs: {
+    name: input({
+      validate: isName,
+      defaultDependency: 'name',
+    }),
+  },
+
+  steps: () => [
+    withDirectory({
+      directory: input.updateValue({validate: isDirectory}),
+    }),
+
+    exposeDependency({
+      dependency: '#directory',
+    }),
+  ],
+});
diff --git a/src/data/composite/wiki-properties/referenceList.js b/src/data/composite/wiki-properties/referenceList.js
index af634a68..ebd5947c 100644
--- a/src/data/composite/wiki-properties/referenceList.js
+++ b/src/data/composite/wiki-properties/referenceList.js
@@ -1,5 +1,6 @@
 // Stores and exposes a list of references to other data objects; all items
-// must be references to the same type, which is specified on the class input.
+// must be references to the same type, which is either implied from the class
+// input, or explicitly set on the referenceType input.
 //
 // See also:
 //  - singleReference
@@ -18,7 +19,17 @@ export default templateCompositeFrom({
   compose: false,
 
   inputs: {
-    class: input.staticValue({validate: isThingClass}),
+    class: input.staticValue({
+      validate: isThingClass,
+      acceptsNull: true,
+      defaultValue: null,
+    }),
+
+    referenceType: input.staticValue({
+      type: 'string',
+      acceptsNull: true,
+      defaultValue: null,
+    }),
 
     data: inputWikiData({allowMixedTypes: false}),
 
@@ -27,10 +38,13 @@ export default templateCompositeFrom({
 
   update: ({
     [input.staticValue('class')]: thingClass,
+    [input.staticValue('referenceType')]: referenceType,
   }) => ({
     validate:
       validateReferenceList(
-        thingClass[Symbol.for('Thing.referenceType')]),
+        (thingClass
+          ? thingClass[Symbol.for('Thing.referenceType')]
+          : referenceType)),
   }),
 
   steps: () => [