« 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/things
diff options
context:
space:
mode:
Diffstat (limited to 'src/data/things')
-rw-r--r--src/data/things/album.js157
-rw-r--r--src/data/things/art-tag.js10
-rw-r--r--src/data/things/artist.js6
-rw-r--r--src/data/things/composite.js727
-rw-r--r--src/data/things/flash.js6
-rw-r--r--src/data/things/group.js6
-rw-r--r--src/data/things/homepage-layout.js16
-rw-r--r--src/data/things/language.js9
-rw-r--r--src/data/things/news-entry.js6
-rw-r--r--src/data/things/static-page.js6
-rw-r--r--src/data/things/thing.js713
-rw-r--r--src/data/things/track.js471
-rw-r--r--src/data/things/wiki-info.js6
13 files changed, 97 insertions, 2042 deletions
diff --git a/src/data/things/album.js b/src/data/things/album.js
index fd8a71d3..e3ac1651 100644
--- a/src/data/things/album.js
+++ b/src/data/things/album.js
@@ -1,21 +1,12 @@
+import {input} from '#composite';
 import find from '#find';
-import {empty, stitchArrays} from '#sugar';
-import {isDate, isTrackSectionList} from '#validators';
-import {filterMultipleArrays} from '#wiki-data';
+import {isDate} from '#validators';
+
+import {exposeDependency, exposeUpdateValueOrContinue}
+  from '#composite/control-flow';
+import {exitWithoutContribs} from '#composite/wiki-data';
 
 import {
-  exitWithoutDependency,
-  exitWithoutUpdateValue,
-  exposeDependency,
-  exposeUpdateValueOrContinue,
-  input,
-  fillMissingListItems,
-  withFlattenedList,
-  withPropertiesFromList,
-  withUnflattenedList,
-} from '#composite';
-
-import Thing, {
   additionalFiles,
   commentary,
   color,
@@ -24,7 +15,6 @@ import Thing, {
   contributionList,
   dimensions,
   directory,
-  exitWithoutContribs,
   fileExtension,
   flag,
   name,
@@ -33,8 +23,14 @@ import Thing, {
   simpleString,
   urls,
   wikiData,
-  withResolvedReferenceList,
-} from './thing.js';
+} from '#composite/wiki-properties';
+
+import {
+  withTracks,
+  withTrackSections,
+} from '#composite/things/album';
+
+import Thing from './thing.js';
 
 export class Album extends Thing {
   static [Thing.referenceType] = 'album';
@@ -101,100 +97,8 @@ export class Album extends Thing {
     additionalFiles: additionalFiles(),
 
     trackSections: [
-      exitWithoutDependency({
-        dependency: 'trackData',
-        value: input.value([]),
-      }),
-
-      exitWithoutUpdateValue({
-        mode: input.value('empty'),
-        value: input.value([]),
-      }),
-
-      withPropertiesFromList({
-        list: input.updateValue(),
-        prefix: input.value('#sections'),
-        properties: input.value([
-          'tracks',
-          'dateOriginallyReleased',
-          'isDefaultTrackSection',
-          'color',
-        ]),
-      }),
-
-      fillMissingListItems({
-        list: '#sections.tracks',
-        fill: input.value([]),
-      }),
-
-      fillMissingListItems({
-        list: '#sections.isDefaultTrackSection',
-        fill: input.value(false),
-      }),
-
-      fillMissingListItems({
-        list: '#sections.color',
-        fill: input.dependency('color'),
-      }),
-
-      withFlattenedList({
-        list: '#sections.tracks',
-      }).outputs({
-        ['#flattenedList']: '#trackRefs',
-        ['#flattenedIndices']: '#sections.startIndex',
-      }),
-
-      withResolvedReferenceList({
-        list: '#trackRefs',
-        data: 'trackData',
-        notFoundMode: input.value('null'),
-        find: input.value(find.track),
-      }).outputs({
-        ['#resolvedReferenceList']: '#tracks',
-      }),
-
-      withUnflattenedList({
-        list: '#tracks',
-        indices: '#sections.startIndex',
-      }).outputs({
-        ['#unflattenedList']: '#sections.tracks',
-      }),
-
-      {
-        flags: {update: true, expose: true},
-
-        update: {validate: isTrackSectionList},
-
-        expose: {
-          dependencies: [
-            '#sections.tracks',
-            '#sections.color',
-            '#sections.dateOriginallyReleased',
-            '#sections.isDefaultTrackSection',
-            '#sections.startIndex',
-          ],
-
-          transform(trackSections, {
-            '#sections.tracks': tracks,
-            '#sections.color': color,
-            '#sections.dateOriginallyReleased': dateOriginallyReleased,
-            '#sections.isDefaultTrackSection': isDefaultTrackSection,
-            '#sections.startIndex': startIndex,
-          }) {
-            filterMultipleArrays(
-              tracks, color, dateOriginallyReleased, isDefaultTrackSection, startIndex,
-              tracks => !empty(tracks));
-
-            return stitchArrays({
-              tracks,
-              color,
-              dateOriginallyReleased,
-              isDefaultTrackSection,
-              startIndex,
-            });
-          }
-        },
-      },
+      withTrackSections(),
+      exposeDependency({dependency: '#trackSections'}),
     ],
 
     artistContribs: contributionList(),
@@ -231,33 +135,8 @@ export class Album extends Thing {
     hasBannerArt: contribsPresent({contribs: 'bannerArtistContribs'}),
 
     tracks: [
-      exitWithoutDependency({
-        dependency: 'trackData',
-        value: input.value([]),
-      }),
-
-      exitWithoutDependency({
-        dependency: 'trackSections',
-        mode: input.value('empty'),
-        value: input.value([]),
-      }),
-
-      {
-        dependencies: ['trackSections'],
-        compute: (continuation, {trackSections}) =>
-          continuation({
-            '#trackRefs': trackSections
-              .flatMap(section => section.tracks ?? []),
-          }),
-      },
-
-      withResolvedReferenceList({
-        list: '#trackRefs',
-        data: 'trackData',
-        find: input.value(find.track),
-      }),
-
-      exposeDependency({dependency: '#resolvedReferenceList'}),
+      withTracks(),
+      exposeDependency({dependency: '#tracks'}),
     ],
   });
 
diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js
index ba3cbd0d..1266a4e0 100644
--- a/src/data/things/art-tag.js
+++ b/src/data/things/art-tag.js
@@ -1,14 +1,18 @@
-import {exposeUpdateValueOrContinue, input} from '#composite';
+import {input} from '#composite';
 import {sortAlbumsTracksChronologically} from '#wiki-data';
 import {isName} from '#validators';
 
-import Thing, {
+import {exposeUpdateValueOrContinue} from '#composite/control-flow';
+
+import {
   color,
   directory,
   flag,
   name,
   wikiData,
-} from './thing.js';
+} from '#composite/wiki-properties';
+
+import Thing from './thing.js';
 
 export class ArtTag extends Thing {
   static [Thing.referenceType] = 'tag';
diff --git a/src/data/things/artist.js b/src/data/things/artist.js
index 085e5663..ff9f8aee 100644
--- a/src/data/things/artist.js
+++ b/src/data/things/artist.js
@@ -2,7 +2,7 @@ import {input} from '#composite';
 import find from '#find';
 import {isName, validateArrayItems} from '#validators';
 
-import Thing, {
+import {
   directory,
   fileExtension,
   flag,
@@ -11,7 +11,9 @@ import Thing, {
   singleReference,
   urls,
   wikiData,
-} from './thing.js';
+} from '#composite/wiki-properties';
+
+import Thing from './thing.js';
 
 export class Artist extends Thing {
   static [Thing.referenceType] = 'artist';
diff --git a/src/data/things/composite.js b/src/data/things/composite.js
index c03f8833..7e068dce 100644
--- a/src/data/things/composite.js
+++ b/src/data/things/composite.js
@@ -2,14 +2,7 @@ import {inspect} from 'node:util';
 
 import {colors} from '#cli';
 import {TupleMap} from '#wiki-data';
-
-import {
-  a,
-  is,
-  isString,
-  isWholeNumber,
-  validateArrayItems,
-} from '#validators';
+import {a} from '#validators';
 
 import {
   decorateErrorWithIndex,
@@ -1639,721 +1632,3 @@ export function debugComposite(fn) {
   compositeFrom.debug = false;
   return value;
 }
-
-// Exposes a dependency exactly as it is; this is typically the base of a
-// composition which was created to serve as one property's descriptor.
-//
-// Please note that this *doesn't* verify that the dependency exists, so
-// if you provide the wrong name or it hasn't been set by a previous
-// compositional step, the property will be exposed as undefined instead
-// of null.
-//
-export const exposeDependency = templateCompositeFrom({
-  annotation: `exposeDependency`,
-
-  compose: false,
-
-  inputs: {
-    dependency: input.staticDependency({acceptsNull: true}),
-  },
-
-  steps: () => [
-    {
-      dependencies: [input('dependency')],
-      compute: ({
-        [input('dependency')]: dependency
-      }) => dependency,
-    },
-  ],
-});
-
-// Exposes a constant value exactly as it is; like exposeDependency, this
-// is typically the base of a composition serving as a particular property
-// descriptor. It generally follows steps which will conditionally early
-// exit with some other value, with the exposeConstant base serving as the
-// fallback default value.
-export const exposeConstant = templateCompositeFrom({
-  annotation: `exposeConstant`,
-
-  compose: false,
-
-  inputs: {
-    value: input.staticValue(),
-  },
-
-  steps: () => [
-    {
-      dependencies: [input('value')],
-      compute: ({
-        [input('value')]: value,
-      }) => value,
-    },
-  ],
-});
-
-// Checks the availability of a dependency and provides the result to later
-// steps under '#availability' (by default). This is mainly intended for use
-// by the more specific utilities, which you should consider using instead.
-// Customize {mode} to select one of these modes, or default to 'null':
-//
-// * 'null':  Check that the value isn't null (and not undefined either).
-// * 'empty': Check that the value is neither null, undefined, nor an empty
-//            array.
-// * 'falsy': Check that the value isn't false when treated as a boolean
-//            (nor an empty array). Keep in mind this will also be false
-//            for values like zero and the empty string!
-//
-
-const inputAvailabilityCheckMode = () => input({
-  validate: is('null', 'empty', 'falsy'),
-  defaultValue: 'null',
-});
-
-export const withResultOfAvailabilityCheck = templateCompositeFrom({
-  annotation: `withResultOfAvailabilityCheck`,
-
-  inputs: {
-    from: input({acceptsNull: true}),
-    mode: inputAvailabilityCheckMode(),
-  },
-
-  outputs: ['#availability'],
-
-  steps: () => [
-    {
-      dependencies: [input('from'), input('mode')],
-
-      compute: (continuation, {
-        [input('from')]: value,
-        [input('mode')]: mode,
-      }) => {
-        let availability;
-
-        switch (mode) {
-          case 'null':
-            availability = value !== undefined && value !== null;
-            break;
-
-          case 'empty':
-            availability = value !== undefined && !empty(value);
-            break;
-
-          case 'falsy':
-            availability = !!value && (!Array.isArray(value) || !empty(value));
-            break;
-        }
-
-        return continuation({'#availability': availability});
-      },
-    },
-  ],
-});
-
-// Exposes a dependency as it is, or continues if it's unavailable.
-// See withResultOfAvailabilityCheck for {mode} options!
-export const exposeDependencyOrContinue = templateCompositeFrom({
-  annotation: `exposeDependencyOrContinue`,
-
-  inputs: {
-    dependency: input({acceptsNull: true}),
-    mode: inputAvailabilityCheckMode(),
-  },
-
-  steps: () => [
-    withResultOfAvailabilityCheck({
-      from: input('dependency'),
-      mode: input('mode'),
-    }),
-
-    {
-      dependencies: ['#availability', input('dependency')],
-      compute: (continuation, {
-        ['#availability']: availability,
-        [input('dependency')]: dependency,
-      }) =>
-        (availability
-          ? continuation.exit(dependency)
-          : continuation()),
-    },
-  ],
-});
-
-// Exposes the update value of an {update: true} property as it is,
-// or continues if it's unavailable. See withResultOfAvailabilityCheck
-// for {mode} options! Also provide {validate} here to conveniently
-// set a custom validation check for this property's update value.
-export const exposeUpdateValueOrContinue = templateCompositeFrom({
-  annotation: `exposeUpdateValueOrContinue`,
-
-  inputs: {
-    mode: inputAvailabilityCheckMode(),
-
-    validate: input({
-      type: 'function',
-      defaultValue: null,
-    }),
-  },
-
-  update: ({
-    [input.staticValue('validate')]: validate,
-  }) =>
-    (validate
-      ? {validate}
-      : {}),
-
-  steps: () => [
-    exposeDependencyOrContinue({
-      dependency: input.updateValue(),
-      mode: input('mode'),
-    }),
-  ],
-});
-
-// Early exits if a dependency isn't available.
-// See withResultOfAvailabilityCheck for {mode} options!
-export const exitWithoutDependency = templateCompositeFrom({
-  annotation: `exitWithoutDependency`,
-
-  inputs: {
-    dependency: input({acceptsNull: true}),
-    mode: inputAvailabilityCheckMode(),
-    value: input({defaultValue: null}),
-  },
-
-  steps: () => [
-    withResultOfAvailabilityCheck({
-      from: input('dependency'),
-      mode: input('mode'),
-    }),
-
-    {
-      dependencies: ['#availability', input('value')],
-      compute: (continuation, {
-        ['#availability']: availability,
-        [input('value')]: value,
-      }) =>
-        (availability
-          ? continuation()
-          : continuation.exit(value)),
-    },
-  ],
-});
-
-// Early exits if this property's update value isn't available.
-// See withResultOfAvailabilityCheck for {mode} options!
-export const exitWithoutUpdateValue = templateCompositeFrom({
-  annotation: `exitWithoutUpdateValue`,
-
-  inputs: {
-    mode: inputAvailabilityCheckMode(),
-    value: input({defaultValue: null}),
-  },
-
-  steps: () => [
-    exitWithoutDependency({
-      dependency: input.updateValue(),
-      mode: input('mode'),
-      value: input('value'),
-    }),
-  ],
-});
-
-// Raises if a dependency isn't available.
-// See withResultOfAvailabilityCheck for {mode} options!
-export const raiseOutputWithoutDependency = templateCompositeFrom({
-  annotation: `raiseOutputWithoutDependency`,
-
-  inputs: {
-    dependency: input({acceptsNull: true}),
-    mode: inputAvailabilityCheckMode(),
-    output: input.staticValue({defaultValue: {}}),
-  },
-
-  outputs: ({
-    [input.staticValue('output')]: output,
-  }) => Object.keys(output),
-
-  steps: () => [
-    withResultOfAvailabilityCheck({
-      from: input('dependency'),
-      mode: input('mode'),
-    }),
-
-    {
-      dependencies: ['#availability', input('output')],
-      compute: (continuation, {
-        ['#availability']: availability,
-        [input('output')]: output,
-      }) =>
-        (availability
-          ? continuation()
-          : continuation.raiseOutputAbove(output)),
-    },
-  ],
-});
-
-// Raises if this property's update value isn't available.
-// See withResultOfAvailabilityCheck for {mode} options!
-export const raiseOutputWithoutUpdateValue = templateCompositeFrom({
-  annotation: `raiseOutputWithoutUpdateValue`,
-
-  inputs: {
-    mode: inputAvailabilityCheckMode(),
-    output: input.staticValue({defaultValue: {}}),
-  },
-
-  outputs: ({
-    [input.staticValue('output')]: output,
-  }) => Object.keys(output),
-
-  steps: () => [
-    withResultOfAvailabilityCheck({
-      from: input.updateValue(),
-      mode: input('mode'),
-    }),
-
-    {
-      dependencies: ['#availability', input('output')],
-      compute: (continuation, {
-        ['#availability']: availability,
-        [input('output')]: output,
-      }) =>
-        (availability
-          ? continuation()
-          : continuation.raiseOutputAbove(output)),
-    },
-  ],
-});
-
-// Gets a property of some object (in a dependency) and provides that value.
-// If the object itself is null, or the object doesn't have the listed property,
-// the provided dependency will also be null.
-export const withPropertyFromObject = templateCompositeFrom({
-  annotation: `withPropertyFromObject`,
-
-  inputs: {
-    object: input({type: 'object', acceptsNull: true}),
-    property: input({type: 'string'}),
-  },
-
-  outputs: ({
-    [input.staticDependency('object')]: object,
-    [input.staticValue('property')]: property,
-  }) =>
-    (object && property
-      ? (object.startsWith('#')
-          ? [`${object}.${property}`]
-          : [`#${object}.${property}`])
-      : ['#value']),
-
-  steps: () => [
-    {
-      dependencies: [
-        input.staticDependency('object'),
-        input.staticValue('property'),
-      ],
-
-      compute: (continuation, {
-        [input.staticDependency('object')]: object,
-        [input.staticValue('property')]: property,
-      }) => continuation({
-        '#output':
-          (object && property
-            ? (object.startsWith('#')
-                ? `${object}.${property}`
-                : `#${object}.${property}`)
-            : '#value'),
-      }),
-    },
-
-    {
-      dependencies: [
-        '#output',
-        input('object'),
-        input('property'),
-      ],
-
-      compute: (continuation, {
-        ['#output']: output,
-        [input('object')]: object,
-        [input('property')]: property,
-      }) => continuation({
-        [output]:
-          (object === null
-            ? null
-            : object[property] ?? null),
-      }),
-    },
-  ],
-});
-
-// Gets the listed properties from some object, providing each property's value
-// as a dependency prefixed with the same name as the object (by default).
-// If the object itself is null, all provided dependencies will be null;
-// if it's missing only select properties, those will be provided as null.
-export const withPropertiesFromObject = templateCompositeFrom({
-  annotation: `withPropertiesFromObject`,
-
-  inputs: {
-    object: input({type: 'object', acceptsNull: true}),
-
-    properties: input({
-      type: 'array',
-      validate: validateArrayItems(isString),
-    }),
-
-    prefix: input.staticValue({type: 'string', defaultValue: null}),
-  },
-
-  outputs: ({
-    [input.staticDependency('object')]: object,
-    [input.staticValue('properties')]: properties,
-    [input.staticValue('prefix')]: prefix,
-  }) =>
-    (properties
-      ? properties.map(property =>
-          (prefix
-            ? `${prefix}.${property}`
-         : object
-            ? `${object}.${property}`
-            : `#object.${property}`))
-      : ['#object']),
-
-  steps: () => [
-    {
-      dependencies: [input('object'), input('properties')],
-      compute: (continuation, {
-        [input('object')]: object,
-        [input('properties')]: properties,
-      }) => continuation({
-        ['#entries']:
-          (object === null
-            ? properties.map(property => [property, null])
-            : properties.map(property => [property, object[property]])),
-      }),
-    },
-
-    {
-      dependencies: [
-        input.staticDependency('object'),
-        input.staticValue('properties'),
-        input.staticValue('prefix'),
-        '#entries',
-      ],
-
-      compute: (continuation, {
-        [input.staticDependency('object')]: object,
-        [input.staticValue('properties')]: properties,
-        [input.staticValue('prefix')]: prefix,
-        ['#entries']: entries,
-      }) =>
-        (properties
-          ? continuation(
-              Object.fromEntries(
-                entries.map(([property, value]) => [
-                  (prefix
-                    ? `${prefix}.${property}`
-                 : object
-                    ? `${object}.${property}`
-                    : `#object.${property}`),
-                  value ?? null,
-                ])))
-          : continuation({
-              ['#object']:
-                Object.fromEntries(entries),
-            })),
-    },
-  ],
-});
-
-// Gets a property from each of a list of objects (in a dependency) and
-// provides the results. This doesn't alter any list indices, so positions
-// which were null in the original list are kept null here. Objects which don't
-// have the specified property are retained in-place as null.
-export function withPropertyFromList({
-  list,
-  property,
-  into = null,
-}) {
-  into ??=
-    (list.startsWith('#')
-      ? `${list}.${property}`
-      : `#${list}.${property}`);
-
-  return {
-    annotation: `withPropertyFromList`,
-    flags: {expose: true, compose: true},
-
-    expose: {
-      mapDependencies: {list},
-      mapContinuation: {into},
-      options: {property},
-
-      compute(continuation, {list, '#options': {property}}) {
-        if (list === undefined || empty(list)) {
-          return continuation({into: []});
-        }
-
-        return continuation({
-          into:
-            list.map(item =>
-              (item === null || item === undefined
-                ? null
-                : item[property] ?? null)),
-        });
-      },
-    },
-  };
-}
-
-// Gets the listed properties from each of a list of objects, providing lists
-// of property values each into a dependency prefixed with the same name as the
-// list (by default). Like withPropertyFromList, this doesn't alter indices.
-export const withPropertiesFromList = templateCompositeFrom({
-  annotation: `withPropertiesFromList`,
-
-  inputs: {
-    list: input({type: 'array'}),
-
-    properties: input({
-      validate: validateArrayItems(isString),
-    }),
-
-    prefix: input.staticValue({type: 'string', defaultValue: null}),
-  },
-
-  outputs: ({
-    [input.staticDependency('list')]: list,
-    [input.staticValue('properties')]: properties,
-    [input.staticValue('prefix')]: prefix,
-  }) =>
-    (properties
-      ? properties.map(property =>
-          (prefix
-            ? `${prefix}.${property}`
-         : list
-            ? `${list}.${property}`
-            : `#list.${property}`))
-      : ['#lists']),
-
-  steps: () => [
-    {
-      dependencies: [input('list'), input('properties')],
-      compute: (continuation, {
-        [input('list')]: list,
-        [input('properties')]: properties,
-      }) => continuation({
-        ['#lists']:
-          Object.fromEntries(
-            properties.map(property => [
-              property,
-              list.map(item => item[property] ?? null),
-            ])),
-      }),
-    },
-
-    {
-      dependencies: [
-        input.staticDependency('list'),
-        input.staticValue('properties'),
-        input.staticValue('prefix'),
-        '#lists',
-      ],
-
-      compute: (continuation, {
-        [input.staticDependency('list')]: list,
-        [input.staticValue('properties')]: properties,
-        [input.staticValue('prefix')]: prefix,
-        ['#lists']: lists,
-      }) =>
-        (properties
-          ? continuation(
-              Object.fromEntries(
-                properties.map(property => [
-                  (prefix
-                    ? `${prefix}.${property}`
-                 : list
-                    ? `${list}.${property}`
-                    : `#list.${property}`),
-                  lists[property],
-                ])))
-          : continuation({'#lists': lists})),
-    },
-  ],
-});
-
-// Replaces items of a list, which are null or undefined, with some fallback
-// value. By default, this replaces the passed dependency.
-export const fillMissingListItems = templateCompositeFrom({
-  annotation: `fillMissingListItems`,
-
-  inputs: {
-    list: input({type: 'array'}),
-    fill: input({acceptsNull: true}),
-  },
-
-  outputs: ({
-    [input.staticDependency('list')]: list,
-  }) => [list ?? '#list'],
-
-  steps: () => [
-    {
-      dependencies: [input('list'), input('fill')],
-      compute: (continuation, {
-        [input('list')]: list,
-        [input('fill')]: fill,
-      }) => continuation({
-        ['#filled']:
-          list.map(item => item ?? fill),
-      }),
-    },
-
-    {
-      dependencies: [input.staticDependency('list'), '#filled'],
-      compute: (continuation, {
-        [input.staticDependency('list')]: list,
-        ['#filled']: filled,
-      }) => continuation({
-        [list ?? '#list']:
-          filled,
-      }),
-    },
-  ],
-});
-
-// Filters particular values out of a list. Note that this will always
-// completely skip over null, but can be used to filter out any other
-// primitive or object value.
-export const excludeFromList = templateCompositeFrom({
-  annotation: `excludeFromList`,
-
-  inputs: {
-    list: input(),
-
-    item: input({defaultValue: null}),
-    items: input({type: 'array', defaultValue: null}),
-  },
-
-  outputs: ({
-    [input.staticDependency('list')]: list,
-  }) => [list ?? '#list'],
-
-  steps: () => [
-    {
-      dependencies: [
-        input.staticDependency('list'),
-        input('list'),
-        input('item'),
-        input('items'),
-      ],
-
-      compute: (continuation, {
-        [input.staticDependency('list')]: listName,
-        [input('list')]: listContents,
-        [input('item')]: excludeItem,
-        [input('items')]: excludeItems,
-      }) => continuation({
-        [listName ?? '#list']:
-          listContents.filter(item => {
-            if (excludeItem !== null && item === excludeItem) return false;
-            if (!empty(excludeItems) && excludeItems.includes(item)) return false;
-            return true;
-          }),
-      }),
-    },
-  ],
-});
-
-// Flattens an array with one level of nested arrays, providing as dependencies
-// both the flattened array as well as the original starting indices of each
-// successive source array.
-export const withFlattenedList = templateCompositeFrom({
-  annotation: `withFlattenedList`,
-
-  inputs: {
-    list: input({type: 'array'}),
-  },
-
-  outputs: ['#flattenedList', '#flattenedIndices'],
-
-  steps: () => [
-    {
-      dependencies: [input('list')],
-      compute(continuation, {
-        [input('list')]: sourceList,
-      }) {
-        const flattenedList = sourceList.flat();
-        const indices = [];
-        let lastEndIndex = 0;
-        for (const {length} of sourceList) {
-          indices.push(lastEndIndex);
-          lastEndIndex += length;
-        }
-
-        return continuation({
-          ['#flattenedList']: flattenedList,
-          ['#flattenedIndices']: indices,
-        });
-      },
-    },
-  ],
-});
-
-// After mapping the contents of a flattened array in-place (being careful to
-// retain the original indices by replacing unmatched results with null instead
-// of filtering them out), this function allows for recombining them. It will
-// filter out null and undefined items by default (pass {filter: false} to
-// disable this).
-export const withUnflattenedList = templateCompositeFrom({
-  annotation: `withUnflattenedList`,
-
-  inputs: {
-    list: input({
-      type: 'array',
-      defaultDependency: '#flattenedList',
-    }),
-
-    indices: input({
-      validate: validateArrayItems(isWholeNumber),
-      defaultDependency: '#flattenedIndices',
-    }),
-
-    filter: input({
-      type: 'boolean',
-      defaultValue: true,
-    }),
-  },
-
-  outputs: ['#unflattenedList'],
-
-  steps: () => [
-    {
-      dependencies: [input('list'), input('indices'), input('filter')],
-      compute(continuation, {
-        [input('list')]: list,
-        [input('indices')]: indices,
-        [input('filter')]: filter,
-      }) {
-        const unflattenedList = [];
-
-        for (let i = 0; i < indices.length; i++) {
-          const startIndex = indices[i];
-          const endIndex =
-            (i === indices.length - 1
-              ? list.length
-              : indices[i + 1]);
-
-          const values = list.slice(startIndex, endIndex);
-          unflattenedList.push(
-            (filter
-              ? values.filter(value => value !== null && value !== undefined)
-              : values));
-        }
-
-        return continuation({
-          ['#unflattenedList']: unflattenedList,
-        });
-      },
-    },
-  ],
-});
diff --git a/src/data/things/flash.js b/src/data/things/flash.js
index c3f90260..8fb1edfa 100644
--- a/src/data/things/flash.js
+++ b/src/data/things/flash.js
@@ -9,7 +9,7 @@ import {
   oneOf,
 } from '#validators';
 
-import Thing, {
+import {
   color,
   contributionList,
   fileExtension,
@@ -19,7 +19,9 @@ import Thing, {
   simpleString,
   urls,
   wikiData,
-} from './thing.js';
+} from '#composite/wiki-properties';
+
+import Thing from './thing.js';
 
 export class Flash extends Thing {
   static [Thing.referenceType] = 'flash';
diff --git a/src/data/things/group.js b/src/data/things/group.js
index 0b117801..d5ae03e7 100644
--- a/src/data/things/group.js
+++ b/src/data/things/group.js
@@ -1,7 +1,7 @@
 import {input} from '#composite';
 import find from '#find';
 
-import Thing, {
+import {
   color,
   directory,
   name,
@@ -9,7 +9,9 @@ import Thing, {
   simpleString,
   urls,
   wikiData,
-} from './thing.js';
+} from '#composite/wiki-properties';
+
+import Thing from './thing.js';
 
 export class Group extends Thing {
   static [Thing.referenceType] = 'group';
diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js
index bcf99e80..de9d0e50 100644
--- a/src/data/things/homepage-layout.js
+++ b/src/data/things/homepage-layout.js
@@ -1,11 +1,7 @@
+import {input} from '#composite';
 import find from '#find';
 
 import {
-  exposeDependency,
-  input,
-} from '#composite';
-
-import {
   is,
   isCountingNumber,
   isString,
@@ -16,14 +12,18 @@ import {
   validateReference,
 } from '#validators';
 
-import Thing, {
+import {exposeDependency} from '#composite/control-flow';
+import {withResolvedReference} from '#composite/wiki-data';
+
+import {
   color,
   name,
   referenceList,
   simpleString,
   wikiData,
-  withResolvedReference,
-} from './thing.js';
+} from '#composite/wiki-properties';
+
+import Thing from './thing.js';
 
 export class HomepageLayout extends Thing {
   static [Thing.getPropertyDescriptors] = ({HomepageLayoutRow}) => ({
diff --git a/src/data/things/language.js b/src/data/things/language.js
index a325d6a6..fe74f7bf 100644
--- a/src/data/things/language.js
+++ b/src/data/things/language.js
@@ -1,13 +1,14 @@
 import {Tag} from '#html';
 import {isLanguageCode} from '#validators';
 
-import CacheableObject from './cacheable-object.js';
-
-import Thing, {
+import {
   externalFunction,
   flag,
   simpleString,
-} from './thing.js';
+} from '#composite/wiki-properties';
+
+import CacheableObject from './cacheable-object.js';
+import Thing from './thing.js';
 
 export class Language extends Thing {
   static [Thing.getPropertyDescriptors] = () => ({
diff --git a/src/data/things/news-entry.js b/src/data/things/news-entry.js
index 6984874e..ba065c25 100644
--- a/src/data/things/news-entry.js
+++ b/src/data/things/news-entry.js
@@ -1,9 +1,11 @@
-import Thing, {
+import {
   directory,
   name,
   simpleDate,
   simpleString,
-} from './thing.js';
+} from '#composite/wiki-properties';
+
+import Thing from './thing.js';
 
 export class NewsEntry extends Thing {
   static [Thing.referenceType] = 'news-entry';
diff --git a/src/data/things/static-page.js b/src/data/things/static-page.js
index 0133e0b6..f03e4405 100644
--- a/src/data/things/static-page.js
+++ b/src/data/things/static-page.js
@@ -1,10 +1,12 @@
 import {isName} from '#validators';
 
-import Thing, {
+import {
   directory,
   name,
   simpleString,
-} from './thing.js';
+} from '#composite/wiki-properties';
+
+import Thing from './thing.js';
 
 export class StaticPage extends Thing {
   static [Thing.referenceType] = 'static';
diff --git a/src/data/things/thing.js b/src/data/things/thing.js
index f1302e17..a47f8506 100644
--- a/src/data/things/thing.js
+++ b/src/data/things/thing.js
@@ -1,48 +1,9 @@
-// Thing: base class for wiki data types, providing wiki-specific utility
-// functions on top of essential CacheableObject behavior.
+// Thing: base class for wiki data types, providing interfaces generally useful
+// to all wiki data objects on top of foundational CacheableObject behavior.
 
 import {inspect} from 'node:util';
 
 import {colors} from '#cli';
-import find from '#find';
-import {stitchArrays, unique} from '#sugar';
-import {filterMultipleArrays, getKebabCase} from '#wiki-data';
-import {is} from '#validators';
-
-import {
-  compositeFrom,
-  exitWithoutDependency,
-  exposeConstant,
-  exposeDependency,
-  exposeDependencyOrContinue,
-  input,
-  raiseOutputWithoutDependency,
-  templateCompositeFrom,
-  withResultOfAvailabilityCheck,
-  withPropertiesFromList,
-} from '#composite';
-
-import {
-  isAdditionalFileList,
-  isBoolean,
-  isColor,
-  isCommentary,
-  isContributionList,
-  isDate,
-  isDimensions,
-  isDirectory,
-  isDuration,
-  isFileExtension,
-  isName,
-  isString,
-  isType,
-  isURL,
-  validateArrayItems,
-  validateInstanceOf,
-  validateReference,
-  validateReferenceList,
-  validateWikiData,
-} from '#validators';
 
 import CacheableObject from './cacheable-object.js';
 
@@ -77,673 +38,3 @@ export default class Thing extends CacheableObject {
     return `${thing.constructor[Thing.referenceType]}:${thing.directory}`;
   }
 }
-
-// Property descriptor templates
-//
-// Regularly reused property descriptors, for ease of access and generally
-// duplicating less code across wiki data types. These are specialized utility
-// functions, so check each for how its own arguments behave!
-
-export function name(defaultName) {
-  return {
-    flags: {update: true, expose: true},
-    update: {validate: isName, default: defaultName},
-  };
-}
-
-export function color() {
-  return {
-    flags: {update: true, expose: true},
-    update: {validate: isColor},
-  };
-}
-
-export function directory() {
-  return {
-    flags: {update: true, expose: true},
-    update: {validate: isDirectory},
-    expose: {
-      dependencies: ['name'],
-      transform(directory, {name}) {
-        if (directory === null && name === null) return null;
-        else if (directory === null) return getKebabCase(name);
-        else return directory;
-      },
-    },
-  };
-}
-
-export function urls() {
-  return {
-    flags: {update: true, expose: true},
-    update: {validate: validateArrayItems(isURL)},
-    expose: {transform: (value) => value ?? []},
-  };
-}
-
-// A file extension! Or the default, if provided when calling this.
-export function fileExtension(defaultFileExtension = null) {
-  return {
-    flags: {update: true, expose: true},
-    update: {validate: isFileExtension},
-    expose: {transform: (value) => value ?? defaultFileExtension},
-  };
-}
-
-// Plain ol' image dimensions. This is a two-item array of positive integers,
-// corresponding to width and height respectively.
-export function dimensions() {
-  return {
-    flags: {update: true, expose: true},
-    update: {validate: isDimensions},
-  };
-}
-
-// Duration! This is a number of seconds, possibly floating point, always
-// at minimum zero.
-export function duration() {
-  return {
-    flags: {update: true, expose: true},
-    update: {validate: isDuration},
-  };
-}
-
-// Straightforward flag descriptor for a variety of property purposes.
-// Provide a default value, true or false!
-export function flag(defaultValue = false) {
-  // TODO:                        ^ Are you actually kidding me
-  if (typeof defaultValue !== 'boolean') {
-    throw new TypeError(`Always set explicit defaults for flags!`);
-  }
-
-  return {
-    flags: {update: true, expose: true},
-    update: {validate: isBoolean, default: defaultValue},
-  };
-}
-
-// General date type, used as the descriptor for a bunch of properties.
-// This isn't dynamic though - it won't inherit from a date stored on
-// another object, for example.
-export function simpleDate() {
-  return {
-    flags: {update: true, expose: true},
-    update: {validate: isDate},
-  };
-}
-
-// General string type. This should probably generally be avoided in favor
-// of more specific validation, but using it makes it easy to find where we
-// might want to improve later, and it's a useful shorthand meanwhile.
-export function simpleString() {
-  return {
-    flags: {update: true, expose: true},
-    update: {validate: isString},
-  };
-}
-
-// External function. These should only be used as dependencies for other
-// properties, so they're left unexposed.
-export function externalFunction() {
-  return {
-    flags: {update: true},
-    update: {validate: (t) => typeof t === 'function'},
-  };
-}
-
-// Strong 'n sturdy contribution list, rolling a list of references (provided
-// as this property's update value) and the resolved results (as get exposed)
-// into one property. Update value will look something like this:
-//
-//   [
-//     {who: 'Artist Name', what: 'Viola'},
-//     {who: 'artist:john-cena', what: null},
-//     ...
-//   ]
-//
-// ...typically as processed from YAML, spreadsheet, or elsewhere.
-// Exposes as the same, but with the "who" replaced with matches found in
-// artistData - which means this always depends on an `artistData` property
-// also existing on this object!
-//
-export function contributionList() {
-  return compositeFrom({
-    annotation: `contributionList`,
-
-    compose: false,
-
-    update: {validate: isContributionList},
-
-    steps: [
-      withResolvedContribs({from: input.updateValue()}),
-      exposeDependencyOrContinue({dependency: '#resolvedContribs'}),
-      exposeConstant({value: input.value([])}),
-    ],
-  });
-}
-
-// Artist commentary! Generally present on tracks and albums.
-export function commentary() {
-  return {
-    flags: {update: true, expose: true},
-    update: {validate: isCommentary},
-  };
-}
-
-// This is a somewhat more involved data structure - it's for additional
-// or "bonus" files associated with albums or tracks (or anything else).
-// It's got this form:
-//
-//     [
-//         {title: 'Booklet', files: ['Booklet.pdf']},
-//         {
-//             title: 'Wallpaper',
-//             description: 'Cool Wallpaper!',
-//             files: ['1440x900.png', '1920x1080.png']
-//         },
-//         {title: 'Alternate Covers', description: null, files: [...]},
-//         ...
-//     ]
-//
-export function additionalFiles() {
-  return {
-    flags: {update: true, expose: true},
-    update: {validate: isAdditionalFileList},
-    expose: {
-      transform: (additionalFiles) =>
-        additionalFiles ?? [],
-    },
-  };
-}
-
-const thingClassInput = {
-  validate(thingClass) {
-    isType(thingClass, 'function');
-
-    if (!Object.hasOwn(thingClass, Thing.referenceType)) {
-      throw new TypeError(`Expected a Thing constructor, missing Thing.referenceType`);
-    }
-
-    return true;
-  },
-};
-
-// A reference list! Keep in mind this is for general references to wiki
-// objects of (usually) other Thing subclasses, not specifically leitmotif
-// references in tracks (although that property uses referenceList too!).
-//
-// The underlying function validateReferenceList expects a string like
-// 'artist' or 'track', but this utility keeps from having to hard-code the
-// string in multiple places by referencing the value saved on the class
-// instead.
-export const referenceList = templateCompositeFrom({
-  annotation: `referenceList`,
-
-  compose: false,
-
-  inputs: {
-    class: input.staticValue(thingClassInput),
-
-    data: inputWikiData({allowMixedTypes: false}),
-    find: input({type: 'function'}),
-  },
-
-  update: ({
-    [input.staticValue('class')]: thingClass,
-  }) => {
-    const {[Thing.referenceType]: referenceType} = thingClass;
-    return {validate: validateReferenceList(referenceType)};
-  },
-
-  steps: () => [
-    withResolvedReferenceList({
-      list: input.updateValue(),
-      data: input('data'),
-      find: input('find'),
-    }),
-
-    exposeDependency({dependency: '#resolvedReferenceList'}),
-  ],
-});
-
-// Corresponding function for a single reference.
-export const singleReference = templateCompositeFrom({
-  annotation: `singleReference`,
-
-  compose: false,
-
-  inputs: {
-    class: input(thingClassInput),
-    find: input({type: 'function'}),
-    data: inputWikiData({allowMixedTypes: false}),
-  },
-
-  update: ({
-    [input.staticValue('class')]: thingClass,
-  }) => {
-    const {[Thing.referenceType]: referenceType} = thingClass;
-    return {validate: validateReference(referenceType)};
-  },
-
-  steps: () => [
-    withResolvedReference({
-      ref: input.updateValue(),
-      data: input('data'),
-      find: input('find'),
-    }),
-
-    exposeDependency({dependency: '#resolvedReference'}),
-  ],
-});
-
-// Nice 'n simple shorthand for an exposed-only flag which is true when any
-// contributions are present in the specified property.
-export const contribsPresent = templateCompositeFrom({
-  annotation: `contribsPresent`,
-
-  compose: false,
-
-  inputs: {
-    contribs: input.staticDependency({
-      validate: isContributionList,
-      acceptsNull: true,
-    }),
-  },
-
-  steps: () => [
-    withResultOfAvailabilityCheck({
-      from: input('contribs'),
-      mode: input.value('empty'),
-    }),
-
-    exposeDependency({dependency: '#availability'}),
-  ],
-});
-
-// Neat little shortcut for "reversing" the reference lists stored on other
-// things - for example, tracks specify a "referenced tracks" property, and
-// you would use this to compute a corresponding "referenced *by* tracks"
-// property. Naturally, the passed ref list property is of the things in the
-// wiki data provided, not the requesting Thing itself.
-export const reverseReferenceList = templateCompositeFrom({
-  annotation: `reverseReferenceList`,
-
-  compose: false,
-
-  inputs: {
-    data: inputWikiData({allowMixedTypes: false}),
-    list: input({type: 'string'}),
-  },
-
-  steps: () => [
-    withReverseReferenceList({
-      data: input('data'),
-      list: input('list'),
-    }),
-
-    exposeDependency({dependency: '#reverseReferenceList'}),
-  ],
-});
-
-// General purpose wiki data constructor, for properties like artistData,
-// trackData, etc.
-export function wikiData(thingClass) {
-  return {
-    flags: {update: true},
-    update: {
-      validate: validateArrayItems(validateInstanceOf(thingClass)),
-    },
-  };
-}
-
-// This one's kinda tricky: it parses artist "references" from the
-// commentary content, and finds the matching artist for each reference.
-// This is mostly useful for credits and listings on artist pages.
-export const commentatorArtists = templateCompositeFrom({
-  annotation: `commentatorArtists`,
-
-  compose: false,
-
-  steps: () => [
-    exitWithoutDependency({
-      dependency: 'commentary',
-      mode: input.value('falsy'),
-      value: input.value([]),
-    }),
-
-    {
-      dependencies: ['commentary'],
-      compute: (continuation, {commentary}) =>
-        continuation({
-          '#artistRefs':
-            Array.from(
-              commentary
-                .replace(/<\/?b>/g, '')
-                .matchAll(/<i>(?<who>.*?):<\/i>/g))
-              .map(({groups: {who}}) => who),
-        }),
-    },
-
-    withResolvedReferenceList({
-      list: '#artistRefs',
-      data: 'artistData',
-      find: input.value(find.artist),
-    }).outputs({
-      '#resolvedReferenceList': '#artists',
-    }),
-
-    {
-      flags: {expose: true},
-
-      expose: {
-        dependencies: ['#artists'],
-        compute: ({'#artists': artists}) =>
-          unique(artists),
-      },
-    },
-  ],
-});
-
-// Compositional utilities
-
-// TODO: This doesn't access a class's own ThingSubclass[Thing.referenceType]
-// value because classes aren't initialized by when templateCompositeFrom gets
-// called (see: circular imports). So the reference types have to be hard-coded,
-// which somewhat defeats the point of storing them on the class in the first
-// place...
-export function inputWikiData({
-  referenceType = '',
-  allowMixedTypes = false,
-} = {}) {
-  return input({
-    validate: validateWikiData({referenceType, allowMixedTypes}),
-    acceptsNull: true,
-  });
-}
-
-// Resolves the contribsByRef contained in the provided dependency,
-// providing (named by the second argument) the result. "Resolving"
-// means mapping the "who" reference of each contribution to an artist
-// object, and filtering out those whose "who" doesn't match any artist.
-export const withResolvedContribs = templateCompositeFrom({
-  annotation: `withResolvedContribs`,
-
-  inputs: {
-    from: input({
-      validate: isContributionList,
-      acceptsNull: true,
-    }),
-
-    notFoundMode: input({
-      validate: is('exit', 'filter', 'null'),
-      defaultValue: 'null',
-    }),
-  },
-
-  outputs: ['#resolvedContribs'],
-
-  steps: () => [
-    raiseOutputWithoutDependency({
-      dependency: input('from'),
-      mode: input.value('empty'),
-      output: input.value({
-        ['#resolvedContribs']: [],
-      }),
-    }),
-
-    withPropertiesFromList({
-      list: input('from'),
-      properties: input.value(['who', 'what']),
-      prefix: input.value('#contribs'),
-    }),
-
-    withResolvedReferenceList({
-      list: '#contribs.who',
-      data: 'artistData',
-      find: input.value(find.artist),
-      notFoundMode: input('notFoundMode'),
-    }).outputs({
-      ['#resolvedReferenceList']: '#contribs.who',
-    }),
-
-    {
-      dependencies: ['#contribs.who', '#contribs.what'],
-
-      compute(continuation, {
-        ['#contribs.who']: who,
-        ['#contribs.what']: what,
-      }) {
-        filterMultipleArrays(who, what, (who, _what) => who);
-        return continuation({
-          ['#resolvedContribs']: stitchArrays({who, what}),
-        });
-      },
-    },
-  ],
-});
-
-// Shorthand for exiting if the contribution list (usually a property's update
-// value) resolves to empty - ensuring that the later computed results are only
-// returned if these contributions are present.
-export const exitWithoutContribs = templateCompositeFrom({
-  annotation: `exitWithoutContribs`,
-
-  inputs: {
-    contribs: input({
-      validate: isContributionList,
-      acceptsNull: true,
-    }),
-
-    value: input({defaultValue: null}),
-  },
-
-  steps: () => [
-    withResolvedContribs({
-      from: input('contribs'),
-    }),
-
-    withResultOfAvailabilityCheck({
-      from: '#resolvedContribs',
-      mode: input.value('empty'),
-    }),
-
-    {
-      dependencies: ['#availability', input('value')],
-      compute: (continuation, {
-        ['#availability']: availability,
-        [input('value')]: value,
-      }) =>
-        (availability
-          ? continuation()
-          : continuation.exit(value)),
-    },
-  ],
-});
-
-// Resolves a reference by using the provided find function to match it
-// within the provided thingData dependency. This will early exit if the
-// data dependency is null, or, if notFoundMode is set to 'exit', if the find
-// function doesn't match anything for the reference. Otherwise, the data
-// object is provided on the output dependency; or null, if the reference
-// doesn't match anything or itself was null to begin with.
-export const withResolvedReference = templateCompositeFrom({
-  annotation: `withResolvedReference`,
-
-  inputs: {
-    ref: input({type: 'string', acceptsNull: true}),
-
-    data: inputWikiData({allowMixedTypes: false}),
-    find: input({type: 'function'}),
-
-    notFoundMode: input({
-      validate: is('null', 'exit'),
-      defaultValue: 'null',
-    }),
-  },
-
-  outputs: ['#resolvedReference'],
-
-  steps: () => [
-    raiseOutputWithoutDependency({
-      dependency: input('ref'),
-      output: input.value({
-        ['#resolvedReference']: null,
-      }),
-    }),
-
-    exitWithoutDependency({
-      dependency: input('data'),
-    }),
-
-    {
-      dependencies: [
-        input('ref'),
-        input('data'),
-        input('find'),
-        input('notFoundMode'),
-      ],
-
-      compute(continuation, {
-        [input('ref')]: ref,
-        [input('data')]: data,
-        [input('find')]: findFunction,
-        [input('notFoundMode')]: notFoundMode,
-      }) {
-        const match = findFunction(ref, data, {mode: 'quiet'});
-
-        if (match === null && notFoundMode === 'exit') {
-          return continuation.exit(null);
-        }
-
-        return continuation.raiseOutput({
-          ['#resolvedReference']: match ?? null,
-        });
-      },
-    },
-  ],
-});
-
-// Resolves a list of references, with each reference matched with provided
-// data in the same way as withResolvedReference. This will early exit if the
-// data dependency is null (even if the reference list is empty). By default
-// it will filter out references which don't match, but this can be changed
-// to early exit ({notFoundMode: 'exit'}) or leave null in place ('null').
-export const withResolvedReferenceList = templateCompositeFrom({
-  annotation: `withResolvedReferenceList`,
-
-  inputs: {
-    list: input({
-      validate: validateArrayItems(isString),
-      acceptsNull: true,
-    }),
-
-    data: inputWikiData({allowMixedTypes: false}),
-    find: input({type: 'function'}),
-
-    notFoundMode: input({
-      validate: is('exit', 'filter', 'null'),
-      defaultValue: 'filter',
-    }),
-  },
-
-  outputs: ['#resolvedReferenceList'],
-
-  steps: () => [
-    exitWithoutDependency({
-      dependency: input('data'),
-      value: input.value([]),
-    }),
-
-    raiseOutputWithoutDependency({
-      dependency: input('list'),
-      mode: input.value('empty'),
-      output: input.value({
-        ['#resolvedReferenceList']: [],
-      }),
-    }),
-
-    {
-      dependencies: [input('list'), input('data'), input('find')],
-      compute: (continuation, {
-        [input('list')]: list,
-        [input('data')]: data,
-        [input('find')]: findFunction,
-      }) =>
-        continuation({
-          '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})),
-        }),
-    },
-
-    {
-      dependencies: ['#matches'],
-      compute: (continuation, {'#matches': matches}) =>
-        (matches.every(match => match)
-          ? continuation.raiseOutput({
-              ['#resolvedReferenceList']: matches,
-            })
-          : continuation()),
-    },
-
-    {
-      dependencies: ['#matches', input('notFoundMode')],
-      compute(continuation, {
-        ['#matches']: matches,
-        [input('notFoundMode')]: notFoundMode,
-      }) {
-        switch (notFoundMode) {
-          case 'exit':
-            return continuation.exit([]);
-
-          case 'filter':
-            return continuation.raiseOutput({
-              ['#resolvedReferenceList']:
-                matches.filter(match => match),
-            });
-
-          case 'null':
-            return continuation.raiseOutput({
-              ['#resolvedReferenceList']:
-                matches.map(match => match ?? null),
-            });
-
-          default:
-            throw new TypeError(`Expected notFoundMode to be exit, filter, or null`);
-        }
-      },
-    },
-  ],
-});
-
-// Check out the info on reverseReferenceList!
-// This is its composable form.
-export const withReverseReferenceList = templateCompositeFrom({
-  annotation: `withReverseReferenceList`,
-
-  inputs: {
-    data: inputWikiData({allowMixedTypes: false}),
-    list: input({type: 'string'}),
-  },
-
-  outputs: ['#reverseReferenceList'],
-
-  steps: () => [
-    exitWithoutDependency({
-      dependency: input('data'),
-      value: input.value([]),
-    }),
-
-    {
-      dependencies: [input.myself(), input('data'), input('list')],
-
-      compute: (continuation, {
-        [input.myself()]: thisThing,
-        [input('data')]: data,
-        [input('list')]: refListProperty,
-      }) =>
-        continuation({
-          ['#reverseReferenceList']:
-            data.filter(thing => thing[refListProperty].includes(thisThing)),
-        }),
-    },
-  ],
-});
diff --git a/src/data/things/track.js b/src/data/things/track.js
index c77bf889..193ad891 100644
--- a/src/data/things/track.js
+++ b/src/data/things/track.js
@@ -1,35 +1,28 @@
 import {inspect} from 'node:util';
 
 import {colors} from '#cli';
+import {input} from '#composite';
 import find from '#find';
-import {empty} from '#sugar';
 
 import {
-  exitWithoutDependency,
-  excludeFromList,
-  exposeConstant,
-  exposeDependency,
-  exposeDependencyOrContinue,
-  exposeUpdateValueOrContinue,
-  input,
-  raiseOutputWithoutDependency,
-  templateCompositeFrom,
-  withPropertyFromObject,
-} from '#composite';
-
-import {
-  is,
-  isBoolean,
   isColor,
   isContributionList,
   isDate,
   isFileExtension,
-  validateWikiData,
 } from '#validators';
 
-import CacheableObject from './cacheable-object.js';
+import {withPropertyFromObject} from '#composite/data';
+import {withResolvedContribs} from '#composite/wiki-data';
+
+import {
+  exitWithoutDependency,
+  exposeConstant,
+  exposeDependency,
+  exposeDependencyOrContinue,
+  exposeUpdateValueOrContinue,
+} from '#composite/control-flow';
 
-import Thing, {
+import {
   additionalFiles,
   commentary,
   commentatorArtists,
@@ -45,10 +38,22 @@ import Thing, {
   simpleString,
   urls,
   wikiData,
-  withResolvedContribs,
-  withResolvedReference,
-  withReverseReferenceList,
-} from './thing.js';
+} from '#composite/wiki-properties';
+
+import {
+  exitWithoutUniqueCoverArt,
+  inheritFromOriginalRelease,
+  trackReverseReferenceList,
+  withAlbum,
+  withAlwaysReferenceByDirectory,
+  withContainingTrackSection,
+  withHasUniqueCoverArt,
+  withOtherReleases,
+  withPropertyFromAlbum,
+} from '#composite/things/track';
+
+import CacheableObject from './cacheable-object.js';
+import Thing from './thing.js';
 
 export class Track extends Thing {
   static [Thing.referenceType] = 'track';
@@ -84,39 +89,9 @@ export class Track extends Thing {
       exposeDependency({dependency: '#album.color'}),
     ],
 
-    // Controls how find.track works - it'll never be matched by a reference
-    // just to the track's name, which means you don't have to always reference
-    // some *other* (much more commonly referenced) track by directory instead
-    // of more naturally by name.
     alwaysReferenceByDirectory: [
-      exposeUpdateValueOrContinue({
-        validate: input.value(isBoolean),
-      }),
-
-      excludeFromList({
-        list: 'trackData',
-        item: input.myself(),
-      }),
-
-      withOriginalRelease({
-        data: '#trackData',
-      }),
-
-      exitWithoutDependency({
-        dependency: '#originalRelease',
-        value: input.value(false),
-      }),
-
-      withPropertyFromObject({
-        object: '#originalRelease',
-        property: input.value('name'),
-      }),
-
-      {
-        dependencies: ['name', '#originalRelease.name'],
-        compute: ({name, '#originalRelease.name': originalName}) =>
-          name === originalName,
-      },
+      withAlwaysReferenceByDirectory(),
+      exposeDependency({dependency: '#alwaysReferenceByDirectory'}),
     ],
 
     // Disables presenting the track as though it has its own unique artwork.
@@ -298,61 +273,20 @@ export class Track extends Thing {
       exposeDependency({dependency: '#album.date'}),
     ],
 
-    // Whether or not the track has "unique" cover artwork - a cover which is
-    // specifically associated with this track in particular, rather than with
-    // the track's album as a whole. This is typically used to select between
-    // displaying the track artwork and a fallback, such as the album artwork
-    // or a placeholder. (This property is named hasUniqueCoverArt instead of
-    // the usual hasCoverArt to emphasize that it does not inherit from the
-    // album.)
     hasUniqueCoverArt: [
       withHasUniqueCoverArt(),
       exposeDependency({dependency: '#hasUniqueCoverArt'}),
     ],
 
     otherReleases: [
-      exitWithoutDependency({
-        dependency: 'trackData',
-        mode: input.value('empty'),
-      }),
-
-      withOriginalRelease({
-        selfIfOriginal: input.value(true),
-      }),
-
-      {
-        flags: {expose: true},
-        expose: {
-          dependencies: [input.myself(), '#originalRelease', 'trackData'],
-          compute: ({
-            [input.myself()]: thisTrack,
-            ['#originalRelease']: originalRelease,
-            trackData,
-          }) =>
-            (originalRelease === thisTrack
-              ? []
-              : [originalRelease])
-              .concat(trackData.filter(track =>
-                track !== originalRelease &&
-                track !== thisTrack &&
-                track.originalReleaseTrack === originalRelease)),
-        },
-      },
+      withOtherReleases(),
+      exposeDependency({dependency: '#otherReleases'}),
     ],
 
-    // Specifically exclude re-releases from this list - while it's useful to
-    // get from a re-release to the tracks it references, re-releases aren't
-    // generally relevant from the perspective of the tracks being referenced.
-    // Filtering them from data here hides them from the corresponding field
-    // on the site (obviously), and has the bonus of not counting them when
-    // counting the number of times a track has been referenced, for use in
-    // the "Tracks - by Times Referenced" listing page (or other data
-    // processing).
     referencedByTracks: trackReverseReferenceList({
       list: input.value('referencedTracks'),
     }),
 
-    // For the same reasoning, exclude re-releases from sampled tracks too.
     sampledByTracks: trackReverseReferenceList({
       list: input.value('sampledTracks'),
     }),
@@ -386,344 +320,3 @@ export class Track extends Thing {
     return parts.join('');
   }
 }
-
-// Early exits with a value inherited from the original release, if
-// this track is a rerelease, and otherwise continues with no further
-// dependencies provided. If allowOverride is true, then the continuation
-// will also be called if the original release exposed the requested
-// property as null.
-export const inheritFromOriginalRelease = templateCompositeFrom({
-  annotation: `Track.inheritFromOriginalRelease`,
-
-  inputs: {
-    property: input({type: 'string'}),
-    allowOverride: input({type: 'boolean', defaultValue: false}),
-  },
-
-  steps: () => [
-    withOriginalRelease(),
-
-    {
-      dependencies: [
-        '#originalRelease',
-        input('property'),
-        input('allowOverride'),
-      ],
-
-      compute: (continuation, {
-        ['#originalRelease']: originalRelease,
-        [input('property')]: originalProperty,
-        [input('allowOverride')]: allowOverride,
-      }) => {
-        if (!originalRelease) return continuation();
-
-        const value = originalRelease[originalProperty];
-        if (allowOverride && value === null) return continuation();
-
-        return continuation.exit(value);
-      },
-    },
-  ],
-});
-
-// Gets the track's album. This will early exit if albumData is missing.
-// By default, if there's no album whose list of tracks includes this track,
-// the output dependency will be null; set {notFoundMode: 'exit'} to early
-// exit instead.
-export const withAlbum = templateCompositeFrom({
-  annotation: `Track.withAlbum`,
-
-  inputs: {
-    notFoundMode: input({
-      validate: is('exit', 'null'),
-      defaultValue: 'null',
-    }),
-  },
-
-  outputs: ['#album'],
-
-  steps: () => [
-    raiseOutputWithoutDependency({
-      dependency: 'albumData',
-      mode: input.value('empty'),
-      output: input.value({
-        ['#album']: null,
-      }),
-    }),
-
-    {
-      dependencies: [input.myself(), 'albumData'],
-      compute: (continuation, {
-        [input.myself()]: track,
-        ['albumData']: albumData,
-      }) =>
-        continuation({
-          ['#album']:
-            albumData.find(album => album.tracks.includes(track)),
-        }),
-    },
-
-    raiseOutputWithoutDependency({
-      dependency: '#album',
-      output: input.value({
-        ['#album']: null,
-      }),
-    }),
-
-    {
-      dependencies: ['#album'],
-      compute: (continuation, {'#album': album}) =>
-        continuation.raiseOutput({'#album': album}),
-    },
-  ],
-});
-
-// Gets a single property from this track's album, providing it as the same
-// property name prefixed with '#album.' (by default). If the track's album
-// isn't available, then by default, the property will be provided as null;
-// set {notFoundMode: 'exit'} to early exit instead.
-export const withPropertyFromAlbum = templateCompositeFrom({
-  annotation: `withPropertyFromAlbum`,
-
-  inputs: {
-    property: input.staticValue({type: 'string'}),
-
-    notFoundMode: input({
-      validate: is('exit', 'null'),
-      defaultValue: 'null',
-    }),
-  },
-
-  outputs: ({
-    [input.staticValue('property')]: property,
-  }) => ['#album.' + property],
-
-  steps: () => [
-    withAlbum({
-      notFoundMode: input('notFoundMode'),
-    }),
-
-    withPropertyFromObject({
-      object: '#album',
-      property: input('property'),
-    }),
-
-    {
-      dependencies: ['#value', input.staticValue('property')],
-      compute: (continuation, {
-        ['#value']: value,
-        [input.staticValue('property')]: property,
-      }) => continuation({
-        ['#album.' + property]: value,
-      }),
-    },
-  ],
-});
-
-// Gets the track section containing this track from its album's track list.
-// If notFoundMode is set to 'exit', this will early exit if the album can't be
-// found or if none of its trackSections includes the track for some reason.
-export const withContainingTrackSection = templateCompositeFrom({
-  annotation: `withContainingTrackSection`,
-
-  inputs: {
-    notFoundMode: input({
-      validate: is('exit', 'null'),
-      defaultValue: 'null',
-    }),
-  },
-
-  outputs: ['#trackSection'],
-
-  steps: () => [
-    withPropertyFromAlbum({
-      property: input.value('trackSections'),
-      notFoundMode: input('notFoundMode'),
-    }),
-
-    {
-      dependencies: [
-        input.myself(),
-        input('notFoundMode'),
-        '#album.trackSections',
-      ],
-
-      compute(continuation, {
-        [input.myself()]: track,
-        [input('notFoundMode')]: notFoundMode,
-        ['#album.trackSections']: trackSections,
-      }) {
-        if (!trackSections) {
-          return continuation.raiseOutput({
-            ['#trackSection']: null,
-          });
-        }
-
-        const trackSection =
-          trackSections.find(({tracks}) => tracks.includes(track));
-
-        if (trackSection) {
-          return continuation.raiseOutput({
-            ['#trackSection']: trackSection,
-          });
-        } else if (notFoundMode === 'exit') {
-          return continuation.exit(null);
-        } else {
-          return continuation.raiseOutput({
-            ['#trackSection']: null,
-          });
-        }
-      },
-    },
-  ],
-});
-
-// Just includes the original release of this track as a dependency.
-// If this track isn't a rerelease, then it'll provide null, unless the
-// {selfIfOriginal} option is set, in which case it'll provide this track
-// itself. Note that this will early exit if the original release is
-// specified by reference and that reference doesn't resolve to anything.
-// Outputs to '#originalRelease' by default.
-export const withOriginalRelease = templateCompositeFrom({
-  annotation: `withOriginalRelease`,
-
-  inputs: {
-    selfIfOriginal: input({type: 'boolean', defaultValue: false}),
-
-    data: input({
-      validate: validateWikiData({referenceType: 'track'}),
-      defaultDependency: 'trackData',
-    }),
-  },
-
-  outputs: ['#originalRelease'],
-
-  steps: () => [
-    withResolvedReference({
-      ref: 'originalReleaseTrack',
-      data: input('data'),
-      find: input.value(find.track),
-      notFoundMode: input.value('exit'),
-    }).outputs({
-      ['#resolvedReference']: '#originalRelease',
-    }),
-
-    {
-      dependencies: [
-        input.myself(),
-        input('selfIfOriginal'),
-        '#originalRelease',
-      ],
-
-      compute: (continuation, {
-        [input.myself()]: track,
-        [input('selfIfOriginal')]: selfIfOriginal,
-        ['#originalRelease']: originalRelease,
-      }) =>
-        continuation({
-          ['#originalRelease']:
-            (originalRelease ??
-              (selfIfOriginal
-                ? track
-                : null)),
-        }),
-    },
-  ],
-});
-
-// The algorithm for checking if a track has unique cover art is used in a
-// couple places, so it's defined in full as a compositional step.
-export const withHasUniqueCoverArt = templateCompositeFrom({
-  annotation: 'withHasUniqueCoverArt',
-
-  outputs: ['#hasUniqueCoverArt'],
-
-  steps: () => [
-    {
-      dependencies: ['disableUniqueCoverArt'],
-      compute: (continuation, {disableUniqueCoverArt}) =>
-        (disableUniqueCoverArt
-          ? continuation.raiseOutput({
-              ['#hasUniqueCoverArt']: false,
-            })
-          : continuation()),
-    },
-
-    withResolvedContribs({from: 'coverArtistContribs'}),
-
-    {
-      dependencies: ['#resolvedContribs'],
-      compute: (continuation, {
-        ['#resolvedContribs']: contribsFromTrack,
-      }) =>
-        (empty(contribsFromTrack)
-          ? continuation()
-          : continuation.raiseOutput({
-              ['#hasUniqueCoverArt']: true,
-            })),
-    },
-
-    withPropertyFromAlbum({
-      property: input.value('trackCoverArtistContribs'),
-    }),
-
-    {
-      dependencies: ['#album.trackCoverArtistContribs'],
-      compute: (continuation, {
-        ['#album.trackCoverArtistContribs']: contribsFromAlbum,
-      }) =>
-        continuation.raiseOutput({
-          ['#hasUniqueCoverArt']:
-            !empty(contribsFromAlbum),
-        }),
-    },
-  ],
-});
-
-// Shorthand for checking if the track has unique cover art and exposing a
-// fallback value if it isn't.
-export const exitWithoutUniqueCoverArt = templateCompositeFrom({
-  annotation: `exitWithoutUniqueCoverArt`,
-
-  inputs: {
-    value: input({defaultValue: null}),
-  },
-
-  steps: () => [
-    withHasUniqueCoverArt(),
-
-    exitWithoutDependency({
-      dependency: '#hasUniqueCoverArt',
-      mode: input.value('falsy'),
-      value: input('value'),
-    }),
-  ],
-});
-
-export const trackReverseReferenceList = 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/things/wiki-info.js b/src/data/things/wiki-info.js
index c764b528..0460f272 100644
--- a/src/data/things/wiki-info.js
+++ b/src/data/things/wiki-info.js
@@ -2,14 +2,16 @@ import {input} from '#composite';
 import find from '#find';
 import {isLanguageCode, isName, isURL} from '#validators';
 
-import Thing, {
+import {
   color,
   flag,
   name,
   referenceList,
   simpleString,
   wikiData,
-} from './thing.js';
+} from '#composite/wiki-properties';
+
+import Thing from './thing.js';
 
 export class WikiInfo extends Thing {
   static [Thing.getPropertyDescriptors] = ({Group}) => ({