« get me outta code hell

real pragma, and some eslint fixes - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/data
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2022-06-26 18:02:27 -0300
committer(quasar) nebula <qznebula@protonmail.com>2022-06-26 18:02:27 -0300
commitc75b029160248b6935e5c0f5156cc7a870311e82 (patch)
tree693c5cca195e50b048b0086e768aa06a7c1986ee /src/data
parentf65e712fe8b8b1a196da2db286ebc6a5c9bf7433 (diff)
real pragma, and some eslint fixes
Diffstat (limited to 'src/data')
-rw-r--r--src/data/cacheable-object.js33
-rw-r--r--src/data/patches.js45
-rw-r--r--src/data/serialize.js6
-rw-r--r--src/data/things.js691
-rw-r--r--src/data/validators.js42
-rw-r--r--src/data/yaml.js595
6 files changed, 702 insertions, 710 deletions
diff --git a/src/data/cacheable-object.js b/src/data/cacheable-object.js
index 47281939..fe1817f6 100644
--- a/src/data/cacheable-object.js
+++ b/src/data/cacheable-object.js
@@ -1,5 +1,7 @@
-// @format
-//
+/**
+ * @format
+ */
+
 // Generally extendable class for caching properties and handling dependencies,
 // with a few key properties:
 //
@@ -76,16 +78,16 @@
 //      function, which provides a mapping of exposed property names to whether
 //      or not their dependencies are yet met.
 
-import { color, ENABLE_COLOR } from "../util/cli.js";
+import {color, ENABLE_COLOR} from '../util/cli.js';
 
-import { inspect as nodeInspect } from "util";
+import {inspect as nodeInspect} from 'util';
 
 function inspect(value) {
-  return nodeInspect(value, { colors: ENABLE_COLOR });
+  return nodeInspect(value, {colors: ENABLE_COLOR});
 }
 
 export default class CacheableObject {
-  static instance = Symbol("CacheableObject `this` instance");
+  static instance = Symbol('CacheableObject `this` instance');
 
   #propertyUpdateValues = Object.create(null);
   #propertyUpdateCacheInvalidators = Object.create(null);
@@ -109,7 +111,7 @@ export default class CacheableObject {
       return new Proxy(this, {
         get: (obj, key) => {
           if (!Object.hasOwn(obj, key)) {
-            if (key !== "constructor") {
+            if (key !== 'constructor') {
               CacheableObject._invalidAccesses.add(
                 `(${obj.constructor.name}).${key}`
               );
@@ -125,7 +127,7 @@ export default class CacheableObject {
     for (const [property, descriptor] of Object.entries(
       this.constructor.propertyDescriptors
     )) {
-      const { flags, update } = descriptor;
+      const {flags, update} = descriptor;
 
       if (!flags.update) {
         continue;
@@ -149,7 +151,7 @@ export default class CacheableObject {
     for (const [property, descriptor] of Object.entries(
       this.constructor.propertyDescriptors
     )) {
-      const { flags } = descriptor;
+      const {flags} = descriptor;
 
       const definition = {
         configurable: false,
@@ -173,9 +175,8 @@ export default class CacheableObject {
   }
 
   #getUpdateObjectDefinitionSetterFunction(property) {
-    const { update } = this.#getPropertyDescriptor(property);
+    const {update} = this.#getPropertyDescriptor(property);
     const validate = update?.validate;
-    const allowNull = update?.allowNull;
 
     return (newValue) => {
       const oldValue = this.#propertyUpdateValues[property];
@@ -209,10 +210,6 @@ export default class CacheableObject {
     };
   }
 
-  #getUpdatePropertyValidateFunction(property) {
-    const descriptor = this.#getPropertyDescriptor(property);
-  }
-
   #getPropertyDescriptor(property) {
     return this.constructor.propertyDescriptors[property];
   }
@@ -225,7 +222,7 @@ export default class CacheableObject {
   }
 
   #getExposeObjectDefinitionGetterFunction(property) {
-    const { flags } = this.#getPropertyDescriptor(property);
+    const {flags} = this.#getPropertyDescriptor(property);
     const compute = this.#getExposeComputeFunction(property);
 
     if (compute) {
@@ -248,7 +245,7 @@ export default class CacheableObject {
   }
 
   #getExposeComputeFunction(property) {
-    const { flags, expose } = this.#getPropertyDescriptor(property);
+    const {flags, expose} = this.#getPropertyDescriptor(property);
 
     const compute = expose?.compute;
     const transform = expose?.transform;
@@ -286,7 +283,7 @@ export default class CacheableObject {
   }
 
   #getExposeCheckCacheValidFunction(property) {
-    const { flags, expose } = this.#getPropertyDescriptor(property);
+    const {flags, expose} = this.#getPropertyDescriptor(property);
 
     let valid = false;
 
diff --git a/src/data/patches.js b/src/data/patches.js
index 937fb099..dc757fa9 100644
--- a/src/data/patches.js
+++ b/src/data/patches.js
@@ -1,5 +1,5 @@
-// @format
-//
+/** @format */
+
 // --> Patch
 
 export class Patch {
@@ -76,7 +76,7 @@ export class Patch {
               inputs[inputName] = [Patch.INPUT_AVAILABLE, output[1]];
               break;
           }
-          throw new Error("Unreachable");
+          throw new Error('Unreachable');
         }
 
         case Patch.INPUT_MANAGED_CONNECTION: {
@@ -169,7 +169,7 @@ export class PatchManager extends Patch {
       return false;
     }
 
-    for (const inputNames of patch.inputNames) {
+    for (const inputName of patch.inputNames) {
       const input = patch.inputs[inputName];
       if (input[0] === Patch.INPUT_MANAGED_CONNECTION) {
         this.dropManagedInput(input[1]);
@@ -202,7 +202,7 @@ export class PatchManager extends Patch {
   }
 
   dropManagedInput(identifier) {
-    return delete this.managedInputs[key];
+    return delete this.managedInputs[identifier];
   }
 
   getManagedInput(identifier) {
@@ -213,7 +213,7 @@ export class PatchManager extends Patch {
     return this.computeManagedInput(patch, outputName, memory);
   }
 
-  computeManagedInput(patch, outputName, memory) {
+  computeManagedInput(patch, outputName) {
     // Override this function in subclasses to alter behavior of the "wire"
     // used for connecting patches.
 
@@ -255,7 +255,7 @@ export class PatchManager extends Patch {
 }
 
 class PatchManagerExternalInputPatch extends Patch {
-  constructor({ manager, ...rest }) {
+  constructor({manager, ...rest}) {
     super({
       manager,
       inputNames: manager.inputNames,
@@ -284,7 +284,7 @@ class PatchManagerExternalInputPatch extends Patch {
 }
 
 class PatchManagerExternalOutputPatch extends Patch {
-  constructor({ manager, ...rest }) {
+  constructor({manager, ...rest}) {
     super({
       manager,
       inputNames: manager.outputNames,
@@ -312,7 +312,6 @@ class PatchManagerExternalOutputPatch extends Patch {
 
 const caches = Symbol();
 const common = Symbol();
-const hsmusic = Symbol();
 
 Patch[caches] = {
   WireCachedPatchManager: class extends PatchManager {
@@ -327,8 +326,8 @@ Patch[caches] = {
     computeManagedInput(patch, outputName, memory) {
       let cache = true;
 
-      const { previousInputs } = memory;
-      const { inputs } = patch;
+      const {previousInputs} = memory;
+      const {inputs} = patch;
       if (memory.previousInputs) {
         for (const inputName of patch.inputNames) {
           // TODO: This doesn't account for connections whose values
@@ -348,7 +347,7 @@ Patch[caches] = {
 
       const outputs = patch.computeOutputs();
       memory.previousOutputs = outputs;
-      memory.previousInputs = { ...inputs };
+      memory.previousInputs = {...inputs};
       return outputs[outputName];
     }
   },
@@ -356,8 +355,8 @@ Patch[caches] = {
 
 Patch[common] = {
   Stringify: class extends Patch {
-    static inputNames = ["value"];
-    static outputNames = ["value"];
+    static inputNames = ['value'];
+    static outputNames = ['value'];
 
     compute(inputs, outputs) {
       if (inputs.value[0] === Patch.INPUT_AVAILABLE) {
@@ -369,8 +368,8 @@ Patch[common] = {
   },
 
   Echo: class extends Patch {
-    static inputNames = ["value"];
-    static outputNames = ["value"];
+    static inputNames = ['value'];
+    static outputNames = ['value'];
 
     compute(inputs, outputs) {
       if (inputs.value[0] === Patch.INPUT_AVAILABLE) {
@@ -383,16 +382,16 @@ Patch[common] = {
 };
 
 const PM = new Patch[caches].WireCachedPatchManager({
-  inputNames: ["externalInput"],
-  outputNames: ["externalOutput"],
+  inputNames: ['externalInput'],
+  outputNames: ['externalOutput'],
 });
 
-const P1 = new Patch[common].Stringify({ manager: PM });
-const P2 = new Patch[common].Echo({ manager: PM });
+const P1 = new Patch[common].Stringify({manager: PM});
+const P2 = new Patch[common].Echo({manager: PM});
 
-PM.addExternalInput(P1, "value", "externalInput");
-PM.addManagedInput(P2, "value", P1, "value");
-PM.setExternalOutput("externalOutput", P2, "value");
+PM.addExternalInput(P1, 'value', 'externalInput');
+PM.addManagedInput(P2, 'value', P1, 'value');
+PM.setExternalOutput('externalOutput', P2, 'value');
 
 PM.inputs.externalInput = [Patch.INPUT_CONSTANT, 123];
 console.log(PM.computeOutputs());
diff --git a/src/data/serialize.js b/src/data/serialize.js
index 13b20e13..a4206fd0 100644
--- a/src/data/serialize.js
+++ b/src/data/serialize.js
@@ -1,5 +1,5 @@
-// @format
-//
+/** @format */
+
 // serialize-util.js: simple interface and utility functions for converting
 // Things into a directly serializeable format
 
@@ -18,7 +18,7 @@ export function toRefs(things) {
 }
 
 export function toContribRefs(contribs) {
-  return contribs?.map(({ who, what }) => ({ who: toRef(who), what }));
+  return contribs?.map(({who, what}) => ({who: toRef(who), what}));
 }
 
 // Interface
diff --git a/src/data/things.js b/src/data/things.js
index c86add30..e2bbfd70 100644
--- a/src/data/things.js
+++ b/src/data/things.js
@@ -1,9 +1,9 @@
-// @format
-//
+/** @format */
+
 // things.js: class definitions for various object types used across the wiki,
 // most of which correspond to an output page, such as Track, Album, Artist
 
-import CacheableObject from "./cacheable-object.js";
+import CacheableObject from './cacheable-object.js';
 
 import {
   isAdditionalFileList,
@@ -16,32 +16,30 @@ import {
   isDimensions,
   isDirectory,
   isDuration,
-  isInstance,
   isFileExtension,
   isLanguageCode,
   isName,
   isNumber,
   isURL,
   isString,
-  isWholeNumber,
   oneOf,
   validateArrayItems,
   validateInstanceOf,
   validateReference,
   validateReferenceList,
-} from "./validators.js";
+} from './validators.js';
 
-import * as S from "./serialize.js";
+import * as S from './serialize.js';
 
 import {
   getKebabCase,
   sortAlbumsTracksChronologically,
-} from "../util/wiki-data.js";
+} from '../util/wiki-data.js';
 
-import find from "../util/find.js";
+import find from '../util/find.js';
 
-import { inspect } from "util";
-import { color } from "../util/cli.js";
+import {inspect} from 'util';
+import {color} from '../util/cli.js';
 
 // Stub classes (and their exports) at the top of the file - these are
 // referenced later when we actually define static class fields. We deliberately
@@ -96,16 +94,16 @@ export class Language extends CacheableObject {}
 // Before initializing property descriptors, set additional independent
 // constants on the classes (which are referenced later).
 
-Thing.referenceType = Symbol("Thing.referenceType");
+Thing.referenceType = Symbol('Thing.referenceType');
 
-Album[Thing.referenceType] = "album";
-Track[Thing.referenceType] = "track";
-Artist[Thing.referenceType] = "artist";
-Group[Thing.referenceType] = "group";
-ArtTag[Thing.referenceType] = "tag";
-NewsEntry[Thing.referenceType] = "news-entry";
-StaticPage[Thing.referenceType] = "static";
-Flash[Thing.referenceType] = "flash";
+Album[Thing.referenceType] = 'album';
+Track[Thing.referenceType] = 'track';
+Artist[Thing.referenceType] = 'artist';
+Group[Thing.referenceType] = 'group';
+ArtTag[Thing.referenceType] = 'tag';
+NewsEntry[Thing.referenceType] = 'news-entry';
+StaticPage[Thing.referenceType] = 'static';
+Flash[Thing.referenceType] = 'flash';
 
 // -> Thing: base class for wiki data types, providing wiki-specific utility
 // functions on top of essential CacheableObject behavior.
@@ -115,21 +113,21 @@ Flash[Thing.referenceType] = "flash";
 // functions, so check each for how its own arguments behave!
 Thing.common = {
   name: (defaultName) => ({
-    flags: { update: true, expose: true },
-    update: { validate: isName, default: defaultName },
+    flags: {update: true, expose: true},
+    update: {validate: isName, default: defaultName},
   }),
 
   color: () => ({
-    flags: { update: true, expose: true },
-    update: { validate: isColor },
+    flags: {update: true, expose: true},
+    update: {validate: isColor},
   }),
 
   directory: () => ({
-    flags: { update: true, expose: true },
-    update: { validate: isDirectory },
+    flags: {update: true, expose: true},
+    update: {validate: isDirectory},
     expose: {
-      dependencies: ["name"],
-      transform(directory, { name }) {
+      dependencies: ['name'],
+      transform(directory, {name}) {
         if (directory === null && name === null) return null;
         else if (directory === null) return getKebabCase(name);
         else return directory;
@@ -138,27 +136,27 @@ Thing.common = {
   }),
 
   urls: () => ({
-    flags: { update: true, expose: true },
-    update: { validate: validateArrayItems(isURL) },
+    flags: {update: true, expose: true},
+    update: {validate: validateArrayItems(isURL)},
   }),
 
   // A file extension! Or the default, if provided when calling this.
   fileExtension: (defaultFileExtension = null) => ({
-    flags: { update: true, expose: true },
-    update: { validate: isFileExtension },
-    expose: { transform: (value) => value ?? defaultFileExtension },
+    flags: {update: true, expose: true},
+    update: {validate: isFileExtension},
+    expose: {transform: (value) => value ?? defaultFileExtension},
   }),
 
   // Straightforward flag descriptor for a variety of property purposes.
   // Provide a default value, true or false!
   flag: (defaultValue = false) => {
-    if (typeof defaultValue !== "boolean") {
+    if (typeof defaultValue !== 'boolean') {
       throw new TypeError(`Always set explicit defaults for flags!`);
     }
 
     return {
-      flags: { update: true, expose: true },
-      update: { validate: isBoolean, default: defaultValue },
+      flags: {update: true, expose: true},
+      update: {validate: isBoolean, default: defaultValue},
     };
   },
 
@@ -166,23 +164,23 @@ Thing.common = {
   // This isn't dynamic though - it won't inherit from a date stored on
   // another object, for example.
   simpleDate: () => ({
-    flags: { update: true, expose: true },
-    update: { validate: isDate },
+    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.
   simpleString: () => ({
-    flags: { update: true, expose: true },
-    update: { validate: isString },
+    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.
   externalFunction: () => ({
-    flags: { update: true },
-    update: { validate: (t) => typeof t === "function" },
+    flags: {update: true},
+    update: {validate: (t) => typeof t === 'function'},
   }),
 
   // Super simple "contributions by reference" list, used for a variety of
@@ -197,14 +195,14 @@ Thing.common = {
   //
   // ...processed from YAML, spreadsheet, or any other kind of input.
   contribsByRef: () => ({
-    flags: { update: true, expose: true },
-    update: { validate: isContributionList },
+    flags: {update: true, expose: true},
+    update: {validate: isContributionList},
   }),
 
   // Artist commentary! Generally present on tracks and albums.
   commentary: () => ({
-    flags: { update: true, expose: true },
-    update: { validate: isCommentary },
+    flags: {update: true, expose: true},
+    update: {validate: isCommentary},
   }),
 
   // This is a somewhat more involved data structure - it's for additional
@@ -223,8 +221,8 @@ Thing.common = {
   //     ]
   //
   additionalFiles: () => ({
-    flags: { update: true, expose: true },
-    update: { validate: isAdditionalFileList },
+    flags: {update: true, expose: true},
+    update: {validate: isAdditionalFileList},
   }),
 
   // A reference list! Keep in mind this is for general references to wiki
@@ -236,7 +234,7 @@ Thing.common = {
   // string in multiple places by referencing the value saved on the class
   // instead.
   referenceList: (thingClass) => {
-    const { [Thing.referenceType]: referenceType } = thingClass;
+    const {[Thing.referenceType]: referenceType} = thingClass;
     if (!referenceType) {
       throw new Error(
         `The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`
@@ -244,14 +242,14 @@ Thing.common = {
     }
 
     return {
-      flags: { update: true, expose: true },
-      update: { validate: validateReferenceList(referenceType) },
+      flags: {update: true, expose: true},
+      update: {validate: validateReferenceList(referenceType)},
     };
   },
 
   // Corresponding function for a single reference.
   singleReference: (thingClass) => {
-    const { [Thing.referenceType]: referenceType } = thingClass;
+    const {[Thing.referenceType]: referenceType} = thingClass;
     if (!referenceType) {
       throw new Error(
         `The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`
@@ -259,8 +257,8 @@ Thing.common = {
     }
 
     return {
-      flags: { update: true, expose: true },
-      update: { validate: validateReference(referenceType) },
+      flags: {update: true, expose: true},
+      update: {validate: validateReference(referenceType)},
     };
   },
 
@@ -272,7 +270,7 @@ Thing.common = {
     thingDataProperty,
     findFn
   ) => ({
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
       dependencies: [referenceListProperty, thingDataProperty],
@@ -282,7 +280,7 @@ Thing.common = {
       }) =>
         refs && thingData
           ? refs
-              .map((ref) => findFn(ref, thingData, { mode: "quiet" }))
+              .map((ref) => findFn(ref, thingData, {mode: 'quiet'}))
               .filter(Boolean)
           : [],
     },
@@ -294,15 +292,14 @@ Thing.common = {
     thingDataProperty,
     findFn
   ) => ({
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
       dependencies: [singleReferenceProperty, thingDataProperty],
       compute: ({
         [singleReferenceProperty]: ref,
         [thingDataProperty]: thingData,
-      }) =>
-        ref && thingData ? findFn(ref, thingData, { mode: "quiet" }) : null,
+      }) => (ref && thingData ? findFn(ref, thingData, {mode: 'quiet'}) : null),
     },
   }),
 
@@ -322,17 +319,17 @@ Thing.common = {
   // reference list is somehow messed up, or artistData isn't being provided
   // properly.)
   dynamicContribs: (contribsByRefProperty) => ({
-    flags: { expose: true },
+    flags: {expose: true},
     expose: {
-      dependencies: ["artistData", contribsByRefProperty],
-      compute: ({ artistData, [contribsByRefProperty]: contribsByRef }) =>
+      dependencies: ['artistData', contribsByRefProperty],
+      compute: ({artistData, [contribsByRefProperty]: contribsByRef}) =>
         contribsByRef && artistData
           ? contribsByRef
-              .map(({ who: ref, what }) => ({
+              .map(({who: ref, what}) => ({
                 who: find.artist(ref, artistData),
                 what,
               }))
-              .filter(({ who }) => who)
+              .filter(({who}) => who)
           : [],
     },
   }),
@@ -350,9 +347,9 @@ Thing.common = {
     thingDataProperty,
     findFn
   ) => ({
-    flags: { expose: true },
+    flags: {expose: true},
     expose: {
-      dependencies: [contribsByRefProperty, thingDataProperty, "artistData"],
+      dependencies: [contribsByRefProperty, thingDataProperty, 'artistData'],
       compute({
         [Thing.instance]: thing,
         [contribsByRefProperty]: contribsByRef,
@@ -362,16 +359,16 @@ Thing.common = {
         if (!artistData) return [];
         const refs =
           contribsByRef ??
-          findFn(thing, thingData, { mode: "quiet" })?.[
+          findFn(thing, thingData, {mode: 'quiet'})?.[
             parentContribsByRefProperty
           ];
         if (!refs) return [];
         return refs
-          .map(({ who: ref, what }) => ({
+          .map(({who: ref, what}) => ({
             who: find.artist(ref, artistData),
             what,
           }))
-          .filter(({ who }) => who);
+          .filter(({who}) => who);
       },
     },
   }),
@@ -382,12 +379,12 @@ Thing.common = {
   // property. Naturally, the passed ref list property is of the things in the
   // wiki data provided, not the requesting Thing itself.
   reverseReferenceList: (wikiDataProperty, referencerRefListProperty) => ({
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
       dependencies: [wikiDataProperty],
 
-      compute: ({ [wikiDataProperty]: wikiData, [Thing.instance]: thing }) =>
+      compute: ({[wikiDataProperty]: wikiData, [Thing.instance]: thing}) =>
         wikiData
           ? wikiData.filter((t) =>
               t[referencerRefListProperty]?.includes(thing)
@@ -400,12 +397,12 @@ Thing.common = {
   // is still a list - this is for matching all the objects whose single
   // reference (in the given property) matches this Thing.
   reverseSingleReference: (wikiDataProperty, referencerRefListProperty) => ({
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
       dependencies: [wikiDataProperty],
 
-      compute: ({ [wikiDataProperty]: wikiData, [Thing.instance]: thing }) =>
+      compute: ({[wikiDataProperty]: wikiData, [Thing.instance]: thing}) =>
         wikiData?.filter((t) => t[referencerRefListProperty] === thing),
     },
   }),
@@ -413,7 +410,7 @@ Thing.common = {
   // General purpose wiki data constructor, for properties like artistData,
   // trackData, etc.
   wikiData: (thingClass) => ({
-    flags: { update: true },
+    flags: {update: true},
     update: {
       validate: validateArrayItems(validateInstanceOf(thingClass)),
     },
@@ -423,21 +420,21 @@ Thing.common = {
   // commentary content, and finds the matching artist for each reference.
   // This is mostly useful for credits and listings on artist pages.
   commentatorArtists: () => ({
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
-      dependencies: ["artistData", "commentary"],
+      dependencies: ['artistData', 'commentary'],
 
-      compute: ({ artistData, commentary }) =>
+      compute: ({artistData, commentary}) =>
         artistData && commentary
           ? Array.from(
               new Set(
                 Array.from(
                   commentary
-                    .replace(/<\/?b>/g, "")
+                    .replace(/<\/?b>/g, '')
                     .matchAll(/<i>(?<who>.*?):<\/i>/g)
-                ).map(({ groups: { who } }) =>
-                  find.artist(who, artistData, { mode: "quiet" })
+                ).map(({groups: {who}}) =>
+                  find.artist(who, artistData, {mode: 'quiet'})
                 )
               )
             )
@@ -472,7 +469,7 @@ Thing.prototype[inspect.custom] = function () {
 
   return (
     (this.name ? `${cname} ${color.green(`"${this.name}"`)}` : `${cname}`) +
-    (this.directory ? ` (${color.blue(Thing.getReference(this))})` : "")
+    (this.directory ? ` (${color.blue(Thing.getReference(this))})` : '')
   );
 };
 
@@ -481,7 +478,7 @@ Thing.prototype[inspect.custom] = function () {
 Album.propertyDescriptors = {
   // Update & expose
 
-  name: Thing.common.name("Unnamed Album"),
+  name: Thing.common.name('Unnamed Album'),
   color: Thing.common.color(),
   directory: Thing.common.directory(),
   urls: Thing.common.urls(),
@@ -491,13 +488,13 @@ Album.propertyDescriptors = {
   dateAddedToWiki: Thing.common.simpleDate(),
 
   coverArtDate: {
-    flags: { update: true, expose: true },
+    flags: {update: true, expose: true},
 
-    update: { validate: isDate },
+    update: {validate: isDate},
 
     expose: {
-      dependencies: ["date"],
-      transform: (coverArtDate, { date }) => coverArtDate ?? date ?? null,
+      dependencies: ['date'],
+      transform: (coverArtDate, {date}) => coverArtDate ?? date ?? null,
     },
   },
 
@@ -511,24 +508,24 @@ Album.propertyDescriptors = {
   artTagsByRef: Thing.common.referenceList(ArtTag),
 
   trackGroups: {
-    flags: { update: true, expose: true },
+    flags: {update: true, expose: true},
 
     update: {
       validate: validateArrayItems(validateInstanceOf(TrackGroup)),
     },
   },
 
-  coverArtFileExtension: Thing.common.fileExtension("jpg"),
-  trackCoverArtFileExtension: Thing.common.fileExtension("jpg"),
+  coverArtFileExtension: Thing.common.fileExtension('jpg'),
+  trackCoverArtFileExtension: Thing.common.fileExtension('jpg'),
 
   wallpaperStyle: Thing.common.simpleString(),
-  wallpaperFileExtension: Thing.common.fileExtension("jpg"),
+  wallpaperFileExtension: Thing.common.fileExtension('jpg'),
 
   bannerStyle: Thing.common.simpleString(),
-  bannerFileExtension: Thing.common.fileExtension("jpg"),
+  bannerFileExtension: Thing.common.fileExtension('jpg'),
   bannerDimensions: {
-    flags: { update: true, expose: true },
-    update: { validate: isDimensions },
+    flags: {update: true, expose: true},
+    update: {validate: isDimensions},
   },
 
   hasCoverArt: Thing.common.flag(true),
@@ -549,44 +546,44 @@ Album.propertyDescriptors = {
 
   // Expose only
 
-  artistContribs: Thing.common.dynamicContribs("artistContribsByRef"),
-  coverArtistContribs: Thing.common.dynamicContribs("coverArtistContribsByRef"),
+  artistContribs: Thing.common.dynamicContribs('artistContribsByRef'),
+  coverArtistContribs: Thing.common.dynamicContribs('coverArtistContribsByRef'),
   trackCoverArtistContribs: Thing.common.dynamicContribs(
-    "trackCoverArtistContribsByRef"
+    'trackCoverArtistContribsByRef'
   ),
   wallpaperArtistContribs: Thing.common.dynamicContribs(
-    "wallpaperArtistContribsByRef"
+    'wallpaperArtistContribsByRef'
   ),
   bannerArtistContribs: Thing.common.dynamicContribs(
-    "bannerArtistContribsByRef"
+    'bannerArtistContribsByRef'
   ),
 
   commentatorArtists: Thing.common.commentatorArtists(),
 
   tracks: {
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
-      dependencies: ["trackGroups", "trackData"],
-      compute: ({ trackGroups, trackData }) =>
+      dependencies: ['trackGroups', 'trackData'],
+      compute: ({trackGroups, trackData}) =>
         trackGroups && trackData
           ? trackGroups
               .flatMap((group) => group.tracksByRef ?? [])
-              .map((ref) => find.track(ref, trackData, { mode: "quiet" }))
+              .map((ref) => find.track(ref, trackData, {mode: 'quiet'}))
               .filter(Boolean)
           : [],
     },
   },
 
   groups: Thing.common.dynamicThingsFromReferenceList(
-    "groupsByRef",
-    "groupData",
+    'groupsByRef',
+    'groupData',
     find.group
   ),
 
   artTags: Thing.common.dynamicThingsFromReferenceList(
-    "artTagsByRef",
-    "artTagData",
+    'artTagsByRef',
+    'artTagData',
     find.artTag
   ),
 };
@@ -632,17 +629,17 @@ Album[S.serializeDescriptors] = {
 TrackGroup.propertyDescriptors = {
   // Update & expose
 
-  name: Thing.common.name("Unnamed Track Group"),
+  name: Thing.common.name('Unnamed Track Group'),
 
   color: {
-    flags: { update: true, expose: true },
+    flags: {update: true, expose: true},
 
-    update: { validate: isColor },
+    update: {validate: isColor},
 
     expose: {
-      dependencies: ["album"],
+      dependencies: ['album'],
 
-      transform(color, { album }) {
+      transform(color, {album}) {
         return color ?? album?.color ?? null;
       },
     },
@@ -657,8 +654,8 @@ TrackGroup.propertyDescriptors = {
   // Update only
 
   album: {
-    flags: { update: true },
-    update: { validate: validateInstanceOf(Album) },
+    flags: {update: true},
+    update: {validate: validateInstanceOf(Album)},
   },
 
   trackData: Thing.common.wikiData(Track),
@@ -666,11 +663,11 @@ TrackGroup.propertyDescriptors = {
   // Expose only
 
   tracks: {
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
-      dependencies: ["tracksByRef", "trackData"],
-      compute: ({ tracksByRef, trackData }) =>
+      dependencies: ['tracksByRef', 'trackData'],
+      compute: ({tracksByRef, trackData}) =>
         tracksByRef && trackData
           ? tracksByRef.map((ref) => find.track(ref, trackData)).filter(Boolean)
           : [],
@@ -678,11 +675,11 @@ TrackGroup.propertyDescriptors = {
   },
 
   startIndex: {
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
-      dependencies: ["album"],
-      compute: ({ album, [TrackGroup.instance]: trackGroup }) =>
+      dependencies: ['album'],
+      compute: ({album, [TrackGroup.instance]: trackGroup}) =>
         album.trackGroups
           .slice(0, album.trackGroups.indexOf(trackGroup))
           .reduce((acc, tg) => acc + tg.tracks.length, 0),
@@ -717,12 +714,12 @@ Track.hasCoverArt = (
 Track.propertyDescriptors = {
   // Update & expose
 
-  name: Thing.common.name("Unnamed Track"),
+  name: Thing.common.name('Unnamed Track'),
   directory: Thing.common.directory(),
 
   duration: {
-    flags: { update: true, expose: true },
-    update: { validate: isDuration },
+    flags: {update: true, expose: true},
+    update: {validate: isDuration},
   },
 
   urls: Thing.common.urls(),
@@ -738,15 +735,15 @@ Track.propertyDescriptors = {
   artTagsByRef: Thing.common.referenceList(ArtTag),
 
   hasCoverArt: {
-    flags: { update: true, expose: true },
+    flags: {update: true, expose: true},
 
-    update: { validate: isBoolean },
+    update: {validate: isBoolean},
 
     expose: {
-      dependencies: ["albumData", "coverArtistContribsByRef"],
+      dependencies: ['albumData', 'coverArtistContribsByRef'],
       transform: (
         hasCoverArt,
-        { albumData, coverArtistContribsByRef, [Track.instance]: track }
+        {albumData, coverArtistContribsByRef, [Track.instance]: track}
       ) =>
         Track.hasCoverArt(
           track,
@@ -758,12 +755,12 @@ Track.propertyDescriptors = {
   },
 
   coverArtFileExtension: {
-    flags: { update: true, expose: true },
+    flags: {update: true, expose: true},
 
-    update: { validate: isFileExtension },
+    update: {validate: isFileExtension},
 
     expose: {
-      dependencies: ["albumData", "coverArtistContribsByRef"],
+      dependencies: ['albumData', 'coverArtistContribsByRef'],
       transform: (
         coverArtFileExtension,
         {
@@ -782,7 +779,7 @@ Track.propertyDescriptors = {
         )
           ? Track.findAlbum(track, albumData)?.trackCoverArtFileExtension
           : Track.findAlbum(track, albumData)?.coverArtFileExtension) ??
-        "jpg",
+        'jpg',
     },
   },
 
@@ -808,11 +805,11 @@ Track.propertyDescriptors = {
   commentatorArtists: Thing.common.commentatorArtists(),
 
   album: {
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
-      dependencies: ["albumData"],
-      compute: ({ [Track.instance]: track, albumData }) =>
+      dependencies: ['albumData'],
+      compute: ({[Track.instance]: track, albumData}) =>
         albumData?.find((album) => album.tracks.includes(track)) ?? null,
     },
   },
@@ -825,28 +822,28 @@ Track.propertyDescriptors = {
   // dataSourceAlbum is available (depending on the Track creator to optionally
   // provide dataSourceAlbumByRef).
   dataSourceAlbum: Thing.common.dynamicThingFromSingleReference(
-    "dataSourceAlbumByRef",
-    "albumData",
+    'dataSourceAlbumByRef',
+    'albumData',
     find.album
   ),
 
   date: {
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
-      dependencies: ["albumData", "dateFirstReleased"],
-      compute: ({ albumData, dateFirstReleased, [Track.instance]: track }) =>
+      dependencies: ['albumData', 'dateFirstReleased'],
+      compute: ({albumData, dateFirstReleased, [Track.instance]: track}) =>
         dateFirstReleased ?? Track.findAlbum(track, albumData)?.date ?? null,
     },
   },
 
   color: {
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
-      dependencies: ["albumData"],
+      dependencies: ['albumData'],
 
-      compute: ({ albumData, [Track.instance]: track }) =>
+      compute: ({albumData, [Track.instance]: track}) =>
         Track.findAlbum(track, albumData)?.trackGroups.find((tg) =>
           tg.tracks.includes(track)
         )?.color ?? null,
@@ -854,15 +851,15 @@ Track.propertyDescriptors = {
   },
 
   coverArtDate: {
-    flags: { update: true, expose: true },
+    flags: {update: true, expose: true},
 
-    update: { validate: isDate },
+    update: {validate: isDate},
 
     expose: {
-      dependencies: ["albumData", "dateFirstReleased"],
+      dependencies: ['albumData', 'dateFirstReleased'],
       transform: (
         coverArtDate,
-        { albumData, dateFirstReleased, [Track.instance]: track }
+        {albumData, dateFirstReleased, [Track.instance]: track}
       ) =>
         coverArtDate ??
         dateFirstReleased ??
@@ -873,16 +870,16 @@ Track.propertyDescriptors = {
   },
 
   originalReleaseTrack: Thing.common.dynamicThingFromSingleReference(
-    "originalReleaseTrackByRef",
-    "trackData",
+    'originalReleaseTrackByRef',
+    'trackData',
     find.track
   ),
 
   otherReleases: {
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
-      dependencies: ["originalReleaseTrackByRef", "trackData"],
+      dependencies: ['originalReleaseTrackByRef', 'trackData'],
 
       compute: ({
         originalReleaseTrackByRef: t1origRef,
@@ -898,7 +895,7 @@ Track.propertyDescriptors = {
         return [
           t1orig,
           ...trackData.filter((t2) => {
-            const { originalReleaseTrack: t2orig } = t2;
+            const {originalReleaseTrack: t2orig} = t2;
             return t2 !== t1 && t2orig && (t2orig === t1orig || t2orig === t1);
           }),
         ].filter(Boolean);
@@ -908,27 +905,27 @@ Track.propertyDescriptors = {
 
   // Previously known as: (track).artists
   artistContribs: Thing.common.dynamicInheritContribs(
-    "artistContribsByRef",
-    "artistContribsByRef",
-    "albumData",
+    'artistContribsByRef',
+    'artistContribsByRef',
+    'albumData',
     Track.findAlbum
   ),
 
   // Previously known as: (track).contributors
-  contributorContribs: Thing.common.dynamicContribs("contributorContribsByRef"),
+  contributorContribs: Thing.common.dynamicContribs('contributorContribsByRef'),
 
   // Previously known as: (track).coverArtists
   coverArtistContribs: Thing.common.dynamicInheritContribs(
-    "coverArtistContribsByRef",
-    "trackCoverArtistContribsByRef",
-    "albumData",
+    'coverArtistContribsByRef',
+    'trackCoverArtistContribsByRef',
+    'albumData',
     Track.findAlbum
   ),
 
   // Previously known as: (track).references
   referencedTracks: Thing.common.dynamicThingsFromReferenceList(
-    "referencedTracksByRef",
-    "trackData",
+    'referencedTracksByRef',
+    'trackData',
     find.track
   ),
 
@@ -941,12 +938,12 @@ Track.propertyDescriptors = {
   // the "Tracks - by Times Referenced" listing page (or other data
   // processing).
   referencedByTracks: {
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
-      dependencies: ["trackData"],
+      dependencies: ['trackData'],
 
-      compute: ({ trackData, [Track.instance]: track }) =>
+      compute: ({trackData, [Track.instance]: track}) =>
         trackData
           ? trackData
               .filter((t) => !t.originalReleaseTrack)
@@ -957,13 +954,13 @@ Track.propertyDescriptors = {
 
   // Previously known as: (track).flashes
   featuredInFlashes: Thing.common.reverseReferenceList(
-    "flashData",
-    "featuredTracks"
+    'flashData',
+    'featuredTracks'
   ),
 
   artTags: Thing.common.dynamicThingsFromReferenceList(
-    "artTagsByRef",
-    "artTagData",
+    'artTagsByRef',
+    'artTagData',
     find.artTag
   ),
 };
@@ -971,12 +968,12 @@ Track.propertyDescriptors = {
 Track.prototype[inspect.custom] = function () {
   const base = Thing.prototype[inspect.custom].apply(this);
 
-  const { album, dataSourceAlbum } = this;
+  const {album, dataSourceAlbum} = this;
   const albumName = album ? album.name : dataSourceAlbum?.name;
   const albumIndex =
     albumName &&
     (album ? album.tracks.indexOf(this) : dataSourceAlbum.tracks.indexOf(this));
-  const trackNum = albumIndex === -1 ? "#?" : `#${albumIndex + 1}`;
+  const trackNum = albumIndex === -1 ? '#?' : `#${albumIndex + 1}`;
 
   return albumName
     ? base + ` (${color.yellow(trackNum)} in ${color.green(albumName)})`
@@ -986,13 +983,13 @@ Track.prototype[inspect.custom] = function () {
 // -> Artist
 
 Artist.filterByContrib = (thingDataProperty, contribsProperty) => ({
-  flags: { expose: true },
+  flags: {expose: true},
 
   expose: {
     dependencies: [thingDataProperty],
 
-    compute: ({ [thingDataProperty]: thingData, [Artist.instance]: artist }) =>
-      thingData?.filter(({ [contribsProperty]: contribs }) =>
+    compute: ({[thingDataProperty]: thingData, [Artist.instance]: artist}) =>
+      thingData?.filter(({[contribsProperty]: contribs}) =>
         contribs?.some((contrib) => contrib.who === artist)
       ),
   },
@@ -1001,16 +998,16 @@ Artist.filterByContrib = (thingDataProperty, contribsProperty) => ({
 Artist.propertyDescriptors = {
   // Update & expose
 
-  name: Thing.common.name("Unnamed Artist"),
+  name: Thing.common.name('Unnamed Artist'),
   directory: Thing.common.directory(),
   urls: Thing.common.urls(),
   contextNotes: Thing.common.simpleString(),
 
   hasAvatar: Thing.common.flag(false),
-  avatarFileExtension: Thing.common.fileExtension("jpg"),
+  avatarFileExtension: Thing.common.fileExtension('jpg'),
 
   aliasNames: {
-    flags: { update: true, expose: true },
+    flags: {update: true, expose: true},
     update: {
       validate: validateArrayItems(isName),
     },
@@ -1029,87 +1026,87 @@ Artist.propertyDescriptors = {
   // Expose only
 
   aliasedArtist: {
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
-      dependencies: ["artistData", "aliasedArtistRef"],
-      compute: ({ artistData, aliasedArtistRef }) =>
+      dependencies: ['artistData', 'aliasedArtistRef'],
+      compute: ({artistData, aliasedArtistRef}) =>
         aliasedArtistRef && artistData
-          ? find.artist(aliasedArtistRef, artistData, { mode: "quiet" })
+          ? find.artist(aliasedArtistRef, artistData, {mode: 'quiet'})
           : null,
     },
   },
 
-  tracksAsArtist: Artist.filterByContrib("trackData", "artistContribs"),
+  tracksAsArtist: Artist.filterByContrib('trackData', 'artistContribs'),
   tracksAsContributor: Artist.filterByContrib(
-    "trackData",
-    "contributorContribs"
+    'trackData',
+    'contributorContribs'
   ),
   tracksAsCoverArtist: Artist.filterByContrib(
-    "trackData",
-    "coverArtistContribs"
+    'trackData',
+    'coverArtistContribs'
   ),
 
   tracksAsAny: {
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
-      dependencies: ["trackData"],
+      dependencies: ['trackData'],
 
-      compute: ({ trackData, [Artist.instance]: artist }) =>
+      compute: ({trackData, [Artist.instance]: artist}) =>
         trackData?.filter((track) =>
           [
             ...track.artistContribs,
             ...track.contributorContribs,
             ...track.coverArtistContribs,
-          ].some(({ who }) => who === artist)
+          ].some(({who}) => who === artist)
         ),
     },
   },
 
   tracksAsCommentator: {
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
-      dependencies: ["trackData"],
+      dependencies: ['trackData'],
 
-      compute: ({ trackData, [Artist.instance]: artist }) =>
-        trackData.filter(({ commentatorArtists }) =>
+      compute: ({trackData, [Artist.instance]: artist}) =>
+        trackData.filter(({commentatorArtists}) =>
           commentatorArtists?.includes(artist)
         ),
     },
   },
 
-  albumsAsAlbumArtist: Artist.filterByContrib("albumData", "artistContribs"),
+  albumsAsAlbumArtist: Artist.filterByContrib('albumData', 'artistContribs'),
   albumsAsCoverArtist: Artist.filterByContrib(
-    "albumData",
-    "coverArtistContribs"
+    'albumData',
+    'coverArtistContribs'
   ),
   albumsAsWallpaperArtist: Artist.filterByContrib(
-    "albumData",
-    "wallpaperArtistContribs"
+    'albumData',
+    'wallpaperArtistContribs'
   ),
   albumsAsBannerArtist: Artist.filterByContrib(
-    "albumData",
-    "bannerArtistContribs"
+    'albumData',
+    'bannerArtistContribs'
   ),
 
   albumsAsCommentator: {
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
-      dependencies: ["albumData"],
+      dependencies: ['albumData'],
 
-      compute: ({ albumData, [Artist.instance]: artist }) =>
-        albumData.filter(({ commentatorArtists }) =>
+      compute: ({albumData, [Artist.instance]: artist}) =>
+        albumData.filter(({commentatorArtists}) =>
           commentatorArtists?.includes(artist)
         ),
     },
   },
 
   flashesAsContributor: Artist.filterByContrib(
-    "flashData",
-    "contributorContribs"
+    'flashData',
+    'contributorContribs'
   ),
 };
 
@@ -1143,7 +1140,7 @@ Artist[S.serializeDescriptors] = {
 Group.propertyDescriptors = {
   // Update & expose
 
-  name: Thing.common.name("Unnamed Group"),
+  name: Thing.common.name('Unnamed Group'),
   directory: Thing.common.directory(),
 
   description: Thing.common.simpleString(),
@@ -1158,42 +1155,42 @@ Group.propertyDescriptors = {
   // Expose only
 
   descriptionShort: {
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
-      dependencies: ["description"],
-      compute: ({ description }) => description.split('<hr class="split">')[0],
+      dependencies: ['description'],
+      compute: ({description}) => description.split('<hr class="split">')[0],
     },
   },
 
   albums: {
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
-      dependencies: ["albumData"],
-      compute: ({ albumData, [Group.instance]: group }) =>
+      dependencies: ['albumData'],
+      compute: ({albumData, [Group.instance]: group}) =>
         albumData?.filter((album) => album.groups.includes(group)) ?? [],
     },
   },
 
   color: {
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
-      dependencies: ["groupCategoryData"],
+      dependencies: ['groupCategoryData'],
 
-      compute: ({ groupCategoryData, [Group.instance]: group }) =>
+      compute: ({groupCategoryData, [Group.instance]: group}) =>
         groupCategoryData.find((category) => category.groups.includes(group))
           ?.color ?? null,
     },
   },
 
   category: {
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
-      dependencies: ["groupCategoryData"],
-      compute: ({ groupCategoryData, [Group.instance]: group }) =>
+      dependencies: ['groupCategoryData'],
+      compute: ({groupCategoryData, [Group.instance]: group}) =>
         groupCategoryData.find((category) => category.groups.includes(group)) ??
         null,
     },
@@ -1203,7 +1200,7 @@ Group.propertyDescriptors = {
 GroupCategory.propertyDescriptors = {
   // Update & expose
 
-  name: Thing.common.name("Unnamed Group Category"),
+  name: Thing.common.name('Unnamed Group Category'),
   color: Thing.common.color(),
 
   groupsByRef: Thing.common.referenceList(Group),
@@ -1215,8 +1212,8 @@ GroupCategory.propertyDescriptors = {
   // Expose only
 
   groups: Thing.common.dynamicThingsFromReferenceList(
-    "groupsByRef",
-    "groupData",
+    'groupsByRef',
+    'groupData',
     find.group
   ),
 };
@@ -1226,7 +1223,7 @@ GroupCategory.propertyDescriptors = {
 ArtTag.propertyDescriptors = {
   // Update & expose
 
-  name: Thing.common.name("Unnamed Art Tag"),
+  name: Thing.common.name('Unnamed Art Tag'),
   directory: Thing.common.directory(),
   color: Thing.common.color(),
   isContentWarning: Thing.common.flag(false),
@@ -1240,16 +1237,16 @@ ArtTag.propertyDescriptors = {
 
   // Previously known as: (tag).things
   taggedInThings: {
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
-      dependencies: ["albumData", "trackData"],
-      compute: ({ albumData, trackData, [ArtTag.instance]: artTag }) =>
+      dependencies: ['albumData', 'trackData'],
+      compute: ({albumData, trackData, [ArtTag.instance]: artTag}) =>
         sortAlbumsTracksChronologically(
           [...albumData, ...trackData].filter((thing) =>
             thing.artTags?.includes(artTag)
           ),
-          { getDate: (o) => o.coverArtDate }
+          {getDate: (o) => o.coverArtDate}
         ),
     },
   },
@@ -1260,7 +1257,7 @@ ArtTag.propertyDescriptors = {
 NewsEntry.propertyDescriptors = {
   // Update & expose
 
-  name: Thing.common.name("Unnamed News Entry"),
+  name: Thing.common.name('Unnamed News Entry'),
   directory: Thing.common.directory(),
   date: Thing.common.simpleDate(),
 
@@ -1269,12 +1266,12 @@ NewsEntry.propertyDescriptors = {
   // Expose only
 
   contentShort: {
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
-      dependencies: ["content"],
+      dependencies: ['content'],
 
-      compute: ({ content }) => content.split('<hr class="split">')[0],
+      compute: ({content}) => content.split('<hr class="split">')[0],
     },
   },
 };
@@ -1284,15 +1281,15 @@ NewsEntry.propertyDescriptors = {
 StaticPage.propertyDescriptors = {
   // Update & expose
 
-  name: Thing.common.name("Unnamed Static Page"),
+  name: Thing.common.name('Unnamed Static Page'),
 
   nameShort: {
-    flags: { update: true, expose: true },
-    update: { validate: isName },
+    flags: {update: true, expose: true},
+    update: {validate: isName},
 
     expose: {
-      dependencies: ["name"],
-      transform: (value, { name }) => value ?? name,
+      dependencies: ['name'],
+      transform: (value, {name}) => value ?? name,
     },
   },
 
@@ -1310,7 +1307,7 @@ HomepageLayout.propertyDescriptors = {
   sidebarContent: Thing.common.simpleString(),
 
   rows: {
-    flags: { update: true, expose: true },
+    flags: {update: true, expose: true},
 
     update: {
       validate: validateArrayItems(validateInstanceOf(HomepageLayoutRow)),
@@ -1321,13 +1318,13 @@ HomepageLayout.propertyDescriptors = {
 HomepageLayoutRow.propertyDescriptors = {
   // Update & expose
 
-  name: Thing.common.name("Unnamed Homepage Row"),
+  name: Thing.common.name('Unnamed Homepage Row'),
 
   type: {
-    flags: { update: true, expose: true },
+    flags: {update: true, expose: true},
 
     update: {
-      validate(value) {
+      validate() {
         throw new Error(`'type' property validator must be overridden`);
       },
     },
@@ -1350,10 +1347,10 @@ HomepageLayoutAlbumsRow.propertyDescriptors = {
   // Update & expose
 
   type: {
-    flags: { update: true, expose: true },
+    flags: {update: true, expose: true},
     update: {
       validate(value) {
-        if (value !== "albums") {
+        if (value !== 'albums') {
           throw new TypeError(`Expected 'albums'`);
         }
 
@@ -1366,25 +1363,25 @@ HomepageLayoutAlbumsRow.propertyDescriptors = {
   sourceAlbumsByRef: Thing.common.referenceList(Album),
 
   countAlbumsFromGroup: {
-    flags: { update: true, expose: true },
-    update: { validate: isCountingNumber },
+    flags: {update: true, expose: true},
+    update: {validate: isCountingNumber},
   },
 
   actionLinks: {
-    flags: { update: true, expose: true },
-    update: { validate: validateArrayItems(isString) },
+    flags: {update: true, expose: true},
+    update: {validate: validateArrayItems(isString)},
   },
 
   // Expose only
 
   sourceGroup: Thing.common.dynamicThingFromSingleReference(
-    "sourceGroupByRef",
-    "groupData",
+    'sourceGroupByRef',
+    'groupData',
     find.group
   ),
   sourceAlbums: Thing.common.dynamicThingsFromReferenceList(
-    "sourceAlbumsByRef",
-    "albumData",
+    'sourceAlbumsByRef',
+    'albumData',
     find.album
   ),
 };
@@ -1394,18 +1391,18 @@ HomepageLayoutAlbumsRow.propertyDescriptors = {
 Flash.propertyDescriptors = {
   // Update & expose
 
-  name: Thing.common.name("Unnamed Flash"),
+  name: Thing.common.name('Unnamed Flash'),
 
   directory: {
-    flags: { update: true, expose: true },
-    update: { validate: isDirectory },
+    flags: {update: true, expose: true},
+    update: {validate: isDirectory},
 
     // Flashes expose directory differently from other Things! Their
     // default directory is dependent on the page number (or ID), not
     // the name.
     expose: {
-      dependencies: ["page"],
-      transform(directory, { page }) {
+      dependencies: ['page'],
+      transform(directory, {page}) {
         if (directory === null && page === null) return null;
         else if (directory === null) return page;
         else return directory;
@@ -1414,8 +1411,8 @@ Flash.propertyDescriptors = {
   },
 
   page: {
-    flags: { update: true, expose: true },
-    update: { validate: oneOf(isString, isNumber) },
+    flags: {update: true, expose: true},
+    update: {validate: oneOf(isString, isNumber)},
 
     expose: {
       transform: (value) => (value === null ? null : value.toString()),
@@ -1424,7 +1421,7 @@ Flash.propertyDescriptors = {
 
   date: Thing.common.simpleDate(),
 
-  coverArtFileExtension: Thing.common.fileExtension("jpg"),
+  coverArtFileExtension: Thing.common.fileExtension('jpg'),
 
   contributorContribsByRef: Thing.common.contribsByRef(),
 
@@ -1440,32 +1437,32 @@ Flash.propertyDescriptors = {
 
   // Expose only
 
-  contributorContribs: Thing.common.dynamicContribs("contributorContribsByRef"),
+  contributorContribs: Thing.common.dynamicContribs('contributorContribsByRef'),
 
   featuredTracks: Thing.common.dynamicThingsFromReferenceList(
-    "featuredTracksByRef",
-    "trackData",
+    'featuredTracksByRef',
+    'trackData',
     find.track
   ),
 
   act: {
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
-      dependencies: ["flashActData"],
+      dependencies: ['flashActData'],
 
-      compute: ({ flashActData, [Flash.instance]: flash }) =>
+      compute: ({flashActData, [Flash.instance]: flash}) =>
         flashActData.find((act) => act.flashes.includes(flash)) ?? null,
     },
   },
 
   color: {
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
-      dependencies: ["flashActData"],
+      dependencies: ['flashActData'],
 
-      compute: ({ flashActData, [Flash.instance]: flash }) =>
+      compute: ({flashActData, [Flash.instance]: flash}) =>
         flashActData.find((act) => act.flashes.includes(flash))?.color ?? null,
     },
   },
@@ -1485,7 +1482,7 @@ Flash[S.serializeDescriptors] = {
 FlashAct.propertyDescriptors = {
   // Update & expose
 
-  name: Thing.common.name("Unnamed Flash Act"),
+  name: Thing.common.name('Unnamed Flash Act'),
   color: Thing.common.color(),
   anchor: Thing.common.simpleString(),
   jump: Thing.common.simpleString(),
@@ -1500,8 +1497,8 @@ FlashAct.propertyDescriptors = {
   // Expose only
 
   flashes: Thing.common.dynamicThingsFromReferenceList(
-    "flashesByRef",
-    "flashData",
+    'flashesByRef',
+    'flashData',
     find.flash
   ),
 };
@@ -1511,16 +1508,16 @@ FlashAct.propertyDescriptors = {
 WikiInfo.propertyDescriptors = {
   // Update & expose
 
-  name: Thing.common.name("Unnamed Wiki"),
+  name: Thing.common.name('Unnamed Wiki'),
 
   // Displayed in nav bar.
   nameShort: {
-    flags: { update: true, expose: true },
-    update: { validate: isName },
+    flags: {update: true, expose: true},
+    update: {validate: isName},
 
     expose: {
-      dependencies: ["name"],
-      transform: (value, { name }) => value ?? name,
+      dependencies: ['name'],
+      transform: (value, {name}) => value ?? name,
     },
   },
 
@@ -1532,13 +1529,13 @@ WikiInfo.propertyDescriptors = {
   footerContent: Thing.common.simpleString(),
 
   defaultLanguage: {
-    flags: { update: true, expose: true },
-    update: { validate: isLanguageCode },
+    flags: {update: true, expose: true},
+    update: {validate: isLanguageCode},
   },
 
   canonicalBase: {
-    flags: { update: true, expose: true },
-    update: { validate: isURL },
+    flags: {update: true, expose: true},
+    update: {validate: isURL},
   },
 
   divideTrackListsByGroupsByRef: Thing.common.referenceList(Group),
@@ -1557,8 +1554,8 @@ WikiInfo.propertyDescriptors = {
   // Expose only
 
   divideTrackListsByGroups: Thing.common.dynamicThingsFromReferenceList(
-    "divideTrackListsByGroupsByRef",
-    "groupData",
+    'divideTrackListsByGroupsByRef',
+    'groupData',
     find.group
   ),
 };
@@ -1566,10 +1563,10 @@ WikiInfo.propertyDescriptors = {
 // -> Language
 
 const intlHelper = (constructor, opts) => ({
-  flags: { expose: true },
+  flags: {expose: true},
   expose: {
-    dependencies: ["code", "intlCode"],
-    compute: ({ code, intlCode }) => {
+    dependencies: ['code', 'intlCode'],
+    compute: ({code, intlCode}) => {
       const constructCode = intlCode ?? code;
       if (!constructCode) return null;
       return Reflect.construct(constructor, [constructCode, opts]);
@@ -1584,8 +1581,8 @@ Language.propertyDescriptors = {
   // from other languages (similar to how "Directory" operates in many data
   // objects).
   code: {
-    flags: { update: true, expose: true },
-    update: { validate: isLanguageCode },
+    flags: {update: true, expose: true},
+    update: {validate: isLanguageCode},
   },
 
   // Human-readable name. This should be the language's own native name, not
@@ -1596,11 +1593,11 @@ Language.propertyDescriptors = {
   // Usually this will be the same as the language's general code, but it
   // may be overridden to provide Intl constructors an alternative value.
   intlCode: {
-    flags: { update: true, expose: true },
-    update: { validate: isLanguageCode },
+    flags: {update: true, expose: true},
+    update: {validate: isLanguageCode},
     expose: {
-      dependencies: ["code"],
-      transform: (intlCode, { code }) => intlCode ?? code,
+      dependencies: ['code'],
+      transform: (intlCode, {code}) => intlCode ?? code,
     },
   },
 
@@ -1617,13 +1614,13 @@ Language.propertyDescriptors = {
   // Mapping of translation keys to values (strings). Generally, don't
   // access this object directly - use methods instead.
   strings: {
-    flags: { update: true, expose: true },
-    update: { validate: (t) => typeof t === "object" },
+    flags: {update: true, expose: true},
+    update: {validate: (t) => typeof t === 'object'},
     expose: {
-      dependencies: ["inheritedStrings"],
-      transform(strings, { inheritedStrings }) {
+      dependencies: ['inheritedStrings'],
+      transform(strings, {inheritedStrings}) {
         if (strings || inheritedStrings) {
-          return { ...(inheritedStrings ?? {}), ...(strings ?? {}) };
+          return {...(inheritedStrings ?? {}), ...(strings ?? {})};
         } else {
           return null;
         }
@@ -1634,8 +1631,8 @@ Language.propertyDescriptors = {
   // May be provided to specify "default" strings, generally (but not
   // necessarily) inherited from another Language object.
   inheritedStrings: {
-    flags: { update: true, expose: true },
-    update: { validate: (t) => typeof t === "object" },
+    flags: {update: true, expose: true},
+    update: {validate: (t) => typeof t === 'object'},
   },
 
   // Update only
@@ -1644,20 +1641,20 @@ Language.propertyDescriptors = {
 
   // Expose only
 
-  intl_date: intlHelper(Intl.DateTimeFormat, { full: true }),
+  intl_date: intlHelper(Intl.DateTimeFormat, {full: true}),
   intl_number: intlHelper(Intl.NumberFormat),
-  intl_listConjunction: intlHelper(Intl.ListFormat, { type: "conjunction" }),
-  intl_listDisjunction: intlHelper(Intl.ListFormat, { type: "disjunction" }),
-  intl_listUnit: intlHelper(Intl.ListFormat, { type: "unit" }),
-  intl_pluralCardinal: intlHelper(Intl.PluralRules, { type: "cardinal" }),
-  intl_pluralOrdinal: intlHelper(Intl.PluralRules, { type: "ordinal" }),
+  intl_listConjunction: intlHelper(Intl.ListFormat, {type: 'conjunction'}),
+  intl_listDisjunction: intlHelper(Intl.ListFormat, {type: 'disjunction'}),
+  intl_listUnit: intlHelper(Intl.ListFormat, {type: 'unit'}),
+  intl_pluralCardinal: intlHelper(Intl.PluralRules, {type: 'cardinal'}),
+  intl_pluralOrdinal: intlHelper(Intl.PluralRules, {type: 'ordinal'}),
 
   validKeys: {
-    flags: { expose: true },
+    flags: {expose: true},
 
     expose: {
-      dependencies: ["strings", "inheritedStrings"],
-      compute: ({ strings, inheritedStrings }) =>
+      dependencies: ['strings', 'inheritedStrings'],
+      compute: ({strings, inheritedStrings}) =>
         Array.from(
           new Set([
             ...Object.keys(inheritedStrings ?? {}),
@@ -1668,12 +1665,12 @@ Language.propertyDescriptors = {
   },
 
   strings_htmlEscaped: {
-    flags: { expose: true },
+    flags: {expose: true},
     expose: {
-      dependencies: ["strings", "inheritedStrings", "escapeHTML"],
-      compute({ strings, inheritedStrings, escapeHTML }) {
+      dependencies: ['strings', 'inheritedStrings', 'escapeHTML'],
+      compute({strings, inheritedStrings, escapeHTML}) {
         if (!(strings || inheritedStrings) || !escapeHTML) return null;
-        const allStrings = { ...(inheritedStrings ?? {}), ...(strings ?? {}) };
+        const allStrings = {...(inheritedStrings ?? {}), ...(strings ?? {})};
         return Object.fromEntries(
           Object.entries(allStrings).map(([k, v]) => [k, escapeHTML(v)])
         );
@@ -1683,12 +1680,12 @@ Language.propertyDescriptors = {
 };
 
 const countHelper = (stringKey, argName = stringKey) =>
-  function (value, { unit = false } = {}) {
+  function (value, {unit = false} = {}) {
     return this.$(
       unit
         ? `count.${stringKey}.withUnit.` + this.getUnitForm(value)
         : `count.${stringKey}`,
-      { [argName]: this.formatNumber(value) }
+      {[argName]: this.formatNumber(value)}
     );
   };
 
@@ -1704,7 +1701,7 @@ Object.assign(Language.prototype, {
   },
 
   getUnitForm(value) {
-    this.assertIntlAvailable("intl_pluralCardinal");
+    this.assertIntlAvailable('intl_pluralCardinal');
     return this.intl_pluralCardinal.select(value);
   },
 
@@ -1738,7 +1735,7 @@ Object.assign(Language.prototype, {
     // like, who cares, dude?) Also, this is an array, 8ecause it's handy
     // for the iterating we're a8out to do.
     const processedArgs = Object.entries(args).map(([k, v]) => [
-      k.replace(/[A-Z]/g, "_$&").toUpperCase(),
+      k.replace(/[A-Z]/g, '_$&').toUpperCase(),
       v,
     ]);
 
@@ -1758,55 +1755,55 @@ Object.assign(Language.prototype, {
   },
 
   formatDate(date) {
-    this.assertIntlAvailable("intl_date");
+    this.assertIntlAvailable('intl_date');
     return this.intl_date.format(date);
   },
 
   formatDateRange(startDate, endDate) {
-    this.assertIntlAvailable("intl_date");
+    this.assertIntlAvailable('intl_date');
     return this.intl_date.formatRange(startDate, endDate);
   },
 
-  formatDuration(secTotal, { approximate = false, unit = false } = {}) {
+  formatDuration(secTotal, {approximate = false, unit = false} = {}) {
     if (secTotal === 0) {
-      return this.formatString("count.duration.missing");
+      return this.formatString('count.duration.missing');
     }
 
     const hour = Math.floor(secTotal / 3600);
     const min = Math.floor((secTotal - hour * 3600) / 60);
     const sec = Math.floor(secTotal - hour * 3600 - min * 60);
 
-    const pad = (val) => val.toString().padStart(2, "0");
+    const pad = (val) => val.toString().padStart(2, '0');
 
-    const stringSubkey = unit ? ".withUnit" : "";
+    const stringSubkey = unit ? '.withUnit' : '';
 
     const duration =
       hour > 0
-        ? this.formatString("count.duration.hours" + stringSubkey, {
+        ? this.formatString('count.duration.hours' + stringSubkey, {
             hours: hour,
             minutes: pad(min),
             seconds: pad(sec),
           })
-        : this.formatString("count.duration.minutes" + stringSubkey, {
+        : this.formatString('count.duration.minutes' + stringSubkey, {
             minutes: min,
             seconds: pad(sec),
           });
 
     return approximate
-      ? this.formatString("count.duration.approximate", { duration })
+      ? this.formatString('count.duration.approximate', {duration})
       : duration;
   },
 
   formatIndex(value) {
-    this.assertIntlAvailable("intl_pluralOrdinal");
+    this.assertIntlAvailable('intl_pluralOrdinal');
     return this.formatString(
-      "count.index." + this.intl_pluralOrdinal.select(value),
-      { index: value }
+      'count.index.' + this.intl_pluralOrdinal.select(value),
+      {index: value}
     );
   },
 
   formatNumber(value) {
-    this.assertIntlAvailable("intl_number");
+    this.assertIntlAvailable('intl_number');
     return this.intl_number.format(value);
   },
 
@@ -1817,70 +1814,70 @@ Object.assign(Language.prototype, {
 
     const words =
       value > 1000
-        ? this.formatString("count.words.thousand", { words: num })
-        : this.formatString("count.words", { words: num });
+        ? this.formatString('count.words.thousand', {words: num})
+        : this.formatString('count.words', {words: num});
 
     return this.formatString(
-      "count.words.withUnit." + this.getUnitForm(value),
-      { words }
+      'count.words.withUnit.' + this.getUnitForm(value),
+      {words}
     );
   },
 
   // Conjunction list: A, B, and C
   formatConjunctionList(array) {
-    this.assertIntlAvailable("intl_listConjunction");
+    this.assertIntlAvailable('intl_listConjunction');
     return this.intl_listConjunction.format(array);
   },
 
   // Disjunction lists: A, B, or C
   formatDisjunctionList(array) {
-    this.assertIntlAvailable("intl_listDisjunction");
+    this.assertIntlAvailable('intl_listDisjunction');
     return this.intl_listDisjunction.format(array);
   },
 
   // Unit lists: A, B, C
   formatUnitList(array) {
-    this.assertIntlAvailable("intl_listUnit");
+    this.assertIntlAvailable('intl_listUnit');
     return this.intl_listUnit.format(array);
   },
 
   // File sizes: 42.5 kB, 127.2 MB, 4.13 GB, 998.82 TB
   formatFileSize(bytes) {
-    if (!bytes) return "";
+    if (!bytes) return '';
 
     bytes = parseInt(bytes);
-    if (isNaN(bytes)) return "";
+    if (isNaN(bytes)) return '';
 
     const round = (exp) => Math.round(bytes / 10 ** (exp - 1)) / 10;
 
     if (bytes >= 10 ** 12) {
-      return this.formatString("count.fileSize.terabytes", {
+      return this.formatString('count.fileSize.terabytes', {
         terabytes: round(12),
       });
     } else if (bytes >= 10 ** 9) {
-      return this.formatString("count.fileSize.gigabytes", {
+      return this.formatString('count.fileSize.gigabytes', {
         gigabytes: round(9),
       });
     } else if (bytes >= 10 ** 6) {
-      return this.formatString("count.fileSize.megabytes", {
+      return this.formatString('count.fileSize.megabytes', {
         megabytes: round(6),
       });
     } else if (bytes >= 10 ** 3) {
-      return this.formatString("count.fileSize.kilobytes", {
+      return this.formatString('count.fileSize.kilobytes', {
         kilobytes: round(3),
       });
     } else {
-      return this.formatString("count.fileSize.bytes", { bytes });
+      return this.formatString('count.fileSize.bytes', {bytes});
     }
   },
 
   // TODO: These are hard-coded. Is there a better way?
-  countAdditionalFiles: countHelper("additionalFiles", "files"),
-  countAlbums: countHelper("albums"),
-  countCommentaryEntries: countHelper("commentaryEntries", "entries"),
-  countContributions: countHelper("contributions"),
-  countCoverArts: countHelper("coverArts"),
-  countTimesReferenced: countHelper("timesReferenced"),
-  countTimesUsed: countHelper("timesUsed"),
-  countTracks: countHelper("tracks"),
+  countAdditionalFiles: countHelper('additionalFiles', 'files'),
+  countAlbums: countHelper('albums'),
+  countCommentaryEntries: countHelper('commentaryEntries', 'entries'),
+  countContributions: countHelper('contributions'),
+  countCoverArts: countHelper('coverArts'),
+  countTimesReferenced: countHelper('timesReferenced'),
+  countTimesUsed: countHelper('timesUsed'),
+  countTracks: countHelper('tracks'),
 });
diff --git a/src/data/validators.js b/src/data/validators.js
index eab26896..8d922399 100644
--- a/src/data/validators.js
+++ b/src/data/validators.js
@@ -1,13 +1,13 @@
-// @format
+/** @format */
 
-import { withAggregate } from "../util/sugar.js";
+import {withAggregate} from '../util/sugar.js';
 
-import { color, ENABLE_COLOR, decorateTime } from "../util/cli.js";
+import {color, ENABLE_COLOR} from '../util/cli.js';
 
-import { inspect as nodeInspect } from "util";
+import {inspect as nodeInspect} from 'util';
 
 function inspect(value) {
-  return nodeInspect(value, { colors: ENABLE_COLOR });
+  return nodeInspect(value, {colors: ENABLE_COLOR});
 }
 
 // Basic types (primitives)
@@ -24,11 +24,11 @@ function isType(value, type) {
 }
 
 export function isBoolean(value) {
-  return isType(value, "boolean");
+  return isType(value, 'boolean');
 }
 
 export function isNumber(value) {
-  return isType(value, "number");
+  return isType(value, 'number');
 }
 
 export function isPositive(number) {
@@ -86,7 +86,7 @@ export function isWholeNumber(number) {
 }
 
 export function isString(value) {
-  return isType(value, "string");
+  return isType(value, 'string');
 }
 
 export function isStringNonEmpty(value) {
@@ -116,7 +116,7 @@ export function isDate(value) {
 }
 
 export function isObject(value) {
-  isType(value, "object");
+  isType(value, 'object');
 
   // Note: Please remember that null is always a valid value for properties
   // held by a CacheableObject. This assertion is exclusively for use in other
@@ -127,7 +127,7 @@ export function isObject(value) {
 }
 
 export function isArray(value) {
-  if (typeof value !== "object" || value === null || !Array.isArray(value))
+  if (typeof value !== 'object' || value === null || !Array.isArray(value))
     throw new TypeError(`Expected an array, got ${value}`);
 
   return true;
@@ -156,7 +156,7 @@ export function validateArrayItems(itemValidator) {
   return (array) => {
     isArray(array);
 
-    withAggregate({ message: "Errors validating array items" }, ({ wrap }) => {
+    withAggregate({message: 'Errors validating array items'}, ({wrap}) => {
       array.forEach(wrap(fn));
     });
 
@@ -173,7 +173,7 @@ export function validateInstanceOf(constructor) {
 export function isColor(color) {
   isStringNonEmpty(color);
 
-  if (color.startsWith("#")) {
+  if (color.startsWith('#')) {
     if (![1 + 3, 1 + 4, 1 + 6, 1 + 8].includes(color.length))
       throw new TypeError(
         `Expected #rgb, #rgba, #rrggbb, or #rrggbbaa, got length ${color.length}`
@@ -192,7 +192,7 @@ export function isCommentary(commentary) {
   return isString(commentary);
 }
 
-const isArtistRef = validateReference("artist");
+const isArtistRef = validateReference('artist');
 
 export function validateProperties(spec) {
   const specEntries = Object.entries(spec);
@@ -205,8 +205,8 @@ export function validateProperties(spec) {
       throw new TypeError(`Expected an object, got array`);
 
     withAggregate(
-      { message: `Errors validating object properties` },
-      ({ call }) => {
+      {message: `Errors validating object properties`},
+      ({call}) => {
         for (const [specKey, specValidator] of specEntries) {
           call(() => {
             const value = object[specKey];
@@ -229,7 +229,7 @@ export function validateProperties(spec) {
             throw new Error(
               `Unknown keys present (${
                 unknownKeys.length
-              }): [${unknownKeys.join(", ")}]`
+              }): [${unknownKeys.join(', ')}]`
             );
           });
         }
@@ -273,7 +273,7 @@ export function isDimensions(dimensions) {
 export function isDirectory(directory) {
   isStringNonEmpty(directory);
 
-  if (directory.match(/[^a-zA-Z0-9_\-]/))
+  if (directory.match(/[^a-zA-Z0-9_-]/))
     throw new TypeError(
       `Expected only letters, numbers, dash, and underscore, got "${directory}"`
     );
@@ -291,7 +291,7 @@ export function isDuration(duration) {
 export function isFileExtension(string) {
   isStringNonEmpty(string);
 
-  if (string[0] === ".")
+  if (string[0] === '.')
     throw new TypeError(`Expected no dot (.) at the start of file extension`);
 
   if (string.match(/[^a-zA-Z0-9_]/))
@@ -321,7 +321,7 @@ export function isURL(string) {
   return true;
 }
 
-export function validateReference(type = "track") {
+export function validateReference(type = 'track') {
   return (ref) => {
     isStringNonEmpty(ref);
 
@@ -332,7 +332,7 @@ export function validateReference(type = "track") {
     if (!match) throw new TypeError(`Malformed reference`);
 
     const {
-      groups: { typePart, directoryPart },
+      groups: {typePart, directoryPart},
     } = match;
 
     if (typePart && typePart !== type)
@@ -348,7 +348,7 @@ export function validateReference(type = "track") {
   };
 }
 
-export function validateReferenceList(type = "") {
+export function validateReferenceList(type = '') {
   return validateArrayItems(validateReference(type));
 }
 
diff --git a/src/data/yaml.js b/src/data/yaml.js
index 5058bb39..a4255764 100644
--- a/src/data/yaml.js
+++ b/src/data/yaml.js
@@ -1,13 +1,13 @@
-// @format
-//
+/** @format */
+
 // yaml.js - specification for HSMusic YAML data file format and utilities for
 // loading and processing YAML files and documents
 
-import * as path from "path";
-import yaml from "js-yaml";
+import * as path from 'path';
+import yaml from 'js-yaml';
 
-import { readFile } from "fs/promises";
-import { inspect as nodeInspect } from "util";
+import {readFile} from 'fs/promises';
+import {inspect as nodeInspect} from 'util';
 
 import {
   Album,
@@ -19,16 +19,15 @@ import {
   GroupCategory,
   HomepageLayout,
   HomepageLayoutAlbumsRow,
-  HomepageLayoutRow,
   NewsEntry,
   StaticPage,
   Thing,
   Track,
   TrackGroup,
   WikiInfo,
-} from "./things.js";
+} from './things.js';
 
-import { color, ENABLE_COLOR, logInfo, logWarn } from "../util/cli.js";
+import {color, ENABLE_COLOR, logInfo, logWarn} from '../util/cli.js';
 
 import {
   decorateErrorWithIndex,
@@ -36,36 +35,36 @@ import {
   openAggregate,
   showAggregate,
   withAggregate,
-} from "../util/sugar.js";
+} from '../util/sugar.js';
 
 import {
   sortAlbumsTracksChronologically,
   sortAlphabetically,
   sortChronologically,
-} from "../util/wiki-data.js";
+} from '../util/wiki-data.js';
 
-import find, { bindFind } from "../util/find.js";
-import { findFiles } from "../util/io.js";
+import find, {bindFind} from '../util/find.js';
+import {findFiles} from '../util/io.js';
 
 // --> General supporting stuff
 
 function inspect(value) {
-  return nodeInspect(value, { colors: ENABLE_COLOR });
+  return nodeInspect(value, {colors: ENABLE_COLOR});
 }
 
 // --> YAML data repository structure constants
 
-export const WIKI_INFO_FILE = "wiki-info.yaml";
-export const BUILD_DIRECTIVE_DATA_FILE = "build-directives.yaml";
-export const HOMEPAGE_LAYOUT_DATA_FILE = "homepage.yaml";
-export const ARTIST_DATA_FILE = "artists.yaml";
-export const FLASH_DATA_FILE = "flashes.yaml";
-export const NEWS_DATA_FILE = "news.yaml";
-export const ART_TAG_DATA_FILE = "tags.yaml";
-export const GROUP_DATA_FILE = "groups.yaml";
-export const STATIC_PAGE_DATA_FILE = "static-pages.yaml";
+export const WIKI_INFO_FILE = 'wiki-info.yaml';
+export const BUILD_DIRECTIVE_DATA_FILE = 'build-directives.yaml';
+export const HOMEPAGE_LAYOUT_DATA_FILE = 'homepage.yaml';
+export const ARTIST_DATA_FILE = 'artists.yaml';
+export const FLASH_DATA_FILE = 'flashes.yaml';
+export const NEWS_DATA_FILE = 'news.yaml';
+export const ART_TAG_DATA_FILE = 'tags.yaml';
+export const GROUP_DATA_FILE = 'groups.yaml';
+export const STATIC_PAGE_DATA_FILE = 'static-pages.yaml';
 
-export const DATA_ALBUM_DIRECTORY = "album";
+export const DATA_ALBUM_DIRECTORY = 'album';
 
 // --> Document processing functions
 
@@ -119,7 +118,7 @@ function makeProcessDocument(
   );
 
   const decorateErrorWithName = (fn) => {
-    const nameField = propertyFieldMapping["name"];
+    const nameField = propertyFieldMapping['name'];
     if (!nameField) return fn;
 
     return (document) => {
@@ -168,8 +167,8 @@ function makeProcessDocument(
     const thing = Reflect.construct(thingClass, []);
 
     withAggregate(
-      { message: `Errors applying ${color.green(thingClass.name)} properties` },
-      ({ call }) => {
+      {message: `Errors applying ${color.green(thingClass.name)} properties`},
+      ({call}) => {
         for (const [property, value] of Object.entries(sourceProperties)) {
           call(() => (thing[property] = value));
         }
@@ -184,7 +183,7 @@ makeProcessDocument.UnknownFieldsError = class UnknownFieldsError extends (
   Error
 ) {
   constructor(fields) {
-    super(`Unknown fields present: ${fields.join(", ")}`);
+    super(`Unknown fields present: ${fields.join(', ')}`);
     this.fields = fields;
   }
 };
@@ -192,72 +191,72 @@ makeProcessDocument.UnknownFieldsError = class UnknownFieldsError extends (
 export const processAlbumDocument = makeProcessDocument(Album, {
   fieldTransformations: {
     Artists: parseContributors,
-    "Cover Artists": parseContributors,
-    "Default Track Cover Artists": parseContributors,
-    "Wallpaper Artists": parseContributors,
-    "Banner Artists": parseContributors,
+    'Cover Artists': parseContributors,
+    'Default Track Cover Artists': parseContributors,
+    'Wallpaper Artists': parseContributors,
+    'Banner Artists': parseContributors,
 
     Date: (value) => new Date(value),
-    "Date Added": (value) => new Date(value),
-    "Cover Art Date": (value) => new Date(value),
-    "Default Track Cover Art Date": (value) => new Date(value),
+    'Date Added': (value) => new Date(value),
+    'Cover Art Date': (value) => new Date(value),
+    'Default Track Cover Art Date': (value) => new Date(value),
 
-    "Banner Dimensions": parseDimensions,
+    'Banner Dimensions': parseDimensions,
 
-    "Additional Files": parseAdditionalFiles,
+    'Additional Files': parseAdditionalFiles,
   },
 
   propertyFieldMapping: {
-    name: "Album",
+    name: 'Album',
 
-    color: "Color",
-    directory: "Directory",
-    urls: "URLs",
+    color: 'Color',
+    directory: 'Directory',
+    urls: 'URLs',
 
-    artistContribsByRef: "Artists",
-    coverArtistContribsByRef: "Cover Artists",
-    trackCoverArtistContribsByRef: "Default Track Cover Artists",
+    artistContribsByRef: 'Artists',
+    coverArtistContribsByRef: 'Cover Artists',
+    trackCoverArtistContribsByRef: 'Default Track Cover Artists',
 
-    coverArtFileExtension: "Cover Art File Extension",
-    trackCoverArtFileExtension: "Track Art File Extension",
+    coverArtFileExtension: 'Cover Art File Extension',
+    trackCoverArtFileExtension: 'Track Art File Extension',
 
-    wallpaperArtistContribsByRef: "Wallpaper Artists",
-    wallpaperStyle: "Wallpaper Style",
-    wallpaperFileExtension: "Wallpaper File Extension",
+    wallpaperArtistContribsByRef: 'Wallpaper Artists',
+    wallpaperStyle: 'Wallpaper Style',
+    wallpaperFileExtension: 'Wallpaper File Extension',
 
-    bannerArtistContribsByRef: "Banner Artists",
-    bannerStyle: "Banner Style",
-    bannerFileExtension: "Banner File Extension",
-    bannerDimensions: "Banner Dimensions",
+    bannerArtistContribsByRef: 'Banner Artists',
+    bannerStyle: 'Banner Style',
+    bannerFileExtension: 'Banner File Extension',
+    bannerDimensions: 'Banner Dimensions',
 
-    date: "Date",
-    trackArtDate: "Default Track Cover Art Date",
-    coverArtDate: "Cover Art Date",
-    dateAddedToWiki: "Date Added",
+    date: 'Date',
+    trackArtDate: 'Default Track Cover Art Date',
+    coverArtDate: 'Cover Art Date',
+    dateAddedToWiki: 'Date Added',
 
-    hasCoverArt: "Has Cover Art",
-    hasTrackArt: "Has Track Art",
-    hasTrackNumbers: "Has Track Numbers",
-    isMajorRelease: "Major Release",
-    isListedOnHomepage: "Listed on Homepage",
+    hasCoverArt: 'Has Cover Art',
+    hasTrackArt: 'Has Track Art',
+    hasTrackNumbers: 'Has Track Numbers',
+    isMajorRelease: 'Major Release',
+    isListedOnHomepage: 'Listed on Homepage',
 
-    groupsByRef: "Groups",
-    artTagsByRef: "Art Tags",
-    commentary: "Commentary",
+    groupsByRef: 'Groups',
+    artTagsByRef: 'Art Tags',
+    commentary: 'Commentary',
 
-    additionalFiles: "Additional Files",
+    additionalFiles: 'Additional Files',
   },
 });
 
 export const processTrackGroupDocument = makeProcessDocument(TrackGroup, {
   fieldTransformations: {
-    "Date Originally Released": (value) => new Date(value),
+    'Date Originally Released': (value) => new Date(value),
   },
 
   propertyFieldMapping: {
-    name: "Group",
-    color: "Color",
-    dateOriginallyReleased: "Date Originally Released",
+    name: 'Group',
+    color: 'Color',
+    dateOriginallyReleased: 'Date Originally Released',
   },
 });
 
@@ -265,60 +264,60 @@ export const processTrackDocument = makeProcessDocument(Track, {
   fieldTransformations: {
     Duration: getDurationInSeconds,
 
-    "Date First Released": (value) => new Date(value),
-    "Cover Art Date": (value) => new Date(value),
+    'Date First Released': (value) => new Date(value),
+    'Cover Art Date': (value) => new Date(value),
 
     Artists: parseContributors,
     Contributors: parseContributors,
-    "Cover Artists": parseContributors,
+    'Cover Artists': parseContributors,
 
-    "Additional Files": parseAdditionalFiles,
+    'Additional Files': parseAdditionalFiles,
   },
 
   propertyFieldMapping: {
-    name: "Track",
+    name: 'Track',
 
-    directory: "Directory",
-    duration: "Duration",
-    urls: "URLs",
+    directory: 'Directory',
+    duration: 'Duration',
+    urls: 'URLs',
 
-    coverArtDate: "Cover Art Date",
-    coverArtFileExtension: "Cover Art File Extension",
-    dateFirstReleased: "Date First Released",
-    hasCoverArt: "Has Cover Art",
-    hasURLs: "Has URLs",
+    coverArtDate: 'Cover Art Date',
+    coverArtFileExtension: 'Cover Art File Extension',
+    dateFirstReleased: 'Date First Released',
+    hasCoverArt: 'Has Cover Art',
+    hasURLs: 'Has URLs',
 
-    referencedTracksByRef: "Referenced Tracks",
-    artistContribsByRef: "Artists",
-    contributorContribsByRef: "Contributors",
-    coverArtistContribsByRef: "Cover Artists",
-    artTagsByRef: "Art Tags",
-    originalReleaseTrackByRef: "Originally Released As",
+    referencedTracksByRef: 'Referenced Tracks',
+    artistContribsByRef: 'Artists',
+    contributorContribsByRef: 'Contributors',
+    coverArtistContribsByRef: 'Cover Artists',
+    artTagsByRef: 'Art Tags',
+    originalReleaseTrackByRef: 'Originally Released As',
 
-    commentary: "Commentary",
-    lyrics: "Lyrics",
+    commentary: 'Commentary',
+    lyrics: 'Lyrics',
 
-    additionalFiles: "Additional Files",
+    additionalFiles: 'Additional Files',
   },
 
-  ignoredFields: ["Sampled Tracks"],
+  ignoredFields: ['Sampled Tracks'],
 });
 
 export const processArtistDocument = makeProcessDocument(Artist, {
   propertyFieldMapping: {
-    name: "Artist",
+    name: 'Artist',
 
-    directory: "Directory",
-    urls: "URLs",
-    hasAvatar: "Has Avatar",
-    avatarFileExtension: "Avatar File Extension",
+    directory: 'Directory',
+    urls: 'URLs',
+    hasAvatar: 'Has Avatar',
+    avatarFileExtension: 'Avatar File Extension',
 
-    aliasNames: "Aliases",
+    aliasNames: 'Aliases',
 
-    contextNotes: "Context Notes",
+    contextNotes: 'Context Notes',
   },
 
-  ignoredFields: ["Dead URLs"],
+  ignoredFields: ['Dead URLs'],
 });
 
 export const processFlashDocument = makeProcessDocument(Flash, {
@@ -329,26 +328,26 @@ export const processFlashDocument = makeProcessDocument(Flash, {
   },
 
   propertyFieldMapping: {
-    name: "Flash",
+    name: 'Flash',
 
-    directory: "Directory",
-    page: "Page",
-    date: "Date",
-    coverArtFileExtension: "Cover Art File Extension",
+    directory: 'Directory',
+    page: 'Page',
+    date: 'Date',
+    coverArtFileExtension: 'Cover Art File Extension',
 
-    featuredTracksByRef: "Featured Tracks",
-    contributorContribsByRef: "Contributors",
-    urls: "URLs",
+    featuredTracksByRef: 'Featured Tracks',
+    contributorContribsByRef: 'Contributors',
+    urls: 'URLs',
   },
 });
 
 export const processFlashActDocument = makeProcessDocument(FlashAct, {
   propertyFieldMapping: {
-    name: "Act",
-    color: "Color",
-    anchor: "Anchor",
-    jump: "Jump",
-    jumpColor: "Jump Color",
+    name: 'Act',
+    color: 'Color',
+    anchor: 'Anchor',
+    jump: 'Jump',
+    jumpColor: 'Jump Color',
   },
 });
 
@@ -358,66 +357,66 @@ export const processNewsEntryDocument = makeProcessDocument(NewsEntry, {
   },
 
   propertyFieldMapping: {
-    name: "Name",
-    directory: "Directory",
-    date: "Date",
-    content: "Content",
+    name: 'Name',
+    directory: 'Directory',
+    date: 'Date',
+    content: 'Content',
   },
 });
 
 export const processArtTagDocument = makeProcessDocument(ArtTag, {
   propertyFieldMapping: {
-    name: "Tag",
-    directory: "Directory",
-    color: "Color",
-    isContentWarning: "Is CW",
+    name: 'Tag',
+    directory: 'Directory',
+    color: 'Color',
+    isContentWarning: 'Is CW',
   },
 });
 
 export const processGroupDocument = makeProcessDocument(Group, {
   propertyFieldMapping: {
-    name: "Group",
-    directory: "Directory",
-    description: "Description",
-    urls: "URLs",
+    name: 'Group',
+    directory: 'Directory',
+    description: 'Description',
+    urls: 'URLs',
   },
 });
 
 export const processGroupCategoryDocument = makeProcessDocument(GroupCategory, {
   propertyFieldMapping: {
-    name: "Category",
-    color: "Color",
+    name: 'Category',
+    color: 'Color',
   },
 });
 
 export const processStaticPageDocument = makeProcessDocument(StaticPage, {
   propertyFieldMapping: {
-    name: "Name",
-    nameShort: "Short Name",
-    directory: "Directory",
+    name: 'Name',
+    nameShort: 'Short Name',
+    directory: 'Directory',
 
-    content: "Content",
-    stylesheet: "Style",
+    content: 'Content',
+    stylesheet: 'Style',
 
-    showInNavigationBar: "Show in Navigation Bar",
+    showInNavigationBar: 'Show in Navigation Bar',
   },
 });
 
 export const processWikiInfoDocument = makeProcessDocument(WikiInfo, {
   propertyFieldMapping: {
-    name: "Name",
-    nameShort: "Short Name",
-    color: "Color",
-    description: "Description",
-    footerContent: "Footer Content",
-    defaultLanguage: "Default Language",
-    canonicalBase: "Canonical Base",
-    divideTrackListsByGroupsByRef: "Divide Track Lists By Groups",
-    enableFlashesAndGames: "Enable Flashes & Games",
-    enableListings: "Enable Listings",
-    enableNews: "Enable News",
-    enableArtTagUI: "Enable Art Tag UI",
-    enableGroupUI: "Enable Group UI",
+    name: 'Name',
+    nameShort: 'Short Name',
+    color: 'Color',
+    description: 'Description',
+    footerContent: 'Footer Content',
+    defaultLanguage: 'Default Language',
+    canonicalBase: 'Canonical Base',
+    divideTrackListsByGroupsByRef: 'Divide Track Lists By Groups',
+    enableFlashesAndGames: 'Enable Flashes & Games',
+    enableListings: 'Enable Listings',
+    enableNews: 'Enable News',
+    enableArtTagUI: 'Enable Art Tag UI',
+    enableGroupUI: 'Enable Group UI',
   },
 });
 
@@ -425,10 +424,10 @@ export const processHomepageLayoutDocument = makeProcessDocument(
   HomepageLayout,
   {
     propertyFieldMapping: {
-      sidebarContent: "Sidebar Content",
+      sidebarContent: 'Sidebar Content',
     },
 
-    ignoredFields: ["Homepage"],
+    ignoredFields: ['Homepage'],
   }
 );
 
@@ -437,9 +436,9 @@ export function makeProcessHomepageLayoutRowDocument(rowClass, spec) {
     ...spec,
 
     propertyFieldMapping: {
-      name: "Row",
-      color: "Color",
-      type: "Type",
+      name: 'Row',
+      color: 'Color',
+      type: 'Type',
       ...spec.propertyFieldMapping,
     },
   });
@@ -448,16 +447,16 @@ export function makeProcessHomepageLayoutRowDocument(rowClass, spec) {
 export const homepageLayoutRowTypeProcessMapping = {
   albums: makeProcessHomepageLayoutRowDocument(HomepageLayoutAlbumsRow, {
     propertyFieldMapping: {
-      sourceGroupByRef: "Group",
-      countAlbumsFromGroup: "Count",
-      sourceAlbumsByRef: "Albums",
-      actionLinks: "Actions",
+      sourceGroupByRef: 'Group',
+      countAlbumsFromGroup: 'Count',
+      sourceAlbumsByRef: 'Albums',
+      actionLinks: 'Actions',
     },
   }),
 };
 
 export function processHomepageLayoutRowDocument(document) {
-  const type = document["Type"];
+  const type = document['Type'];
 
   const match = Object.entries(homepageLayoutRowTypeProcessMapping).find(
     ([key]) => key === type
@@ -473,15 +472,15 @@ export function processHomepageLayoutRowDocument(document) {
 // --> Utilities shared across document parsing functions
 
 export function getDurationInSeconds(string) {
-  if (typeof string === "number") {
+  if (typeof string === 'number') {
     return string;
   }
 
-  if (typeof string !== "string") {
+  if (typeof string !== 'string') {
     throw new TypeError(`Expected a string or number, got ${string}`);
   }
 
-  const parts = string.split(":").map((n) => parseInt(n));
+  const parts = string.split(':').map((n) => parseInt(n));
   if (parts.length === 3) {
     return parts[0] * 3600 + parts[1] * 60 + parts[2];
   } else if (parts.length === 2) {
@@ -499,16 +498,16 @@ export function parseAdditionalFiles(array) {
   }
 
   return array.map((item) => ({
-    title: item["Title"],
-    description: item["Description"] ?? null,
-    files: item["Files"],
+    title: item['Title'],
+    description: item['Description'] ?? null,
+    files: item['Files'],
   }));
 }
 
 export function parseCommentary(text) {
   if (text) {
-    const lines = String(text).split("\n");
-    if (!lines[0].replace(/<\/b>/g, "").includes(":</i>")) {
+    const lines = String(text).split('\n');
+    if (!lines[0].replace(/<\/b>/g, '').includes(':</i>')) {
       return {
         error: `An entry is missing commentary citation: "${lines[0].slice(
           0,
@@ -527,7 +526,7 @@ export function parseContributors(contributors) {
     return null;
   }
 
-  if (contributors.length === 1 && contributors[0].startsWith("<i>")) {
+  if (contributors.length === 1 && contributors[0].startsWith('<i>')) {
     const arr = [];
     arr.textContent = contributors[0];
     return arr;
@@ -542,17 +541,17 @@ export function parseContributors(contributors) {
     }
     const who = match[1];
     const what = match[3] || null;
-    return { who, what };
+    return {who, what};
   });
 
-  const badContributor = contributors.find((val) => typeof val === "string");
+  const badContributor = contributors.find((val) => typeof val === 'string');
   if (badContributor) {
     return {
       error: `An entry has an incorrectly formatted contributor, "${badContributor}".`,
     };
   }
 
-  if (contributors.length === 1 && contributors[0].who === "none") {
+  if (contributors.length === 1 && contributors[0].who === 'none') {
     return null;
   }
 
@@ -584,7 +583,7 @@ export const documentModes = {
   // processDocument function. Obviously, each specified data file should only
   // contain one YAML document (an error will be thrown otherwise). Calls save
   // with an array of processed documents (wiki objects).
-  onePerFile: Symbol("Document mode: onePerFile"),
+  onePerFile: Symbol('Document mode: onePerFile'),
 
   // headerAndEntries: One or more documents per file; the first document is
   // treated as a "header" and represents data which pertains to all following
@@ -599,12 +598,12 @@ export const documentModes = {
   // aggregate. However, if the processHeaderDocument function fails, all
   // following documents in the same file will be ignored as well (i.e. an
   // entire file will be excempt from the save() function's input).
-  headerAndEntries: Symbol("Document mode: headerAndEntries"),
+  headerAndEntries: Symbol('Document mode: headerAndEntries'),
 
   // allInOne: One or more documents, all contained in one file. Expects file
   // string (or function) and processDocument function. Calls save with an
   // array of processed documents (wiki objects).
-  allInOne: Symbol("Document mode: allInOne"),
+  allInOne: Symbol('Document mode: allInOne'),
 
   // oneDocumentTotal: Just a single document, represented in one file.
   // Expects file string (or function) and processDocument function. Calls
@@ -614,7 +613,7 @@ export const documentModes = {
   // function won't be called at all, generally resulting in an altogether
   // missing property from the global wikiData object. This should be caught
   // and handled externally.
-  oneDocumentTotal: Symbol("Document mode: oneDocumentTotal"),
+  oneDocumentTotal: Symbol('Document mode: oneDocumentTotal'),
 };
 
 // dataSteps: Top-level array of "steps" for loading YAML document files.
@@ -662,7 +661,7 @@ export const dataSteps = [
         return;
       }
 
-      return { wikiInfo };
+      return {wikiInfo};
     },
   },
 
@@ -671,7 +670,7 @@ export const dataSteps = [
     files: async (dataPath) =>
       (
         await findFiles(path.join(dataPath, DATA_ALBUM_DIRECTORY), {
-          filter: (f) => path.extname(f) === ".yaml",
+          filter: (f) => path.extname(f) === '.yaml',
           joinParentDirectory: false,
         })
       ).map((file) => path.join(DATA_ALBUM_DIRECTORY, file)),
@@ -679,7 +678,7 @@ export const dataSteps = [
     documentMode: documentModes.headerAndEntries,
     processHeaderDocument: processAlbumDocument,
     processEntryDocument(document) {
-      return "Group" in document
+      return 'Group' in document
         ? processTrackGroupDocument(document)
         : processTrackDocument(document);
     },
@@ -688,7 +687,7 @@ export const dataSteps = [
       const albumData = [];
       const trackData = [];
 
-      for (const { header: album, entries } of results) {
+      for (const {header: album, entries} of results) {
         // We can't mutate an array once it's set as a property
         // value, so prepare the tracks and track groups that will
         // show up in a track list all the way before actually
@@ -699,7 +698,7 @@ export const dataSteps = [
 
         const albumRef = Thing.getReference(album);
 
-        function closeCurrentTrackGroup() {
+        const closeCurrentTrackGroup = () => {
           if (currentTracksByRef) {
             let trackGroup;
 
@@ -715,7 +714,7 @@ export const dataSteps = [
             trackGroup.tracksByRef = currentTracksByRef;
             trackGroups.push(trackGroup);
           }
-        }
+        };
 
         for (const entry of entries) {
           if (entry instanceof TrackGroup) {
@@ -743,7 +742,7 @@ export const dataSteps = [
         albumData.push(album);
       }
 
-      return { albumData, trackData };
+      return {albumData, trackData};
     },
   },
 
@@ -771,7 +770,7 @@ export const dataSteps = [
         );
       });
 
-      return { artistData, artistAliasData };
+      return {artistData, artistAliasData};
     },
   },
 
@@ -782,7 +781,7 @@ export const dataSteps = [
 
     documentMode: documentModes.allInOne,
     processDocument(document) {
-      return "Act" in document
+      return 'Act' in document
         ? processFlashActDocument(document)
         : processFlashDocument(document);
     },
@@ -798,7 +797,7 @@ export const dataSteps = [
       for (const thing of results) {
         if (thing instanceof FlashAct) {
           if (flashAct) {
-            Object.assign(flashAct, { flashesByRef });
+            Object.assign(flashAct, {flashesByRef});
           }
 
           flashAct = thing;
@@ -809,13 +808,13 @@ export const dataSteps = [
       }
 
       if (flashAct) {
-        Object.assign(flashAct, { flashesByRef });
+        Object.assign(flashAct, {flashesByRef});
       }
 
       const flashData = results.filter((x) => x instanceof Flash);
       const flashActData = results.filter((x) => x instanceof FlashAct);
 
-      return { flashData, flashActData };
+      return {flashData, flashActData};
     },
   },
 
@@ -825,7 +824,7 @@ export const dataSteps = [
 
     documentMode: documentModes.allInOne,
     processDocument(document) {
-      return "Category" in document
+      return 'Category' in document
         ? processGroupCategoryDocument(document)
         : processGroupDocument(document);
     },
@@ -841,7 +840,7 @@ export const dataSteps = [
       for (const thing of results) {
         if (thing instanceof GroupCategory) {
           if (groupCategory) {
-            Object.assign(groupCategory, { groupsByRef });
+            Object.assign(groupCategory, {groupsByRef});
           }
 
           groupCategory = thing;
@@ -852,7 +851,7 @@ export const dataSteps = [
       }
 
       if (groupCategory) {
-        Object.assign(groupCategory, { groupsByRef });
+        Object.assign(groupCategory, {groupsByRef});
       }
 
       const groupData = results.filter((x) => x instanceof Group);
@@ -860,7 +859,7 @@ export const dataSteps = [
         (x) => x instanceof GroupCategory
       );
 
-      return { groupData, groupCategoryData };
+      return {groupData, groupCategoryData};
     },
   },
 
@@ -877,9 +876,9 @@ export const dataSteps = [
         return;
       }
 
-      const { header: homepageLayout, entries: rows } = results[0];
-      Object.assign(homepageLayout, { rows });
-      return { homepageLayout };
+      const {header: homepageLayout, entries: rows} = results[0];
+      Object.assign(homepageLayout, {rows});
+      return {homepageLayout};
     },
   },
 
@@ -895,7 +894,7 @@ export const dataSteps = [
       sortChronologically(newsData);
       newsData.reverse();
 
-      return { newsData };
+      return {newsData};
     },
   },
 
@@ -909,7 +908,7 @@ export const dataSteps = [
     save(artTagData) {
       sortAlphabetically(artTagData);
 
-      return { artTagData };
+      return {artTagData};
     },
   },
 
@@ -921,12 +920,12 @@ export const dataSteps = [
     processDocument: processStaticPageDocument,
 
     save(staticPageData) {
-      return { staticPageData };
+      return {staticPageData};
     },
   },
 ];
 
-export async function loadAndProcessDataDocuments({ dataPath }) {
+export async function loadAndProcessDataDocuments({dataPath}) {
   const processDataAggregate = openAggregate({
     message: `Errors processing data files`,
   });
@@ -938,7 +937,7 @@ export async function loadAndProcessDataDocuments({ dataPath }) {
         return fn(x, index, array);
       } catch (error) {
         error.message +=
-          (error.message.includes("\n") ? "\n" : " ") +
+          (error.message.includes('\n') ? '\n' : ' ') +
           `(file: ${color.bright(
             color.blue(path.relative(dataPath, x.file))
           )})`;
@@ -949,9 +948,9 @@ export async function loadAndProcessDataDocuments({ dataPath }) {
 
   for (const dataStep of dataSteps) {
     await processDataAggregate.nestAsync(
-      { message: `Errors during data step: ${dataStep.title}` },
-      async ({ call, callAsync, map, mapAsync, nest }) => {
-        const { documentMode } = dataStep;
+      {message: `Errors during data step: ${dataStep.title}`},
+      async ({call, callAsync, map, mapAsync, nest}) => {
+        const {documentMode} = dataStep;
 
         if (!Object.values(documentModes).includes(documentMode)) {
           throw new Error(`Invalid documentMode: ${documentMode.toString()}`);
@@ -969,12 +968,12 @@ export async function loadAndProcessDataDocuments({ dataPath }) {
 
           const file = path.join(
             dataPath,
-            typeof dataStep.file === "function"
+            typeof dataStep.file === 'function'
               ? await callAsync(dataStep.file, dataPath)
               : dataStep.file
           );
 
-          const readResult = await callAsync(readFile, file, "utf-8");
+          const readResult = await callAsync(readFile, file, 'utf-8');
 
           if (!readResult) {
             return;
@@ -992,14 +991,14 @@ export async function loadAndProcessDataDocuments({ dataPath }) {
           let processResults;
 
           if (documentMode === documentModes.oneDocumentTotal) {
-            nest({ message: `Errors processing document` }, ({ call }) => {
+            nest({message: `Errors processing document`}, ({call}) => {
               processResults = call(dataStep.processDocument, yamlResult);
             });
           } else {
-            const { result, aggregate } = mapAggregate(
+            const {result, aggregate} = mapAggregate(
               yamlResult,
               decorateErrorWithIndex(dataStep.processDocument),
-              { message: `Errors processing documents` }
+              {message: `Errors processing documents`}
             );
             processResults = result;
             call(aggregate.close);
@@ -1023,7 +1022,7 @@ export async function loadAndProcessDataDocuments({ dataPath }) {
         }
 
         const files = (
-          typeof dataStep.files === "function"
+          typeof dataStep.files === 'function'
             ? await callAsync(dataStep.files, dataPath)
             : dataStep.files
         ).map((file) => path.join(dataPath, file));
@@ -1031,35 +1030,35 @@ export async function loadAndProcessDataDocuments({ dataPath }) {
         const readResults = await mapAsync(
           files,
           (file) =>
-            readFile(file, "utf-8").then((contents) => ({ file, contents })),
-          { message: `Errors reading data files` }
+            readFile(file, 'utf-8').then((contents) => ({file, contents})),
+          {message: `Errors reading data files`}
         );
 
         const yamlResults = map(
           readResults,
-          decorateErrorWithFile(({ file, contents }) => ({
+          decorateErrorWithFile(({file, contents}) => ({
             file,
             documents: yaml.loadAll(contents),
           })),
-          { message: `Errors parsing data files as valid YAML` }
+          {message: `Errors parsing data files as valid YAML`}
         );
 
         let processResults;
 
         if (documentMode === documentModes.headerAndEntries) {
           nest(
-            { message: `Errors processing data files as valid documents` },
-            ({ call, map }) => {
+            {message: `Errors processing data files as valid documents`},
+            ({call, map}) => {
               processResults = [];
 
-              yamlResults.forEach(({ file, documents }) => {
+              yamlResults.forEach(({file, documents}) => {
                 const [headerDocument, ...entryDocuments] = documents;
 
                 const header = call(
-                  decorateErrorWithFile(({ document }) =>
+                  decorateErrorWithFile(({document}) =>
                     dataStep.processHeaderDocument(document)
                   ),
-                  { file, document: headerDocument }
+                  {file, document: headerDocument}
                 );
 
                 // Don't continue processing files whose header
@@ -1070,13 +1069,13 @@ export async function loadAndProcessDataDocuments({ dataPath }) {
                 }
 
                 const entries = map(
-                  entryDocuments.map((document) => ({ file, document })),
+                  entryDocuments.map((document) => ({file, document})),
                   decorateErrorWithFile(
-                    decorateErrorWithIndex(({ document }) =>
+                    decorateErrorWithIndex(({document}) =>
                       dataStep.processEntryDocument(document)
                     )
                   ),
-                  { message: `Errors processing entry documents` }
+                  {message: `Errors processing entry documents`}
                 );
 
                 // Entries may be incomplete (i.e. any errored
@@ -1084,7 +1083,7 @@ export async function loadAndProcessDataDocuments({ dataPath }) {
                 // represented here) - this is intentional! By
                 // principle, partial output is preferred over
                 // erroring an entire file.
-                processResults.push({ header, entries });
+                processResults.push({header, entries});
               });
             }
           );
@@ -1092,11 +1091,11 @@ export async function loadAndProcessDataDocuments({ dataPath }) {
 
         if (documentMode === documentModes.onePerFile) {
           nest(
-            { message: `Errors processing data files as valid documents` },
-            ({ call, map }) => {
+            {message: `Errors processing data files as valid documents`},
+            ({call}) => {
               processResults = [];
 
-              yamlResults.forEach(({ file, documents }) => {
+              yamlResults.forEach(({file, documents}) => {
                 if (documents.length > 1) {
                   call(
                     decorateErrorWithFile(() => {
@@ -1109,10 +1108,10 @@ export async function loadAndProcessDataDocuments({ dataPath }) {
                 }
 
                 const result = call(
-                  decorateErrorWithFile(({ document }) =>
+                  decorateErrorWithFile(({document}) =>
                     dataStep.processDocument(document)
                   ),
-                  { file, document: documents[0] }
+                  {file, document: documents[0]}
                 );
 
                 if (!result) {
@@ -1155,40 +1154,40 @@ export function linkWikiDataArrays(wikiData) {
 
   const WD = wikiData;
 
-  assignWikiData([WD.wikiInfo], "groupData");
+  assignWikiData([WD.wikiInfo], 'groupData');
 
   assignWikiData(
     WD.albumData,
-    "artistData",
-    "artTagData",
-    "groupData",
-    "trackData"
+    'artistData',
+    'artTagData',
+    'groupData',
+    'trackData'
   );
   WD.albumData.forEach((album) =>
-    assignWikiData(album.trackGroups, "trackData")
+    assignWikiData(album.trackGroups, 'trackData')
   );
 
   assignWikiData(
     WD.trackData,
-    "albumData",
-    "artistData",
-    "artTagData",
-    "flashData",
-    "trackData"
+    'albumData',
+    'artistData',
+    'artTagData',
+    'flashData',
+    'trackData'
   );
   assignWikiData(
     WD.artistData,
-    "albumData",
-    "artistData",
-    "flashData",
-    "trackData"
+    'albumData',
+    'artistData',
+    'flashData',
+    'trackData'
   );
-  assignWikiData(WD.groupData, "albumData", "groupCategoryData");
-  assignWikiData(WD.groupCategoryData, "groupData");
-  assignWikiData(WD.flashData, "artistData", "flashActData", "trackData");
-  assignWikiData(WD.flashActData, "flashData");
-  assignWikiData(WD.artTagData, "albumData", "trackData");
-  assignWikiData(WD.homepageLayout.rows, "albumData", "groupData");
+  assignWikiData(WD.groupData, 'albumData', 'groupCategoryData');
+  assignWikiData(WD.groupCategoryData, 'groupData');
+  assignWikiData(WD.flashData, 'artistData', 'flashActData', 'trackData');
+  assignWikiData(WD.flashActData, 'flashData');
+  assignWikiData(WD.artTagData, 'albumData', 'trackData');
+  assignWikiData(WD.homepageLayout.rows, 'albumData', 'groupData');
 }
 
 export function sortWikiDataArrays(wikiData) {
@@ -1213,28 +1212,28 @@ export function sortWikiDataArrays(wikiData) {
 // build, for example).
 export function filterDuplicateDirectories(wikiData) {
   const deduplicateSpec = [
-    "albumData",
-    "artTagData",
-    "flashData",
-    "groupData",
-    "newsData",
-    "trackData",
+    'albumData',
+    'artTagData',
+    'flashData',
+    'groupData',
+    'newsData',
+    'trackData',
   ];
 
-  const aggregate = openAggregate({ message: `Duplicate directories found` });
+  const aggregate = openAggregate({message: `Duplicate directories found`});
   for (const thingDataProp of deduplicateSpec) {
     const thingData = wikiData[thingDataProp];
     aggregate.nest(
       {
         message: `Duplicate directories found in ${color.green(
-          "wikiData." + thingDataProp
+          'wikiData.' + thingDataProp
         )}`,
       },
-      ({ call }) => {
+      ({call}) => {
         const directoryPlaces = Object.create(null);
         const duplicateDirectories = [];
         for (const thing of thingData) {
-          const { directory } = thing;
+          const {directory} = thing;
           if (directory in directoryPlaces) {
             directoryPlaces[directory].push(thing);
             duplicateDirectories.push(directory);
@@ -1253,7 +1252,7 @@ export function filterDuplicateDirectories(wikiData) {
           call(() => {
             throw new Error(
               `Duplicate directory ${color.green(directory)}:\n` +
-                places.map((thing) => ` - ` + inspect(thing)).join("\n")
+                places.map((thing) => ` - ` + inspect(thing)).join('\n')
             );
           });
         }
@@ -1292,64 +1291,64 @@ export function filterDuplicateDirectories(wikiData) {
 export function filterReferenceErrors(wikiData) {
   const referenceSpec = [
     [
-      "wikiInfo",
+      'wikiInfo',
       {
-        divideTrackListsByGroupsByRef: "group",
+        divideTrackListsByGroupsByRef: 'group',
       },
     ],
 
     [
-      "albumData",
+      'albumData',
       {
-        artistContribsByRef: "_contrib",
-        coverArtistContribsByRef: "_contrib",
-        trackCoverArtistContribsByRef: "_contrib",
-        wallpaperArtistContribsByRef: "_contrib",
-        bannerArtistContribsByRef: "_contrib",
-        groupsByRef: "group",
-        artTagsByRef: "artTag",
+        artistContribsByRef: '_contrib',
+        coverArtistContribsByRef: '_contrib',
+        trackCoverArtistContribsByRef: '_contrib',
+        wallpaperArtistContribsByRef: '_contrib',
+        bannerArtistContribsByRef: '_contrib',
+        groupsByRef: 'group',
+        artTagsByRef: 'artTag',
       },
     ],
 
     [
-      "trackData",
+      'trackData',
       {
-        artistContribsByRef: "_contrib",
-        contributorContribsByRef: "_contrib",
-        coverArtistContribsByRef: "_contrib",
-        referencedTracksByRef: "track",
-        artTagsByRef: "artTag",
-        originalReleaseTrackByRef: "track",
+        artistContribsByRef: '_contrib',
+        contributorContribsByRef: '_contrib',
+        coverArtistContribsByRef: '_contrib',
+        referencedTracksByRef: 'track',
+        artTagsByRef: 'artTag',
+        originalReleaseTrackByRef: 'track',
       },
     ],
 
     [
-      "groupCategoryData",
+      'groupCategoryData',
       {
-        groupsByRef: "group",
+        groupsByRef: 'group',
       },
     ],
 
     [
-      "homepageLayout.rows",
+      'homepageLayout.rows',
       {
-        sourceGroupsByRef: "group",
-        sourceAlbumsByRef: "album",
+        sourceGroupsByRef: 'group',
+        sourceAlbumsByRef: 'album',
       },
     ],
 
     [
-      "flashData",
+      'flashData',
       {
-        contributorContribsByRef: "_contrib",
-        featuredTracksByRef: "track",
+        contributorContribsByRef: '_contrib',
+        featuredTracksByRef: 'track',
       },
     ],
 
     [
-      "flashActData",
+      'flashActData',
       {
-        flashesByRef: "flash",
+        flashesByRef: 'flash',
       },
     ],
   ];
@@ -1364,35 +1363,35 @@ export function filterReferenceErrors(wikiData) {
   const aggregate = openAggregate({
     message: `Errors validating between-thing references in data`,
   });
-  const boundFind = bindFind(wikiData, { mode: "error" });
+  const boundFind = bindFind(wikiData, {mode: 'error'});
   for (const [thingDataProp, propSpec] of referenceSpec) {
     const thingData = getNestedProp(wikiData, thingDataProp);
     aggregate.nest(
       {
         message: `Reference errors in ${color.green(
-          "wikiData." + thingDataProp
+          'wikiData.' + thingDataProp
         )}`,
       },
-      ({ nest }) => {
+      ({nest}) => {
         const things = Array.isArray(thingData) ? thingData : [thingData];
         for (const thing of things) {
           nest(
-            { message: `Reference errors in ${inspect(thing)}` },
-            ({ filter }) => {
+            {message: `Reference errors in ${inspect(thing)}`},
+            ({filter}) => {
               for (const [property, findFnKey] of Object.entries(propSpec)) {
                 if (!thing[property]) continue;
-                if (findFnKey === "_contrib") {
+                if (findFnKey === '_contrib') {
                   thing[property] = filter(
                     thing[property],
-                    decorateErrorWithIndex(({ who }) => {
+                    decorateErrorWithIndex(({who}) => {
                       const alias = find.artist(who, wikiData.artistAliasData, {
-                        mode: "quiet",
+                        mode: 'quiet',
                       });
                       if (alias) {
                         const original = find.artist(
                           alias.aliasedArtistRef,
                           wikiData.artistData,
-                          { mode: "quiet" }
+                          {mode: 'quiet'}
                         );
                         throw new Error(
                           `Reference ${color.red(
@@ -1407,7 +1406,7 @@ export function filterReferenceErrors(wikiData) {
                     {
                       message: `Reference errors in contributions ${color.green(
                         property
-                      )} (${color.green("find.artist")})`,
+                      )} (${color.green('find.artist')})`,
                     }
                   );
                   continue;
@@ -1421,7 +1420,7 @@ export function filterReferenceErrors(wikiData) {
                     {
                       message: `Reference errors in property ${color.green(
                         property
-                      )} (${color.green("find." + findFnKey)})`,
+                      )} (${color.green('find.' + findFnKey)})`,
                     }
                   );
                 } else {
@@ -1429,9 +1428,9 @@ export function filterReferenceErrors(wikiData) {
                     {
                       message: `Reference error in property ${color.green(
                         property
-                      )} (${color.green("find." + findFnKey)})`,
+                      )} (${color.green('find.' + findFnKey)})`,
                     },
-                    ({ call }) => {
+                    ({call}) => {
                       try {
                         call(findFn, value);
                       } catch (error) {
@@ -1460,14 +1459,14 @@ export function filterReferenceErrors(wikiData) {
 // main wiki build process.
 export async function quickLoadAllFromYAML(
   dataPath,
-  { showAggregate: customShowAggregate = showAggregate } = {}
+  {showAggregate: customShowAggregate = showAggregate} = {}
 ) {
   const showAggregate = customShowAggregate;
 
   let wikiData;
 
   {
-    const { aggregate, result } = await loadAndProcessDataDocuments({
+    const {aggregate, result} = await loadAndProcessDataDocuments({
       dataPath,
     });